BrandGhost

Keyed Services in Needlr: Managing Multiple Implementations

Resolving Services by Key

Sometimes you need multiple implementations of the same interface. Perhaps you have different payment processors (Stripe, PayPal, Square), different storage backends (SQL Server, PostgreSQL, MongoDB), or different notification channels (Email, SMS, Push). Keyed services in Needlr allow you to register multiple implementations of the same interface and resolve them by a key, enabling you to choose the right implementation at runtime based on configuration or business logic.

Needlr supports .NET 8's keyed service feature, which allows you to register services with string or object keys and resolve them using IServiceProvider.GetKeyedService<T>(key). This article explains how to register keyed services with Needlr, how to resolve them, and how keyed services integrate with automatic service discovery.

If you are new to dependency injection concepts, the guide on what is inversion of control provides foundational context. For developers familiar with DI containers, this article shows how Needlr simplifies keyed service registration while maintaining flexibility.

This article focuses specifically on keyed services. Topics like automatic service discovery, decorators, and service lifetimes are covered in other articles. Here we are concerned with managing multiple implementations of the same interface.

Understanding Keyed Services

Keyed services allow you to register multiple implementations of the same interface and distinguish between them using a key. This is useful when you have:

  • Multiple Implementations: Different implementations serving different purposes (e.g., Stripe vs PayPal payment processors)
  • Configuration-Driven Selection: Choosing implementations based on app settings or environment variables
  • Strategy Pattern: Implementing strategy pattern where different strategies share an interface
  • Plugin Systems: Different plugins implementing the same interface with different behaviors
  • Multi-Tenant Applications: Different implementations per tenant or customer
  • Feature Flags: Switching implementations based on feature flags or A/B testing

Here is a basic example without keyed services (which would fail):

// This would fail - can't register multiple implementations of the same interface
services.AddSingleton<IPaymentProcessor, StripePaymentProcessor>();
services.AddSingleton<IPaymentProcessor, PayPalPaymentProcessor>(); // Overwrites the first!

With keyed services, you can register both:

services.AddKeyedSingleton<IPaymentProcessor>("stripe", new StripePaymentProcessor());
services.AddKeyedSingleton<IPaymentProcessor>("paypal", new PayPalPaymentProcessor());

Then resolve them by key:

var stripeProcessor = serviceProvider.GetKeyedService<IPaymentProcessor>("stripe");
var paypalProcessor = serviceProvider.GetKeyedService<IPaymentProcessor>("paypal");

Registering Keyed Services with Needlr

Needlr supports keyed services through manual registration. Since keyed services require explicit keys, they are typically registered manually rather than through automatic discovery. However, you can combine automatic discovery with manual keyed registration.

Here is an example that shows keyed service registration:

using Microsoft.Extensions.DependencyInjection;
using NexusLabs.Needlr;
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;

var serviceProvider = new Syringe()
    .UsingSourceGen()
    .UsingPostPluginRegistrationCallback(services =>
    {
        // Register keyed services manually
        services.AddKeyedSingleton<IPaymentProcessor>("stripe", new StripePaymentProcessor());
        services.AddKeyedSingleton<IPaymentProcessor>("paypal", new PayPalPaymentProcessor());
        services.AddKeyedSingleton<IPaymentProcessor>("square", new SquarePaymentProcessor());
    })
    .BuildServiceProvider();

// Resolve by key
var processor = serviceProvider.GetKeyedService<IPaymentProcessor>("stripe");

The UsingPostPluginRegistrationCallback method allows you to add manual registrations after automatic discovery completes. This is the recommended way to register keyed services with Needlr.

Keyed Service Registration Methods

  • AddKeyedSingleton: Register a singleton service with a key
  • AddKeyedScoped: Register a scoped service with a key
  • AddKeyedTransient: Register a transient service with a key
  • Factory Functions: Use factory delegates for complex initialization
  • Multiple Keys: Register the same service with different keys if needed

Resolving Keyed Services

Once keyed services are registered, you can resolve them using IServiceProvider.GetKeyedService<T>(key) or IServiceProvider.GetRequiredKeyedService<T>(key). The GetKeyedService method returns null if the key is not found, while GetRequiredKeyedService throws an exception.

