In Focus

Dependency Injection With Multiple Implementations Of The Same Interface

The ASP.NET Core dependency injection model works well when mapping one interface to one implementation of the interface, but doesn't have a framework to map an interface to multiple interface implementations. This article, along with code samples, shows how to handle this with a delegate and a facade pattern.

Introduction

ASP.NET Core dependency injection has support for mapping an interface to an implementation of the interface, configuring lifetime rules, and managing configuration settings. It works well for most application bootstrapping needs but comes with an implicit assumption - that there is always a one-to-one mapping from the interface to implementation.
 
Workflow and other operations often call for different business rules which are expressed through various implementations of the same interface. For example, you may have an IWorkflowAction interface that has implementations like,

  • SendEmailAction : IWorkflowAction
  • RequestCreditReportAction : IWorkflowAction
  • AssignTaskAction: IWorkflowAction

The implementation of IWorkflowAction needed to process a workflow request is not known when bootstrapping the application. It needs to be resolved at the time the workflow request is received. This article shows you how to configure and manage dependency injection resolution on demand using an ASP.NET Core Web API application.
 
All code samples are available for download in the samples attached to this article. 

Interfaces and Implementations

The dependency injection solution will resolve a single IMathOperationRepository interface to four different implementations to handle addition, subtraction, multiplication, and division. You can extrapolate from this and apply its own business requirements.

  1. public interface IMathOperationRepository
  2. {
  3.   OperationResult PerformOperation(OperationRequest opRequest);
  4. }
  5. public class AddOperationRepository : IMathOperationRepository
  6. {
  7.   public OperationResult PerformOperation(OperationRequest opRequest)
  8.   {
  9.     OperationResult opResult = new OperationResult();
  10.     opResult.Value = opRequest.X + opRequest.Y;
  11.     return opResult;
  12.   }
  13. }

The interface accepts a simple OperationRequest that has an X and Y value and applies the appropriate math operation per interface implementation. The AddOperationRepository adds the two values, the SubtractOperationRepository, subtracts Y from X, etc.

The OperationRequest uses the MathOperationType enumeration to indicate which interface implementation is required.
  1. public enum MathOperationType
  2. {
  3.   Add = 0,
  4.   Subtract = 1,
  5.   Multiply = 2,
  6.   Divide = 3
  7. }
  8. public class OperationRequest
  9. {
  10.   . . .
  11.   [JsonConverter(typeof(StringEnumConverter))]
  12.   [JsonProperty(PropertyName = "operation")]
  13.   public MathOperationType OperationType { get; set; }
  14. }

Bootstrapping

Typical ASP.NET Core dependency injection bootstrapping looks something like this,

  1. services.AddTransient<IMathOperationRepository, AddOperationRepository>();

This would work if the IMathOperationRepository always resolved to the AddOperationRepository implementation; however, it needs to be dynamically resolved based on the type of operation requested at the time of the request, not upon startup. Instead, a delegate is added which accepts a key value that resolves to the appropriate interface implementation. The function uses a simple facade pattern to map the key value to the class.

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3.   services.AddTransient<AddOperationRepository>();
  4.   services.AddTransient<SubtractOperationRepository>();
  5.   services.AddTransient<MultiplyOperationRepository>();
  6.   services.AddTransient<DivideOperationRepository>();
  7.   services.AddTransient<Func<MathOperationType, IMathOperationRepository>>(serviceProvider => key =>
  8.   {
  9.     switch (key)
  10.     {
  11.       case MathOperationType.Add:
  12.         return serviceProvider.GetService<AddOperationRepository>();
  13.       case MathOperationType.Subtract:
  14.         return serviceProvider.GetService<SubtractOperationRepository>();
  15.       case MathOperationType.Multiply:
  16.         return serviceProvider.GetService<MultiplyOperationRepository>();
  17.       case MathOperationType.Divide:
  18.         return serviceProvider.GetService<DivideOperationRepository>();
  19.       default:
  20.         throw new KeyNotFoundException();
  21.     }
  22.   });
  23.   . . .
  24. }
The dependency injection framework passes the delegate to the ValuesController. The Post method uses the enumeration in the OperationType property and retrieves the requested IMathOperationRepository implementation.
  1. [Route("api/[controller]")]  
  2. [ApiController]  
  3. public class ValuesController : ControllerBase  
  4. {  
  5.   private Func<MathOperationType, IMathOperationRepository> _mathRepositoryDelegate;  
  6.   public ValuesController(Func<MathOperationType, IMathOperationRepository> mathRepositoryDelegate)  
  7.   {  
  8.     _mathRepositoryDelegate = mathRepositoryDelegate;  
  9.   }  
  10.   [HttpPost]  
  11.   public ActionResult<OperationResult> Post([FromBody] OperationRequest opRequest)  
  12.   {  
  13.     IMathOperationRepository mathRepository = _mathRepositoryDelegate(opRequest.OperationType);  
  14.     OperationResult opResult = mathRepository.PerformOperation(opRequest);  
  15.     return new ObjectResult(opResult);  
  16.   }  
  17. }  

Testing

 
Test the Values controller in Postman with a POST request that sends the JSON values of the OperationRequest. Setting the values x=3, y=4, and operation to divide yields 0.75. Changing the operation value to add yields 7, subtract yields -1, and multiply yields 12.  
 
The caller determines the operation which, in turn, uses the appropriate interface implementation for the task.  
 
Dependency Injection with Multiple Implementations of the Same Interface 

Summary

 
The math operations, in and of themselves, are not terribly meaningful. They're simple implementations by design.
 
This solution is about mapping a single interface to multiple implementations.  ASP.NET Core dependency injection doesn't have a built-in framework to handle it. Using a delegate and a facade pattern in the dependency injection framework lets developers get the right interface implementation on demand.