Boosting Code Readability and Manageability in ASP.NET Core

In Software development, code readability transforms complexity into clarity.  Martin Fowler said:

Any fool can write code that a computer can understand. Good programmers write code that humans can understand.

Code should be readable and manageable so other developers can understand and work without additional fatigue. .NET has many ways to enhance readability and manageability. This post will explore C#'s extension methods by listing five use cases that I use on all of the ASP.NET Core projects I'm working on.

What are Extension methods in C#?

In C#, the extension method allows developers to add new functionalities to existing types and classes without accessing or modifying the code. They were introduced in C# 3.0 and have been a core part of the .NET ecosystem for better programming offering a clean and flexible approach to code organization.

Let's see some use cases for adding extension methods. You don't need any special permission or a deep understanding of the class. Just define a static class with a static method and write your customized logic in it. The first argument of the method should be the original class whose method you are adding with this keyword.

Use case 1: ASP.NET's  IServiceCollection extension method for dependency injection

We can add an extension method to ASP.NET Core applications IServiceCollection for code readability. This collection registers dependency injections used in web applications such as repositories, services, mappers, custom middleware, etc. This reduces the code complexity in Program.cs by separating register logic with extension methods. The technique is handy for scalable applications with hundreds of injecting services crowding Program.cs files. 

public static class DependencyInjection
{
    public static void InjectedServices(this IServiceCollection services)
    {
        // User DI
        services.AddScoped<IUserRepo, UserRepo>();
        services.AddScoped<IUserService, UserService>();

        services.AddScoped<ILanguageRepo, LanguageRepo>();
        services.AddScoped<ILanguageService, LanguageService>();

        // Material Repos
        services.AddScoped<IMaterialRepo, MaterialRepo>();
        services.AddScoped<IMaterialService, MaterialService>();
        
        services.AddScoped<ICostRepo, CostRepo>();
        services.AddScoped<ICostService, CostService>();
        services.AddScoped<ITravelCostRepo, TravelCostRepo>();
        services.AddScoped<ITravelCostService, TravelCostService>();

        services.AddScoped<NotifyMiddleware>();
        services.AddHttpContextAccessor();
    }
}

You can see I have added all the registering logic in the extension method of IServiceCollection. I further clustered dependency injections into regions for better manageability. We have reduced the Program class fatigue with the extension method. 

Use case 2: String extension method for extending string functionality

Extension methods can add some user-defined functionality to common classes like strings:

public static class StringExtensions
{
    public static bool IsValidEmail(this string email)
    {
        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address == email;
        }
        catch
        {
            return false;
        }
    }
}

// Usage
string email = "example@domain.com";
bool isValid = email.IsValidEmail(); 
Console.WriteLine(isValid); // Output: True

email = "exampleomain.com";
isValid = email.IsValidEmail(); 
Console.WriteLine(isValid); // Output: False

Use case 3: Simplifying user authentication with ClaimsPrincipal

Extension methods provide a clean way to check the role from ClaimsPrincipal of the System.Security.Claims namespace:

public static class ClaimsPrincipalExtensions
{
    public static bool IsInRole(this ClaimsPrincipal user, string role)
    {
        return user?.Identity?.IsAuthenticated == true && user.IsInRole(role);
    }
}

// Usage
if (User.IsInRole("Admin"))
{
    // Admin-specific logic
}

Use case 4: Data validation in MVC controllers

Similarly, we can apply controller validation in a more readable way with ModelStateDictionary extension method:

public static class ModelStateExtensions
{
    public static void AgeValidationError(this ModelStateDictionary modelState, string key, string message)
    {
        modelState.AddModelError(key, message);
    }
}


// Usage in a controller
if (model.Age < 18)
{
    ModelState.AddValidationError("Age", "Age must be 18 or older.");
}

Use case 5: Filtration method in DbContext

We can extend DbContext to enclose methods for custom filtration. This provides a shorthand to get filtered results and avoid writing complex LINQ queries multiple times:

public static class DbContextExtensions
{
    public static IQueryable<User> GetYoungUsers(this DbContext context)
    {
        return context
            .Set<User>()
            .Where(u => u.Age >= 18 && u.Age <= 40)
            .ToList();
    }
}

// Usage
var youngUsers = _context.GetYoungUsers();

Benefits of extension methods

  • Improve Readability: Extension methods separate complex logic by encapsulating it in a separate extension class making the main code easy to read and understand
  • Enhance Maintainability: Logics are enclosed in a central place that makes it easy to change the code when needed.
  • Reusability: While the code is at the central extension method, Users don't need to write that code again and again. Rather the extended method can be called where required.
  • Enhance code organization: Implementation of the extension method fulfills one of the most important concepts of code organization - separation of concern. Separate classes hold the definition of separate logic.
  • Better Domain-Specific Language (DSL) Creation: They enable more domain-specific implementation for the business logic and domain.

Summary

Developers create applications, and they need ease of readability and understanding. The more readable an application is, the better and easier its maintainability is. For clean code, the .NET ecosystem utilizes C# extension methods that make its classes and structs extendable. That means users can add custom methods in any class, centralizing their logic and using those methods as the other methods of that class.

We have seen a few examples of how the extension method reduces code complexity and separates logic. Besides, it adds functionality to any built-in or user-defined class without changing its code. With the power of extension methods, developers can write readable and manageable code.