Preventing Redundant DI Code in ASP.NET Core

By FoxLearn 2/3/2025 8:17:39 AM   52
When working with controllers in ASP.NET Core or ASP.NET Core MVC applications, you may encounter repetitive code related to dependency injection (DI).

For instance, you may find yourself repeatedly injecting the same services into various controllers. This redundancy violates the DRY (Don’t Repeat Yourself) principle, making your code harder to maintain and more error-prone.

Understanding ASP.NET Core’s Base Controller Classes

In ASP.NET Core, there are two primary base controller classes: ControllerBase and Controller.

  • ControllerBase: This class is designed for API controllers. It implements the IController interface and provides methods for routing, HttpContext integration, and managing things like ViewData and TempData. It is intended for API-centric applications where views aren’t needed.
  • Controller: This class extends ControllerBase and includes additional functionality for working with views (like View() and Redirect()). It’s typically used in ASP.NET Core MVC applications.

For API projects, you should derive your controllers from ControllerBase. For MVC projects, use Controller.

Create a Custom Base Controller to Avoid Redundancy

To reduce redundancy in dependency injection across controllers, you can create your own base controller that encapsulates common services. Here's how to implement it.

1. Define Your Entity Class

Let's start with a simple Order entity:

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Address { get; set; }
}

2. Create an Interface for Service Logic

Define an interface that declares the service methods needed by your controllers:

public interface IOrderManager
{
    void Process(Order order);
}

3. Implement the Service

Now, implement the interface in a concrete service class:

public class OrderManager : IOrderManager
{
    public void Process(Order order)
    {
        // Implementation here...
    }
}

4. Define the Custom Base Controller

Create a base controller class that handles common service injections. This will help eliminate the need to inject these services into each individual controller.

[Route("api/[controller]")]
[ApiController]
public class BaseController : ControllerBase
{
    protected readonly ILogger<BaseController> _logger;
    protected readonly IOrderManager _orderManager;

    public BaseController(ILogger<BaseController> logger, IOrderManager orderManager)
    {
        _logger = logger;
        _orderManager = orderManager;
    }
}

5. Create a Derived Controller

Now, create a controller that derives from the custom BaseController. This allows you to access the injected services without repeating the DI code in each controller.

[Route("api/[controller]")]
[ApiController]
public class OrderController : BaseController
{
    public OrderController(ILogger<OrderController> logger, IOrderManager orderManager)
        : base(logger, orderManager)
    {
    }

    [HttpGet]
    public string Get()
    {
        _logger.LogInformation("Getting order details");
        return "OrderController: Get method";
    }

    [HttpPost]
    public IActionResult Process(Order order)
    {
        _orderManager.ProcessOrder(order);
        return Ok("Order processed");
    }
}

The code won’t compile because you need to implement a constructor in the derived controller with the same arguments as the base controller. This would defeat the purpose of using a base controller to reduce redundancy.

To solve this, you can use HttpContext.RequestServices.GetService to access the services within the controller. However, note that HttpContext will be null if accessed in the constructor, so services should be resolved during the request lifecycle instead.

6. Resolve Services Using HttpContext.RequestServices

This allows you to resolve services dynamically during the request lifecycle.

Here’s how to modify your base controller to use HttpContext.RequestServices:

public abstract class BaseController<T> : ControllerBase where T : BaseController<T>
{
    private ILogger<T> _logger;
    protected ILogger<T> Logger => _logger ?? (_logger = HttpContext.RequestServices.GetService<ILogger<T>>());
}

And update your derived controller like this:

[Route("api/[controller]")]
[ApiController]
public class OrderController : BaseController<OrderController>
{
    private readonly IOrderManager _orderManager;

    public OrderController(IOrderManager orderManager)
    {
        _orderManager = orderManager;
    }

    [HttpGet]
    public string Get()
    {
        Logger.LogInformation("Inside Get method");
        return "OrderController: Get method";
    }

    [HttpPost]
    public void ProcessOrder(Order order)
    {
        _orderManager.ProcessOrder(order);
    }
}

7. Register Services in ConfigureServices

Finally, ensure that your services are properly registered in the ConfigureServices method of your Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOrderManager, OrderManager>();
    services.AddControllers();
}

Benefits of Using a Base Controller

By using a custom base controller:

  • Code Reusability: You centralize common functionality (like dependency injection) in one place, making your code cleaner and easier to maintain.
  • Adherence to DRY Principle: You avoid repetitive DI code in each controller.
  • Easier Testing: With dependencies injected into the base controller, unit testing becomes more straightforward.

Avoiding redundancy in DI code in ASP.NET Core can greatly improve your project's maintainability. By implementing a base controller, you can inject common dependencies once and reuse them across multiple controllers, reducing boilerplate code and making your application easier to manage.