Resolution Methods

  • GetKeyedService(key): Returns the service or null if not found
  • GetRequiredKeyedService(key): Returns the service or throws if not found
  • [FromKeyedServices]: Attribute for constructor injection (.NET 8+)
  • IEnumerable: Resolve all keyed services of a type

Here is an example that shows keyed service resolution:

public class PaymentService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;

    public PaymentService(IServiceProvider serviceProvider, IConfiguration configuration)
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        // Get the processor key from configuration
        var processorKey = _configuration["Payment:DefaultProcessor"] ?? "stripe";

        // Resolve the processor by key
        var processor = _serviceProvider.GetRequiredKeyedService<IPaymentProcessor>(processorKey);

        // Use the processor
        return await processor.ProcessPaymentAsync(request);
    }
}

This pattern allows you to select the implementation at runtime based on configuration, user preferences, or business logic.

Factory-Based Keyed Registration

You can use factory functions to create keyed service instances, which is useful when the service requires dependencies:

using Microsoft.Extensions.DependencyInjection;
using NexusLabs.Needlr;
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;

var serviceProvider = new Syringe()
    .UsingSourceGen()
    .UsingPostPluginRegistrationCallback(services =>
    {
        // Register keyed services with factory functions
        services.AddKeyedSingleton<IPaymentProcessor>("stripe", (sp, key) =>
        {
            var config = sp.GetRequiredService<IConfiguration>();
            var logger = sp.GetRequiredService<ILogger<StripePaymentProcessor>>();
            return new StripePaymentProcessor(config, logger);
        });

        services.AddKeyedSingleton<IPaymentProcessor>("paypal", (sp, key) =>
        {
            var config = sp.GetRequiredService<IConfiguration>();
            var logger = sp.GetRequiredService<ILogger<PayPalPaymentProcessor>>();
            return new PayPalPaymentProcessor(config, logger);
        });
    })
    .BuildServiceProvider();

The factory function receives the IServiceProvider and the key, allowing you to resolve dependencies and create the service instance with the appropriate configuration.

Combining Automatic Discovery with Keyed Services

You can combine automatic service discovery with manual keyed registration. This is useful when some implementations should be automatically discovered, while others need explicit keys.

Here is an example:

using Microsoft.Extensions.DependencyInjection;
using NexusLabs.Needlr;
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;

// Automatically discovered service (registered as both IStorageService and SqlStorageService)
public interface IStorageService
{
    Task SaveAsync(string key, object value);
    Task<T> LoadAsync<T>(string key);
}

public class SqlStorageService : IStorageService
{
    public async Task SaveAsync(string key, object value)
    {
        // SQL implementation
        await Task.CompletedTask;
    }

    public async Task<T> LoadAsync<T>(string key)
    {
        // SQL implementation
        return await Task.FromResult(default(T));
    }
}

// Another implementation that should be keyed
public class MongoStorageService : IStorageService
{
    public async Task SaveAsync(string key, object value)
    {
        // MongoDB implementation
        await Task.CompletedTask;
    }

    public async Task<T> LoadAsync<T>(string key)
    {
        // MongoDB implementation
        return await Task.FromResult(default(T));
    }
}

var serviceProvider = new Syringe()
    .UsingSourceGen()
    .UsingPostPluginRegistrationCallback(services =>
    {
        // Mark MongoStorageService to prevent auto-registration
        // Then register it as a keyed service
        services.AddKeyedSingleton<IStorageService>("mongo", new MongoStorageService());
    })
    .BuildServiceProvider();

// SqlStorageService is automatically registered (can inject IStorageService directly)
// MongoStorageService is available as a keyed service
var mongoStorage = serviceProvider.GetKeyedService<IStorageService>("mongo");

In this example, SqlStorageService is automatically discovered and registered as IStorageService. MongoStorageService is registered manually as a keyed service, allowing you to choose between implementations at runtime.

Real-World Example: Multi-Provider Payment System

Let's look at a complete example that demonstrates a realistic use case:

using Microsoft.Extensions.DependencyInjection;
using NexusLabs.Needlr;
using NexusLabs.Needlr.Injection;
using NexusLabs.Needlr.Injection.SourceGen;

