5
\$\begingroup\$

I have a generic Get function that returns a list of objects based on the name of the desired type of object. This has let to an enormous case/switch statement that delegates out to the specific Get functions for the given type. I'd like use reflection or something similar to avoid this huge case/switch. I can't directly cast from the child types to Record.

Here's the case/switch

public static IEnumerable<Record> GetChildren(string tenantName, string parentCollection, string parentId,
    string childCollection)
{
    M5Record parent;
    try
    {
        parent = GetById(tenantName, parentCollection, parentId);
    }
    catch (InvalidOperationException)
    {
        throw new WebFaultException<ErrorResponse>(
            new ErrorResponse { Error = "Not found" }, HttpStatusCode.NotFound);
    }
    childCollection = childCollection.ToLower();
    if (parent == null)
        return new List<Record>();
    switch (childCollection)
    {
        case "assets":
        case "asset":
            return parent.GetAssets();
        case "applications":
        case "application":
            return parent.GetApplications();
        case "appointment":
            return parent.GetAppointments();
        case "bonds":
        case "bond":
            return parent.GetBonds();
        case "buildings":
        case "building":
            return parent.GetBuildings();
        case "certificates":
        case "certificate":
            return parent.GetCertificates();
        case "checklistitems":
        case "checklistitem":
            return parent.GetCheckListItems();
        case "companies":
        case "company":
            return parent.GetCompanies();
        case "complaints":
        case "complaint":
            return parent.GetComplaints();
        case "condition":
            return parent.GetConditions();
        case "contacts":
        case "contact":
            return parent.GetContacts();
        case "courtappearance":
            return parent.GetCourtAppearances();
        case "document":
            return parent.GetDocuments();
        case "electronicsignatures":
        case "electronicsignature":
            return parent.GetElectronicSignatures();
        case "escrowaccount":
            return parent.GetEscrowAccounts();
        case "events":
        case "event":
            return parent.GetEvents();
        case "fees":
        case "fee":
            return parent.GetFees();
        case "history":
            return parent.GetHistory();
        case "inspections":
        case "inspection":
            return parent.GetInspections();
        case "issue":
            return parent.GetIssues();
        case "licenses":
        case "license":
            return parent.GetLicenses();
        case "maintenances":
        case "maintenance":
            return parent.GetMaintenances();
        case "meetings":
        case "meeting":
            return parent.GetMeetings();            
        case "occupants":
        case "occupant":
            return parent.GetOccupants();
        case "owners":
        case "owner":
            return parent.GetOwners();
        case "parcels":
        case "parcel":
            return parent.GetParcels();
        case "project":
            return parent.GetProjects();
        case "permits":
        case "permit":
            return parent.GetPermits();
        case "renewals":
        case "renewal":
            return parent.GetRenewals();
        case "tasks":
        case "task":
            return parent.GetTasks();
        case "time":
            return parent.GetTimes();
        // Sorry, this got chaotic fast.
        case "stockpiletransaction":
            string[] approvedParents = { "Stockpile", "Vehicle", "Maintenance", "WorkOrder" };
            if (approvedParents.Contains(parent.GetType().Name))
            {
                return parent.GetStockpileTransactions();
            }
            return null;
        case "subcode":
        case "subcodes":
            return parent.GetSubcodes();
        case "summons":
            return parent.GetSummons();
        case "subscription":
        case "subscriptions":
            return parent.GetSubscriptions();
        case "stopworkorder":
            return parent.GetStopWorkOrders();
        case "variances":
        case "variance":
            return parent.GetVariances();
        case "violations":
        case "violation":
            return parent.GetViolations();
        case "workflowstep":
            return parent.GetWorkflowSteps();
        case "workorders":
        case "workorder":
            return parent.GetWorkOrders();
        default:
            return new List<Record>();
    }
}

Here's an example of one of the delegate functions. The delegate functions are different enough where it's not feasible to merge them into one function. It gets all the applications that are mapped to the parent record.

