Use DI to instantiate a service with a dynamic parameter?

85 views Asked by At

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?
}
2

There are 2 answers

7
Dai On BEST ANSWER

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 );
    }
}
0
Deev Andrey On

You can register your service with custom factory method

builder.Services.AddScoped<TestService>(s =>
    new TestService(s.GetRequiredService<SomeDependency>(), "Ololo"));