var serviceProvider = new Syringe()
    .UsingSourceGen()
    .UsingPostPluginRegistrationCallback(services =>
    {
        // Register multiple payment processors with keys
        services.AddKeyedSingleton<IPaymentProcessor>("stripe", (sp, key) =>
        {
            var config = sp.GetRequiredService<IConfiguration>();
            var logger = sp.GetRequiredService<ILogger<StripePaymentProcessor>>();
            var apiKey = config["Stripe:ApiKey"];
            return new StripePaymentProcessor(apiKey, logger);
        });

        services.AddKeyedSingleton<IPaymentProcessor>("paypal", (sp, key) =>
        {
            var config = sp.GetRequiredService<IConfiguration>();
            var logger = sp.GetRequiredService<ILogger<PayPalPaymentProcessor>>();
            var clientId = config["PayPal:ClientId"];
            var clientSecret = config["PayPal:ClientSecret"];
            return new PayPalPaymentProcessor(clientId, clientSecret, logger);
        });

        services.AddKeyedSingleton<IPaymentProcessor>("square", (sp, key) =>
        {
            var config = sp.GetRequiredService<IConfiguration>();
            var logger = sp.GetRequiredService<ILogger<SquarePaymentProcessor>>();
            var accessToken = config["Square:AccessToken"];
            return new SquarePaymentProcessor(accessToken, logger);
        });
    })
    .BuildServiceProvider();

// Service that uses keyed services
public interface IPaymentProcessor
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
}

public class StripePaymentProcessor : IPaymentProcessor
{
    private readonly string _apiKey;
    private readonly ILogger<StripePaymentProcessor> _logger;

    public StripePaymentProcessor(string apiKey, ILogger<StripePaymentProcessor> logger)
    {
        _apiKey = apiKey;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogInformation("Processing payment with Stripe");
        // Stripe API integration
        return await Task.FromResult(new PaymentResult { Success = true });
    }
}

public class PayPalPaymentProcessor : IPaymentProcessor
{
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly ILogger<PayPalPaymentProcessor> _logger;

    public PayPalPaymentProcessor(
        string clientId,
        string clientSecret,
        ILogger<PayPalPaymentProcessor> logger)
    {
        _clientId = clientId;
        _clientSecret = clientSecret;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogInformation("Processing payment with PayPal");
        // PayPal API integration
        return await Task.FromResult(new PaymentResult { Success = true });
    }
}

public class SquarePaymentProcessor : IPaymentProcessor
{
    private readonly string _accessToken;
    private readonly ILogger<SquarePaymentProcessor> _logger;

    public SquarePaymentProcessor(string accessToken, ILogger<SquarePaymentProcessor> logger)
    {
        _accessToken = accessToken;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        _logger.LogInformation("Processing payment with Square");
        // Square API integration
        return await Task.FromResult(new PaymentResult { Success = true });
    }
}

// Service that selects processor based on configuration
public class PaymentService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(
        IServiceProvider serviceProvider,
        IConfiguration configuration,
        ILogger<PaymentService> logger)
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
        _logger = logger;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        // Get processor key from request or configuration
        var processorKey = request.ProcessorKey 
            ?? _configuration["Payment:DefaultProcessor"] 
            ?? "stripe";

        _logger.LogInformation("Using payment processor: {ProcessorKey}", processorKey);

        // Resolve the processor by key
        var processor = _serviceProvider.GetRequiredKeyedService<IPaymentProcessor>(processorKey);

        // Process the payment
        return await processor.ProcessPaymentAsync(request);
    }
}

In this example, three payment processors are registered with different keys. The PaymentService selects the appropriate processor based on the request or configuration, allowing the system to support multiple payment providers simultaneously.

Service Lifetimes with Keyed Services

Keyed services support all standard service lifetimes: transient, scoped, and singleton. The lifetime applies to each keyed registration independently.

services.AddKeyedTransient<IService>("key1", new Service1());
services.AddKeyedScoped<IService>("key2", new Service2());
services.AddKeyedSingleton<IService>("key3", new Service3());

Each keyed registration has its own lifetime. A singleton keyed service is shared across all resolutions with that key, while a transient keyed service creates a new instance for each resolution.

Injecting Keyed Services

You can inject keyed services into other services using [FromKeyedServices] attribute (available in .NET 8):

public class OrderService
{
    private readonly IPaymentProcessor _defaultProcessor;
    private readonly IPaymentProcessor _premiumProcessor;