public IEnumerable<Record> GetApplications()
{
    using (var conn = new SqlConnection(_tenant.ConnectionString))
    {
        const string sql = "SELECT * FROM ApplicationView WHERE Application_ID IN " +
                           "(SELECT Entity2Code_ID FROM mpEntity WHERE COALESCE(Deleted, 0) = 0 " +
                           "AND TableName1 = @TableName1 AND Entity1Code_ID = @Entity1Code_ID AND TableName2 = 'Application') " +
                           "AND COALESCE(Tenant_ID, 0) = COALESCE(@Tenant_ID, 0) ";
        return conn.Query<Application>(sql, new
        {
            _tenant.Tenant_ID,
            TableName1 = GetType().Name,
            Entity1Code_ID = GetId()
        });
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ You should consider how hard this will be to maintain, is it going to be convenient to break code for any future changes ? The Enterprise will generally favour lookup tables (preferably in memory) rather than hard coded rulebooks. That may mean dynamic SQL, but does not have to. \$\endgroup\$
    – mckenzm
    Nov 12, 2016 at 2:47

2 Answers 2

5
\$\begingroup\$

Oooh I love these. We're gonna reflect the snot out of this to finish solving it. :)


First let's think about what all the methods have in common.

  • Each one of them is a pluralized version of the object they're returning. That's good.
  • Each one of them is named the exact same as the object they return, excepting the prefix of Get. That's good.
  • Each one of them is a pascal-case version of the object they return. That's good.
  • Each one of them takes no arguments and returns the call immediately. That's good.

There are three exceptions to the rule: history, company and stockpiletransaction which is fine.


So, from here we can devise an experiment.

First, we'll take your input childCollection and manipulate it a little:

var methodName = "get";
methodName += childCollection;
if (methodName.Last() != 's')
{
    methodName += 's';
}

That should sanitize childCollection into the method names.

Make it a method and viola:

string SanitizeName(string childCollection)
{
    var methodName = "get";
    methodName += childCollection;

    if (methodName.Last() != 's')
    {
        methodName += 's';
    }

    return methodName;
}

Next, we can invoke the method pretty easily on the parent:

var method = typeof(M5Record).GetMethods().Where(x => x.Name.ToLower() == SanitizeName(childCollection)).FirstOrDefault();

Then just invoke it:

return (IEnumerable<Record>)method.Invoke(parent, null);

You only need three conditions (as we said above):

  • If childCollection == "history" call parent.GetHistory() (if you change the method name from GetHistory to GetHistorys, this case is no longer required)
  • If childCollection == "stockpiletransaction" do all that work
  • If method == null then return new List<Record>()

If you have C#6.0, you can do the third condition (and make sure null is never returned) with a modification to the call:

return (IEnumerable<Record>)(method?.Invoke(parent, null) ?? new List<Record>());

A not so obvious advantage of this approach is that it supports new API endpoints in addition to the existing ones.

I don't have a lot of time to make this answer more exemplary, but this should get you started.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ You should change the Name.ToLower() to Name.Equals(x, StringComparison.OrdinalIgnoreCase) or I'll need to review your code too ;-P \$\endgroup\$
    – t3chb0t
    Nov 11, 2016 at 19:50
  • \$\begingroup\$ You've missed an edge case (company/companies). It's probably also worth mentioning that you're extending the current API, so whilst currently 'workflowsteps' wouldn't work, with your approach it would (which may or may not be desirable). \$\endgroup\$
    – forsvarir
    Nov 11, 2016 at 20:12
6
\$\begingroup\$

The first thing I would say is that the API is not well-defined. The upstream code should be calling the specific method. The fact that you call a single method based on multiple names is fragile. It seems like you are trying to stick an entire API into a single method.

It would be much better if you exposed one method per "childCollection", for example:

//This method is exposed as part of the web API
public static GetAssets(string tenantName, string parentCollection, string parentId)
{
    var parent = GetParent(tenantName, parentCollection, parentId);

    if (parent == null)
        return new List<Record>();

    //...
}

private static M5Record GetParent(string tenantName, string parentCollection, string parentId) 
{
    try
    {
        return GetById(tenantName, parentCollection, parentId);
    }
    catch (InvalidOperationException)
    {
        throw new WebFaultException<ErrorResponse>(
            new ErrorResponse { Error = "Not found" }, HttpStatusCode.NotFound);
    }
}

And so on for each one of your childCollection cases. This gives you much more flexibility when extending your API or even adjusting what parameters go to what methods (for example, the stockpiletransaction case).

Another thing it gives you is not having to worry about the locale of the text passed in for the switch statement. You are assuming English but what if somebody tried using your API in Spanish? Using a well-defined API forces them to call concrete methods instead of methods by text name.


If you really are stuck here, you can use a delegate "registration" method. For example:

public class MyWebAPI
{
    private static Dictionary<string, Func<M5Record, IEnumerable<Record>>> _registry;


    static MyWebAPI()
    {
        _registry = new Dictionary<string, Func<M5Record, IEnumerable<Record>>>(StringComparer.OrdinalIgnoreCase);
        _registry["asset"] = (p) => { return p.GetAssets(); };
        _registry["assets"] = _registry["asset"];
        //... etc
    }

    public static MyMethod(string methodName)
    {
        var parent = ...;
        _registry[methodName](parent);
    }
} 

Which allows you to easily add cases. You would also need to check that the desired name exists in the dictionary before trying to call it, but you should be able to get the idea here.

The advantage here over reflection is that you do not have to do introspection to figure out if the method exists and is significantly faster than reflection. It also handles the issue of case (as in upper/lower case) a lot better. With reflection, GetAssets and getAssets and getassets may all be different methods (at least in C# if you want to be non-CLS compliant).

You could do reflection if you wanted to, and here is the code that you would need to do it:

private static IEnumerable<Record> CallMethodByName(string methodName, M5Record parent)
{
    var method = typeof(M5Record).GetMethod("get" + methodName,
        BindingFlags.IgnoreCase | BindingFlags.Static | BindingFlags.Public);

    if (method == null)
        method = typeof(M5Record).GetMethod("get" + methodName + "s",
            BindingFlags.IgnoreCase, BindingFlags.Static, BindingFlags.Public);

    if (method != null)
        return method.Invoke(parent, null) as IEnumerable<Record>;

    return new List<Record>();    //To keep with your pattern
}

Which handles (in a very basic way) the plural and non-plural cases for the method name being passed in, so you could pass in raw user input and it would check for both.

\$\endgroup\$
7
  • \$\begingroup\$ I completely agree with everything you just said, and this is the correct way to go about it. Unfortunately, I don't have any control over the front end code that calls this endpoint, so changing how the endpoints are called isn't a feasible solution. \$\endgroup\$ Nov 11, 2016 at 19:11
  • \$\begingroup\$ @MorganThrapp I updated with an example of what you could do if you are stuck with this way of doing things. \$\endgroup\$
    – Ron Beyer
    Nov 11, 2016 at 19:19
  • \$\begingroup\$ I'm not sure why the dictionary is better than the case/switch. Can you elaborate on what the advantages are? \$\endgroup\$ Nov 11, 2016 at 19:21
  • 1
    \$\begingroup\$ The reflection solution here won't work for OP's situation (without just a little modification) because of stockpiletransaction which requires a little additional parameter validation. \$\endgroup\$ Nov 11, 2016 at 19:44
  • 1
    \$\begingroup\$ @EBrown Right, it would require an edge case check. This is another reason I would prefer the delegate registry over reflection, the logic can easily be placed inside the Func and would not require edge case checks inside the main api method. \$\endgroup\$
    – Ron Beyer
    Nov 11, 2016 at 19:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.