Use multiple implementations of an interface in ASP.NET Core

Spread the love

How to register multiple implementations of an interface with the IoC container in ASP.NET Core and retrieve a specific service at runtime

The built-in support for dependency injection in ASP.NET Core is great. However, dealing with multiple implementations of an interface when working with dependency injection in ASP.NET Core is a bit tricky. In this article I’ll show you how to dynamically select a service from such an implementation in ASP.NET Core.

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 Web Application MVC project in Visual Studio 2019. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new ASP.NET Core MVC project in Visual Studio.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web Application” from the list of templates displayed.
  1. Click Next.
  2. In the “Configure your new project” window, specify the name and location for the new project.
  3. Optionally check the “Place solution and project in the same directory” check box, depending on your preferences.
  4. Click Create.
  5. In the “Create a New ASP.NET Core Web Application” window shown next, select .NET Core as the runtime and ASP.NET Core 3.1 (or later) from the drop-down list at the top.
  6. Select “Web Application (Model-View-Controller)” as the project template to create a new ASP.NET Core MVC application.
  7. Ensure that the check boxes “Enable Docker Support” and “Configure for HTTPS” are unchecked as we won’t be using those features here.
  8. Ensure that Authentication is set to “No Authentication” as we won’t be using authentication either.
  9. Click Create.

Following these steps should create a new ASP.NET Core MVC project in Visual Studio 2019. We’ll use this project in the sections below to illustrate how we can register multiple implementations of an interface in ASP.NET Core 3.1.

One problem with the IoC container in ASP.NET Core

The built-in IoC (inversion of control) container in ASP.NET Core may not be as extensive as AutoFac or Unity or some other popular IoC containers, but you should be able to use it to meet your requirements in most cases. However, the built-in IoC container does not allow us to register multiple services and then retrieve a specific service instance at runtime.

There are a few IoC containers that enable you to register concrete types using a unique key that distinguishes the instances of these types. However, the built-in IoC container in ASP.NET Core lacks support for this. Hence registering services that have a common interface and resolving them at runtime isn’t simple. Let’s understand this with an example.

Assume you have an interface called ICustomLogger with the following code:

public interface ICustomLogger
{
   public bool Write(string data);
}

The ICustomLogger interface is implemented by the following three classes:

public class FileLogger : ICustomLogger
{
   public bool Write(string data)
   {
       throw new System.NotImplementedException();
   }
}
public class DbLogger : ICustomLogger
{
   public bool Write(string data)
   {
       throw new System.NotImplementedException();
   }
}
public class EventLogger : ICustomLogger
{
  public bool Write(string data)
  {
      throw new System.NotImplementedException();
  }
}

The FileLogger class is used for logging data to a file, the DbLogger class is used for logging data to a database, and the EventLogger class is used to log data to the event log so that the logs can be viewed using the Event Viewer tool. Note that since logging data is not the objective of this article, the Write methods pertaining to each of these classes do not include any implementation.

Now suppose you’ve added instances of these classes as scoped services in the ConfigureServices method of the Startup class as shown below:

services.AddScoped<ICustomLogger, FileLogger>();
services.AddScoped<ICustomLogger, DbLogger>();
services.AddScoped<ICustomLogger, EventLogger>();

The following code snippet illustrates how you can take advantage of dependency injection in the constructor of the HomeController class to use these services.

public class HomeController : Controller
{
public HomeController(ICustomLogger fileLogger, ICustomLogger dbLogger, ICustomLogger eventLogger)
{
    var obj = fileLogger; //This code has been written for debugging
}
    //Action methods go here
}

When you run this application and the breakpoint is hit, you’ll observe that all three parameters are instances of type EventLogger because EventLogger has been injected last. This is shown in Figure 1 below.

aspnet core ioc example

IDG

Figure 1.

Using dependency injection with multiple implementations of an interface in ASP.NET Core

How do we overcome this limitation of the built-in IoC container in ASP.NET Core? In the solution outlined below, we will use an IEnumerable collection of services to register the services and a delegate to retrieve a specific service instance.

The illustration below will also include example code for using dependency injection in the constructor of your controller class, resolving the service instance, and returning instances of the three classes (FileLogger, DbLogger, and EventLogger) depending on the service type selected at runtime.

Use an IEnumerable collection of service instances

In the ConfigureServices method write the following code to add scoped services of each of the ICustomLogger implementations.

services.AddScoped<ICustomLogger, FileLogger>();
services.AddScoped<ICustomLogger, DbLogger>();
services.AddScoped<ICustomLogger, EventLogger>();

Next, take advantage of dependency injection and an IEnumerable collection of the ICustomLogger implementations in the constructor of the controller class as shown in the code snippet given below.

public HomeController(IEnumerable<ICustomLogger> loggers)
{
   foreach(var logger in loggers)
   {
      var objType = logger.GetType();
   }
}

Use a delegate to retrieve a specific service instance

Consider the following enum that contains integer constants that correspond to the three types FileLogger, DbLogger, and EventLogger.

public enum ServiceType
{
   FileLogger,
   DbLogger,
   EventLogger
}

Next, declare a shared delegate as shown in the code snippet given below.

public delegate ICustomLogger ServiceResolver(ServiceType serviceType);

Now add the following scoped services to the service collection instance as shown in the next code snippet.

services.AddScoped<FileLogger>();
services.AddScoped<DbLogger>();
services.AddScoped<EventLogger>();

Return instances based on service type at runtime

The following code snippet shows how you can return instances of the FileLogger, DbLogger, and EventLogger classes depending on the service type selected at runtime.

services.AddTransient<ServiceResolver>(serviceProvider => serviceTypeName =>
            {
                switch (serviceTypeName)
                {
                    case ServiceType.FileLogger:
                        return serviceProvider.GetService<FileLogger>();
                    case ServiceType.DbLogger:
                        return serviceProvider.GetService<DbLogger>();
                    case ServiceType.EventLogger:
                        return serviceProvider.GetService<EventLogger>();
                    default:
                        return null;
                }
            });

Use dependency injection and resolve the service instance

Lastly, here is how you can use dependency injection in the constructor of your controller class and then resolve the service instance.

public HomeController(Func<ServiceType, ICustomLogger> serviceResolver)
{
    var service = serviceResolver(ServiceType.FileLogger);
}

You could also use reflection to resolve types at runtime but it is not a recommended solution. Another possible solution is using a generic type parameter on the interface.

Join Geezgo for free. Use Geezgo\’s end-to-end encrypted Chat with your Closenets (friends, relatives, colleague etc) in personalized ways.>>

 256 

Leave a Reply