Keyed Service Dependency Injection in .NET

Introduction

In .NET, Dependency Injection (DI) is a powerful pattern for managing dependencies between components, improving code modularity, and easing unit testing. However, there are times when you need to resolve multiple implementations of a single interface or service. This is where Keyed Service Dependency Injection comes into play.

Keyed Service DI allows you to register multiple services with the same interface, distinguished by unique keys, and then resolve them based on them. This approach is beneficial when you have several implementations of the same interface and you want to control which one is injected at runtime.

In this article, we’ll dive into how Keyed Service DI works in .NET, its benefits, and a practical implementation example.

Why Use Keyed Service DI?

The standard DI mechanism in .NET works well when you have one implementation for an interface. However, consider a scenario where you have multiple implementations of an interface that perform similar tasks but differ in specific ways. You may want to select which implementation to use dynamically at runtime.

Without Keyed Service DI, this scenario can lead to the use of additional logic or even anti-patterns, such as switching between implementations based on conditional statements or using service locators.

By using Keyed Service DI, you can achieve,

  1. Cleaner Code: Avoids cluttering the business logic with conditional statements.
  2. Decoupling: Keeps implementations independent, making them easier to maintain and test.
  3. Flexibility: Offers runtime flexibility to decide which implementation to use based on application logic or configuration.

Implementing Keyed Service DI in .NET

Step 1. Open Visual Studio.

  • Open Visual Studio.
  • Click on "Create a new project".

Step 2. Create a New .NET Core Console Application.

  1. In the "Create a new project" window, search for "Console App" and select "Console App (.NET Core)".
  2. Click Next.
  3. Set the project name (e.g., KeyedServiceDemo).
  4. Choose a location to save the project and click Create.
    Location

Step 3. Install Required NuGet Packages (Optional).

If you're working with a web application, you may need additional packages like ASP.NET Core. But for this console app, we’ll stick with the default libraries.

  1. Right-click on the Solution in the Solution Explorer.
  2. Select Manage NuGet Packages.
  3. Search for Microsoft.Extensions.DependencyInjection (should be installed by default in .NET Core projects).
  4. Install it if necessary.
    DependencyInjection

Step 4. Create the Interface.

  1. In Solution Explorer, right-click on the project.
  2. Select Add > New Folder and name it "Services".
  3. Right-click on the Services folder, then select Add > Class.
  4. Name the class IOperationService.cs and click Add.

Inside IOperationService.cs, add.

public interface IOperationService
{
    string PerformOperation();
}

 IOperationService

Step 5. Implement the Services.

Now, you need to create two classes that implement the IOperationService.

5.1. Create AdditionService.

  1. Right-click on the Services folder.
  2. Select Add > Class and name it AdditionService.cs.
  3. Add the following code.
public class AdditionService : IOperationService
{
    public string PerformOperation()
    {
        return "Performing Addition Operation";
    }
}

Following code

5.2. Create SubtractionService.

  1. Right-click on the Services folder again.
  2. Select Add > Class and name it SubtractionService.cs.
  3. Add the following code.
public class SubtractionService : IOperationService
{
    public string PerformOperation()
    {
        return "Performing Subtraction Operation";
    }
}

Services folder

Step 6. Create Enum for Keys.

  1. Right-click on the Services folder.
  2. Select Add > Class and name it OperationType.cs.
  3. Add the following code.
public enum OperationType
{
    Addition,
    Subtraction
}

Class

Step 7. Create the Factory.

  1. Right-click on the Services folder.
  2. Select Add > Class and name it OperationServiceFactory.cs.
  3. Add the following code.
public interface IOperationServiceFactory
{
    IOperationService GetService(OperationType operationType);
}

public class OperationServiceFactory : IOperationServiceFactory
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IDictionary<OperationType, Type> _serviceMapping;

    public OperationServiceFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _serviceMapping = new Dictionary<OperationType, Type>
        {
            { OperationType.Addition, typeof(AdditionService) },
            { OperationType.Subtraction, typeof(SubtractionService) }
        };
    }

    public IOperationService GetService(OperationType operationType)
    {
        var serviceType = _serviceMapping[operationType];
        return (IOperationService)_serviceProvider.GetRequiredService(serviceType);
    }
}

Step 8. Register Services in Program.cs.

  1. Open Program.cs from Solution Explorer.
  2. Replace the default code with the following.
using Microsoft.Extensions.DependencyInjection;
using System;

namespace KeyedServiceDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // Setup Dependency Injection
            var serviceProvider = new ServiceCollection()
                .AddTransient<AdditionService>()
                .AddTransient<SubtractionService>()
                .AddSingleton<IOperationServiceFactory, OperationServiceFactory>()
                .BuildServiceProvider();

            // Resolve Calculator and Execute Operation
            var calculator = new Calculator(serviceProvider.GetRequiredService<IOperationServiceFactory>());
            calculator.ExecuteOperation(OperationType.Addition);
            calculator.ExecuteOperation(OperationType.Subtraction);
        }
    }

    public class Calculator
    {
        private readonly IOperationServiceFactory _operationServiceFactory;

        public Calculator(IOperationServiceFactory operationServiceFactory)
        {
            _operationServiceFactory = operationServiceFactory;
        }

        public void ExecuteOperation(OperationType operationType)
        {
            var service = _operationServiceFactory.GetService(operationType);
            Console.WriteLine(service.PerformOperation());
        }
    }
}

Step 9. Build and Run the Application.

  1. Save all files.
  2. Press Ctrl+Shift+B to build the solution, or click Build > Build Solution.
  3. After building, press F5 to run the application or click the Start button.

Expected Output

You should see the following output in the Console Window.

Console Window

Benefits of Keyed Service DI

  1. Simplifies Logic: Keyed DI reduces the need for if or switch statements to determine which service to use. Instead, it encapsulates this logic in the DI container.
  2. Improves Maintainability: By having different service implementations registered by key, it becomes easier to maintain and extend the application. You can add more services without modifying the core business logic.
  3. Runtime Flexibility: This approach allows for greater flexibility at runtime. Based on user inputs, configurations, or external conditions, the appropriate service implementation can be resolved dynamically.
  4. Better Testing: It’s easier to test different implementations without coupling the test logic to specific conditions or requiring complex mocking.

Real-World Use Cases

Here are some real-world use cases where Keyed Service DI can make a big difference.

  • Notification Systems: Different notification channels like Email, SMS, or Push notifications can be keyed and injected as needed.
  • Payment Gateways: Multiple payment processors (e.g., PayPal, Stripe) could be registered as different services and resolved dynamically.
  • Data Providers: You may want to switch between different data providers (e.g., SQL, NoSQL) depending on the context, environment, or user preferences.

Conclusion

Keyed Service Dependency Injection is a powerful pattern in .NET that offers flexibility and scalability, especially when dealing with multiple service implementations for the same interface. It ensures cleaner, maintainable code and allows for better runtime flexibility.

By leveraging this technique, developers can avoid cluttered code, enhance the separation of concerns, and build more dynamic and testable applications. So, next time you find yourself needing multiple implementations of the same service, consider using Keyed Service DI to simplify your approach.