    public OrderService(
        [FromKeyedServices("stripe")] IPaymentProcessor defaultProcessor,
        [FromKeyedServices("paypal")] IPaymentProcessor premiumProcessor)
    {
        _defaultProcessor = defaultProcessor;
        _premiumProcessor = premiumProcessor;
    }

    public async Task<Order> CreateOrderAsync(OrderRequest request, bool isPremium)
    {
        var processor = isPremium ? _premiumProcessor : _defaultProcessor;
        // Use the selected processor
        return await Task.FromResult(new Order());
    }
}

The [FromKeyedServices] attribute tells the dependency injection container which keyed service to inject. This allows you to inject multiple implementations of the same interface into a single class.

Best Practices

When using keyed services with Needlr, follow these best practices:

Use Meaningful Keys: Choose keys that clearly identify the implementation. String keys like "stripe" or "paypal" are more readable than numeric keys.

Register Keyed Services Manually: Since keyed services require explicit keys, register them manually rather than relying on automatic discovery. Use UsingPostPluginRegistrationCallback to add keyed registrations.

Prefer Configuration-Driven Selection: Use configuration to determine which keyed service to use, making it easy to change implementations without code changes.

Document Key Values: Document the valid key values and their meanings, especially if keys are used across multiple parts of the application.

Consider Factory Registration: Use factory functions for keyed services that have dependencies, ensuring proper dependency resolution.

Comparison with Other Approaches

Keyed services provide a clean alternative to other patterns for managing multiple implementations:

Strategy Pattern: Keyed services can implement the strategy pattern, where the key selects the strategy. This is simpler than manual strategy registration.

Factory Pattern: Instead of creating a factory class, you can use keyed services with IServiceProvider.GetKeyedService<T>(key) to select implementations.

Named Registrations: Keyed services are more type-safe than named registrations and integrate better with dependency injection.

Conclusion

Keyed services provide a powerful way to manage multiple implementations of the same interface. Needlr supports keyed services through manual registration, allowing you to combine automatic discovery for standard services with explicit keyed registration for services that need runtime selection.

Whether you are building a multi-provider payment system, supporting multiple storage backends, or implementing a plugin system, keyed services give you the flexibility to choose implementations at runtime while maintaining type safety and dependency injection benefits.

Frequently Asked Questions

How do I register keyed services with Needlr?

Register keyed services manually using UsingPostPluginRegistrationCallback or in a plugin. Use services.AddKeyedSingleton<T>(key, instance) or similar methods to register keyed services.

Can I use automatic discovery with keyed services?

Keyed services require explicit keys, so they are typically registered manually. However, you can combine automatic discovery for standard services with manual keyed registration for services that need keys.

How do I resolve keyed services?

Use IServiceProvider.GetKeyedService<T>(key) or IServiceProvider.GetRequiredKeyedService<T>(key) to resolve keyed services by their key.

Can I inject keyed services into other services?

Yes, use the [FromKeyedServices] attribute (available in .NET 8) to inject keyed services into constructors. This allows you to inject multiple implementations of the same interface.

What service lifetimes are supported for keyed services?

Keyed services support all standard lifetimes: transient, scoped, and singleton. Each keyed registration has its own independent lifetime.

How do I select a keyed service at runtime?

Resolve keyed services using IServiceProvider.GetKeyedService<T>(key) where the key is determined by configuration, business logic, or user input.

Can I have both keyed and non-keyed registrations of the same interface?

Yes, you can register a service both as a regular service and as a keyed service. The regular registration is resolved when injecting IService, while keyed registrations are resolved using GetKeyedService<T>(key).

Getting Started with Needlr: Fluent DI for .NET Applications

Learn how to install and configure Needlr for dependency injection in .NET with step-by-step setup, NuGet packages, and your first fluent DI application.

Automatic Service Discovery in C# with Needlr: How It Works

Learn how Needlr's automatic service discovery works in C# with convention-based registration, type scanning, and the DoNotAutoRegister attribute for .NET applications.

Automatic Dependency Injection in C#: The Complete Guide to Needlr

Learn how Needlr simplifies dependency injection in C# with automatic service discovery, source generation, and a fluent API for .NET applications.

An error has occurred. This application may no longer respond until reloaded. Reload x