Andy Crouch - Code, Technology & Obfuscation ...

The Power Of .Net Generics

Man Cutting Cloth From Template

Photo: Dương Trần Quốc - Unsplash

Generics are a genuinely useful and powerful tool in .Net. Sometimes developers see part of the potential but not the whole story. This was the case in a recent Pull Request I reviewed.

The Pull Request in question related to some cleaning up that a developer had undertaken within our user management pages. These are standard lists of different types of user which can be filtered, exported to excel etc. The original developer of these pages had done a great Ctrl-C, Crtl-V for the first 2 different types. The developer of the Pull Request I was reviewing had set about to clean this up and used generics to do so. So far so good. The problem came when I noticed a raft of switch statements throughout two of the methods in the revised service. For example:

public MemoryStream GenerateExcelFile<T>(string searchString) where T : IUserManagementModel, new()
{
    var obj = GetUsersQuery<T>(searchString: searchString, page: 0, countPerPage: 0, sort: null, sortAsc: true);
	
    switch (obj.UserType)
    {
        case UserType.Customer:
            return _excelExportService.GenerateExcelFile(obj.UserMembershipList.Select(x => x as CustomerModel).ToList());
        case UserType.Supplier:
            return _excelExportService.GenerateExcelFile(obj.UserMembershipList.Select(x => x as SupplierModel).ToList());
        case UserType.Admin:
            return _excelExportService.GenerateExcelFile(obj.UserMembershipList.Select(x => x as UserProfileModel).ToList());
    }

    throw new Exception($"Unexpected UserType: {obj.UserType}");
}

When I see a switch statement like this I have two clear thoughts. One, this does not allow for easy further extension or modification and two, there is actually just one line of code that needs executing on different models so lets write the code to do that.

public MemoryStream GenerateExcelFile<T>(string searchString) where T : class, IUserManagementModel, new()
{
    var userQuery = GetUsersQuery<T>(searchString: searchString, page: 0, countPerPage: 0, sort: null, sortAsc: true);
    
    return _excelExportService.GenerateExcelFile<T>(userQuery.UserMembershipList.Select(x => (T)x).ToList());
}

This is a nice solution that allows for further IUserManagementModel based objects to be exported to Excel without this code needing to change. This is a real world example of Bertrand Meyer’s Open/Closed principle that I mentioned in my last post.

As I continued the review I came across the issue further as shown in the GetUsersQuery() method show below:

private UserQueryResult<T> GetUsersQuery<T>(string searchString, int page, int countPerPage, string sort, bool sortAsc) where T : IUserManagementModel, new()
{
    IQueryable<GenericUserObject> query;
    UserType userType;
    
    if (typeof(T) == typeof(CustomerModel))
    {
        userType = UserType.Customer;
        query = (from c in _context.Customer
                 where c.Deleted == false
                 select new GenericUserObject { Id = c.Id, Email = c.Email, FullName = c.Name, Name = c.Name });
    }
    else if (typeof(T) == typeof(SupplierModel))
    {
        userType = UserType.Supplier;
        query = (from s in _context.Supplier
                 where s.Deleted == false
                 select new GenericUserObject { Id = s.Id, Email = s.Email, FullName = s.Name, Name = s.Name });
    }
    else if (typeof(T) == typeof(UserProfileModel))
    {
        userType = UserType.Admin;
        query = (from a in _context.Supplier
                 where a.Deleted == false
                 select new GenericUserObject { Id = a.Id, Email = a.Email, FullName = a.Name, Name = a.Name });
    }
    else
    {
        throw new Exception($"Unknown Type: {typeof(T)}");
    }

    // some other code ...
}

Once again the multiple switch statements were replicating code on objects that were all based on IUserManagementModel. It seemed obvious that the UserType and query values could actually be returned from the models. So that’s what we did:

private UserQueryResult<T> GetUsersQuery<T>(string searchString, int page, int countPerPage, string sort, bool sortAsc) where T : IUserManagementModel, new()
{
    IUserManagementModel model = new T();
    UserType userType = model.UserType();
    IQueryable<GenericUserObject> query = model.GetModelQueryFor(_context);

    // some other code ...
}

This was achieved by modifying the IUserManagementModel interface:

IQueryable<GenericUserObject> GetModelQueryFor(DbContext dbContext);
IOrderedQueryable<GenericUserObject> ApplySort(IQueryable<GenericUserObject> query, string sortBy, bool asc);

(The ApplySort() method follows the same approach as GetModelQueryFor() to remove other code in the class that started off with a switch statement handling sorting the query result).

By the time we’d finished pairing up, the developer and myself were happy with the results. We’d removed the switch statements and realised more of the power of generics. That power is released by ensuring that you push all data and behaviour down to your abstractions. That way, your generic code can be written to interact with the abstracted type and not the instance of the objects.

If you’d like to discuss any of my thoughts here then as always please contact me via twitter or email.