How to resolve services in factory class using dependency injection? | C#

252 Views Asked by At

I have six implementations for the same interface registered on my DI container, like so:

services.AddScoped<IProductService, ProductService1>();
services.AddScoped<IProductService, ProductService2>();
// ...
services.AddScoped<IProductService, ProductService6>();

The controller which consumes these services has a parameter productName that I use to choose my desired service. For separation of concerns purposes, I implemented a factory class that takes this string and retrieves the requested service from the service provider. Something like this:

public string ProductController(string productName)
{
   var myService = _productFactory.GetProductService(productName);
   // ... do other stuff
}

This is what I had in mind for my factory class

public IProductService GetProductService(string productName)
{
    switch (productName.ToLower())
    {
        case "product1":
            return _serviceProvider.GetRequiredService<ProductService1>();
        case "product2":
            return _serviceProvider.GetRequiredService<ProductService2>();
        // ...
    }
}

However, this returns an error because it can't find the services of type ProductService1, and so forth. From my understanding of how these methods work, it only finds the type IProductService.

How can I search for a required service based on its implementation? Can I search for a service by name or is there a way to link a string to an implementation on the DI container? I don't see how a factory class would work if I couldn't tell it which implementation it should retrieve on each case.

2

There are 2 best solutions below

0
mason On

You haven't actually registered anything as a ProductService1 or 2 or 3 etc. You registered them all as IProductService. You could just register them as the type you're trying to resolve them as, like this:

services.AddScoped<ProductService1>();

Here's a complete working example:

using Microsoft.Extensions.DependencyInjection;

var factory = new ProductServiceFactory();
var service = factory.GetProductService("product1");
Console.WriteLine(service.ToString());

class ProductServiceFactory
{
    readonly IServiceProvider _serviceProvider;

    public ProductServiceFactory()
    {
        var services = new ServiceCollection();
        services.AddScoped<ProductService1>();
        services.AddScoped<ProductService2>();
        services.AddScoped<ProductService3>();
        _serviceProvider = services.BuildServiceProvider();
    }

    public IProductService GetProductService(string productName)
    {
        switch (productName.ToLower())
        {
            case "product1":
                return _serviceProvider.GetRequiredService<ProductService1>();
            case "product2":
                return _serviceProvider.GetRequiredService<ProductService2>();
            case "product3":
                return _serviceProvider.GetRequiredService<ProductService3>();
            default:
                throw new ArgumentException(productName);
        }
    }
}




interface IProductService { }

class ProductService1 : IProductService { }

class ProductService2 : IProductService { }

class ProductService3 : IProductService { }

Note that this could actually be a bit cleaner. If you use Scrutor to do the registrations, you can stop having to register each individual type and so the next time you add a product service it will automatically get registered.

var services = new ServiceCollection();
services.Scan(s => s.FromAssemblyOf<IProductService>()
                    .AddClasses(c => c.AssignableTo<IProductService>())
                    .AsSelf()
                    );
// no longer needed to do individual registrations
// Scrutor will pick up these and any new IProductService implementations!
//services.AddScoped<ProductService1>();
//services.AddScoped<ProductService2>();
//services.AddScoped<ProductService3>();
_serviceProvider = services.BuildServiceProvider();

There's ways to clean up the factory and make it so you don't have to keep adding to the case statements. For example, let's say your IProductServices have to declare which product they support, we can grab them like this:

using Microsoft.Extensions.DependencyInjection;

var factory = new ProductServiceFactory();
var service = factory.GetProductService("product1");
Console.WriteLine(service.ToString());

class ProductServiceFactory
{
    IEnumerable<IProductService> _productServices;

    public ProductServiceFactory()
    {
        var services = new ServiceCollection();
        services.Scan(s => s.FromAssemblyOf<IProductService>()
                            .AddClasses(c => c.AssignableTo<IProductService>())
                            .AsImplementedInterfaces()
                            );
        var serviceProvider = services.BuildServiceProvider();
        _productServices = serviceProvider.GetServices<IProductService>();
    }

    public IProductService GetProductService(string productName)
    {
        var productService = _productServices.SingleOrDefault(p => productName.Equals(p.ProductName, StringComparison.InvariantCultureIgnoreCase));
        
        if (productService is null)
        {
            throw new ArgumentException(productName);
        }

        return productService;
    }
}


interface IProductService
{
    string ProductName { get; }
}

class ProductService1 : IProductService
{
    public string ProductName => "product1";
}

class ProductService2 : IProductService
{
    public string ProductName => "product2";
}

class ProductService3 : IProductService
{
    public string ProductName => "product3";
}

There's some other techniques that may be a bit more or less efficient or handle your scoping needs better, but I'd say that's a big improvement already.

0
Adam Procházka On

In order to retrieve services based on their class, you need to register them as the class itself not its interface. So instead of the following:

services.AddScoped<IProductService, ProductService1>();

You should use this:

services.AddScoped<ProductService1>();

This way, the ProductService1 type will be associated with the implementation instead of the IProductService type.

Equivalently, you may also register it like this:

services.AddScoped<ProductService1, ProductService1>();