How to avoid redundant DI code in ASP.NET Core

Take advantage of base controllers to avoid dependency injection code duplication and enforce the DRY principle in ASP.NET Core MVC.

How to avoid redundant DI code in ASP.NET Core
Thinkstock

When working with controllers in web applications using ASP.NET Core or ASP.NET Core MVC, you might have encountered code redundancy. For example, you might have come across controllers that use dependency injection (DI) to inject services that they need. If the code that injects the dependencies has been reused in multiple controllers, then you have code redundancy and violation of the DRY principle (don’t repeat yourself).

This article looks at DI code redundancy and shows how to build your custom base controller to avoid such issues. To work with the code examples provided in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.

Create an ASP.NET Core MVC project in Visual Studio

First off, let’s create an ASP.NET Core project in Visual Studio 2019. Following these steps will create a new ASP.NET Core MVC project in Visual Studio 2019.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web App (Model-View-Controller)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences.
  7. Click Next.
  8. In the “Additional Information” window shown next, select .NET 5.0 as the target framework from the drop-down list at the top. Leave the “Authentication Type” as “None” (default).
  9. Ensure that the check boxes “Enable Docker,” “Configure for HTTPS,” and “Enable Razor runtime compilation” are unchecked as we won’t be using any of those features here.
  10. Click Create.

A new ASP.NET Core MVC project will be created. We’ll use this project to work with dependency injection in the subsequent sections of this article.

Now follow the steps outlined below to create additional controllers in your project.

  1. Right-click on the Controllers solution folder.
  2. Select Add -> Controller.
  3. In the “Add New Scaffolded Item” dialog, select API as the template (by default MVC will be selected).
  4. Select the item “API Controller with read/write actions.”
  5. Click Add.
  6. In the “Add New Item” dialog shown next, specify a name for your new controller.
  7. Click Add.

Built-in base controller classes in ASP.NET Core

There are two base classes for controllers, namely ControllerBase and Controller. The ControllerBase class implements the IController interface and provides the implementation for several methods and properties. It defines an abstract method named ExecuteCore that is used to locate the action method and execute it. You should use ControllerBase whenever you are building your APIs.

The Controller class extends the ControllerBase class, provides the ExecuteCore method, and adds several methods you can use in your controller classes such as View() and Redirect(). Like ControllerBase, the Controller class is a base controller class with view support. Hence you should use the Controller class whenever you’re creating your controllers in ASP.NET Core MVC. The ControllerBase class provides the necessary integration with routing and HttpContext so that you can leverage them. It also contains the code required for managing ViewData and TempData.

Implement a base controller class in ASP.NET Core

When we create a new API controller class in ASP.NET Core, it extends the ControllerBase class by default. Next, we’ll create our implementation of a base controller. Our base controller class will extend the ControllerBase class of the framework.

Here is an entity class we will be using in this example:

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

Now create the following interface called IOrderManager that contains the declaration of a method named ProcessOrder.

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

Next create the OrderManager class that extends the IOrderManager interface and implements its ProcessOrder method.

public class OrderManager : IOrderManager
{
   public void ProcessOrder(Order order)
   {
      throw new System.NotImplementedException();
   }
}

The following code listing shows how you can create a base controller class by deriving it from the ControllerBase class.

[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;
   }
}

Extend your custom base controller in ASP.NET Core

You can now create your controllers by deriving from this custom base controller we just created. The following code listing illustrates how you can create your controller class by extending it from the newly created base controller class.

[Route("api/[controller]")]
[ApiController]
public class OrderController : BaseController
{
   private readonly ILogger<OrderController> _logger;
  [HttpGet]
   public string Get()
   {
       return "OrderController";
   }
   [HttpPost]
   public void ProcessOrder(Order order)
   {
      _orderManager.ProcessOrder(order);
   }
}

However, you will discover that the above code will not compile. Here is the error you will be presented with in Visual Studio:

base controller class error IDG

The error states that you need to implement another argument constructor with the same arguments as the constructor of the BaseController class. But why? If you’re going to replicate the dependency injection code in all of your controllers that extend the base controller class you’ve created, it defeats the purpose of extending the class.

To solve this issue, you can take advantage of the HttpContext.RequestServices.GetService<T> extension method. Remember, the HttpContext instance will be null if you’re trying to access it inside the constructor of your controllers.

The services that are available as part of an ASP.NET Core request are accessible through the HttpContext.RequestServices collection. This means that when you request a service, the request will be resolved from this collection.

Add HttpContext to your base controller class in ASP.NET Core

Here’s the updated source code of our BaseController class.

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>>());       
}

The OrderController class extends this abstract BaseController class as shown in the code listing given below.

[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("Hello World!");
            return "Inside the Get method of OrderController";
        }
        [HttpPost]
        public void ProcessOrder(Order order)
        {
            _orderManager.ProcessOrder(order);
        }
    }

And that’s it! The OrderController can leverage the Logger instance as well as take advantage of constructor injection to inject other services.

Finally, remember to register your services in the ConfigureServices method of your Startup class as shown below.

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

It would be best to resolve dependencies using a parameterized constructor, i.e., by using constructor injection. This will help you create classes that are easier to test and easier to maintain.

Copyright © 2021 IDG Communications, Inc.

How to choose a low-code development platform