Search code examples
c#.netasp.net-coredependency-injectionservice-provider

Use DI to instantiate a service with a dynamic parameter?


I have a class with below constructor:

public MyClass(SomeRequiredService srs, string someRandomInput)
{

}

How can I register MyClass with DI but with an option to pass a different someRandomInput each time i need to get an instance of MyClass?

or

With below constructor how can I get access to registered services or IServiceProvider in MyClass ctor while having a dynamic parameter in constructor?

public MyClass(string someRandomInput)
{
    // how access a registered service here using DI?
}

Solution

  • How can I register MyClass with DI but with an option to pass someRandomInput each time i need to get an instance?

    Assuming someRandomInput only needs to be specified once, then you can do this in an implementationFactory: in your service registration:

    IConfiguration cfg = ...
    
    //
    
    services
        .AddSingleton<MyClass>( implementationFactory: ( IServiceProvider sp ) =>
            new MyClass(
                srs            : sp.GetRequiredService<SomeRequiredService>(),
                someRandomInput: cfg.GetString("Foobars")
            )
        );
    

    The same works for AddTransient and AddScoped - the catch being that they'll all use whatever value you pass into someRandomInput in your factory - though your factory method itself could also use the IServiceProvider to get another service as a source for those values.


    More generalised, here's two approaches:

    Option 1: Define an interface that represents your services' data-dependency

    public interface IFoobarsServiceRequisite
    {
        String GetNextStringValue();
    }
    
    public class FoobarsService
    {
        public FoobarsService( SomeRequiredService srs, IFoobarsServiceRequisite requisiteData )
        {
        }
    }
    

    ...then you'd register some implementation of that Requisite service interface.

    Option 2: Not-quite-as-bad-as AbstractSingletonProxyFactoryBean

    (In case anyone missed the reference)

    
    services
        .AddSingleton<SomeRequiredService>()
        .AddSingleton<ISomeRandomStringSource,DefaultSomeRandomStringSource>()
        .AddSingleton<IFoobarsServiceFactory,DefaultFoobarsServiceFactory>()
        .AddTransient<FoobarsService>( sp => sp.GetRequiredService<IFoobarsServiceFactory>().CreateFoobar() );
    
    
    //
    
    public interface IFoobarsServiceFactory
    {
        FoobarsService CreateFoobar();
    }
    
    public class DefaultFoobarsServiceFactory : IFoobarsServiceFactory
    {
        private readonly SomeRequiredService     srs;
        private readonly ISomeRandomStringSource src;
    
        public DefaultFoobarsServiceFactory(
            SomeRequiredService srs,
            ISomeRandomStringSource src
        )
        {
            this.srs = srs;
            this.src = src;
        }
    
        public FoobarsService CreateFoobar()
        {
            return new FoobarsService( this.srs, this.src.GetNextValue() );
        }
    }
    
    public class FoobarsService
    {
        public FoobarsService( SomeRequiredService srs, String someRandomValue )
        {
        }
    }
    

    and it's factories all the way down...


    I was surprised that MEDI still lacks a standardised IServiceFactory interface to handle the awkward edge-cases that need more factory-logic than a simple sp => lambda, but which don't need to explicitly call their .CreateService() methods in application code - especially as EF already has its own IDbContextFactory type.

    Update: Option 3: Factory-service-with-caller-provided-args

    After clarifying things in the comments, I think the OP is after something like this:

    • The FoobarServiceFactory (or an interface thereof) is a normal Singleton service which carries the FoobarService dependencies - but also allows/requires the consumers of FoobarServiceFactory to provide their own arguments for when they want to get an instance of FoobarService, like so:
    services.AddSingleton<IFoobarServiceFactory,FoobarServiceFactory>()
    
    //
    
    public interface IFoobarServiceFactory // Note that this interface is entirely optional: you could just register `FoobarServiceFactory` as a service.
    {
        FoobarsService CreateFoobar( String callerProvidedData );
    }
    
    public class FoobarServiceFactory: IFoobarServiceFactory
    {
        private readonly SomeRequiredService srs;
    
        public DefaultFoobarsServiceFactory( SomeRequiredService srs )
        {
            this.srs = srs;
        }
    
        public FoobarsService CreateFoobar( String callerProvidedData )
        {
            return new FoobarService( this.srs, callerProvidedData );
        }
    }
    
    public class FoobarService
    {
        public FoobarService( SomeRequiredService srs, String someRandomValue )
        {
            // etc
        }
    }
    

    Used like so:

    public class SomeConsumer
    {
        private readonly IFoobarServiceFactory fbFactory;
    
        public SomeConsumer( IFoobarServiceFactory fbFactory )
        {
            this.fbFactory = fbFactory;
        }
    
        public void DoSomething()
        {
            String someRandomString = Math.Random() > 0.5 ? "abc" : "0xdeadbeef";
            
            FoobarService svc = this.fbFactory.Create( someRandomString );
            // do stuff with `svc`
        }
    }
    

    An alternative implementation of IFoobarServiceFactory could use IServiceProvider, if it isn't possible to retain a long-life'd (or unbound lifetime) for SomeRequiredService (and IMO, this is the only acceptable reason to ever need IServiceProvider as an actual dependency (other than breaking cycles in dependency graphs, ofc):

    public class AltFoobarServiceFactory : IFoobarServiceFactory
    {
        private readonly IServiceProvider sp;
    
        public AltFoobarServiceFactory( IServiceProvider sp )
        {
            this.sp = sp;
        }
    
        public FoobarsService CreateFoobar( String callerProvidedData )
        {
            // `ActivatorUtilities` is useful! https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.activatorutilities?view=dotnet-plat-ext-8.0
            return ActivatorUtilities.CreateInstance<FoobarsService>( sp, callerProvidedData );
        }
    }