How can I pass a runtime parameter as part of the dependency resolution?

I need to be able to pass a connection string into some of my service implementations. I am doing this in the constructor. The connection string is configurable by user will be added the ClaimsPrincipal as a Claim.

All fine so far.

Unfortunately, I also want to be able to use the dependency injection features in ASP.NET Core to the fullest and resolve the service implementation though DI.

I have a POC implmentation:

public interface IRootService
{
INestedService NestedService { get; set; }


void DoSomething();
}


public class RootService : IRootService
{
public INestedService NestedService { get; set; }


public RootService(INestedService nestedService)
{
NestedService = nestedService;
}


public void DoSomething()
{
// implement
}
}




public interface INestedService
{
string ConnectionString { get; set; }


void DoSomethingElse();
}


public class NestedService : INestedService
{
public string ConnectionString { get; set; }


public NestedService(string connectionString)
{
ConnectionString = connectionString;
}


public void DoSomethingElse()
{
// implement
}
}

These services have been registered during startup and INestedService has been added the constructor of a controller.

public HomeController(INestedService nestedService)
{
NestedService = nestedService;
}

As expected, I get the error Unable to resolve service for type 'System.String' while attempting to activate 'Test.Dependency.Services.NestedService'.

What are my options here?

85301 次浏览

Simple configuration

public void ConfigureServices(IServiceCollection services)
{
// Choose Scope, Singleton or Transient method
services.AddSingleton<IRootService, RootService>();
services.AddSingleton<INestedService, NestedService>(serviceProvider=>
{
return new NestedService("someConnectionString");
});
}

With appSettings.json

If you decide to hide your connection string inside appSettings.json, e.g:

"Data": {
"ConnectionString": "someConnectionString"
}

Then provided that you've loaded your appSettings.json in the ConfigurationBuilder (usually located in the constructor of the Startup class), then your ConfigureServices would look like this:

public void ConfigureServices(IServiceCollection services)
{
// Choose Scope, Singleton or Transient method
services.AddSingleton<IRootService, RootService>();
services.AddSingleton<INestedService, NestedService>(serviceProvider=>
{
var connectionString = Configuration["Data:ConnectionString"];
return new NestedService(connectionString);
});
}

With extension methods

namespace Microsoft.Extensions.DependencyInjection
{
public static class RootServiceExtensions //you can pick a better name
{
//again pick a better name
public static IServiceCollection AddRootServices(this IServiceCollection services, string connectionString)
{
// Choose Scope, Singleton or Transient method
services.AddSingleton<IRootService, RootService>();
services.AddSingleton<INestedService, NestedService>(_ =>
new NestedService(connectionString));
}
}
}

Then your ConfigureServices method would look like this

public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration["Data:ConnectionString"];
services.AddRootServices(connectionString);
}

With options builder

Should you need more parameters, you can go a step further and create an options class which you pass to RootService's constructor. If it becomes complex, you can use the Builder pattern.

To pass a runtime parameter not known at the start of the application, you have to use the factory pattern. You have two options here:

  1. factory class (similar to how IHttpClientFactory is implemented)

     public class RootService : IRootService
    {
    public RootService(INestedService nested, IOtherService other)
    {
    // ...
    }
    }
    
    
    public class RootServiceFactory : IRootServiceFactory
    {
    // in case you need other dependencies, that can be resolved by DI
    private readonly IServiceProvider services;
    
    
    public RootServiceFactory(IServiceProvider services)
    {
    this.services = services;
    }
    
    
    public IRootService CreateInstance(string connectionString)
    {
    // instantiate service that needs runtime parameter
    var nestedService = new NestedService(connectionString);
    
    
    // note that in this example, RootService also has a dependency on
    // IOtherService - ActivatorUtilities.CreateInstance will automagically
    // resolve that dependency, and any others not explicitly provided, from
    // the specified IServiceProvider
    return ActivatorUtilities.CreateInstance<RootService>(services,
    new object[] { nestedService, });
    }
    }
    

    and inject IRootServiceFactory instead of your IRootService

     IRootService rootService = rootServiceFactory.CreateInstance(connectionString);
    
  2. factory method

     services.AddTransient<Func<string,INestedService>>((provider) =>
    {
    return new Func<string,INestedService>(
    (connectionString) => new NestedService(connectionString)
    );
    });
    

    and inject the factory method into your service instead of INestedService

     public class RootService : IRootService
    {
    public INestedService NestedService { get; set; }
    
    
    public RootService(Func<string,INestedService> nestedServiceFactory)
    {
    NestedService = nestedServiceFactory("ConnectionStringHere");
    }
    
    
    public void DoSomething()
    {
    // implement
    }
    }
    

    or resolve it per call

     public class RootService : IRootService
    {
    public Func<string,INestedService> NestedServiceFactory { get; set; }
    
    
    public RootService(Func<string,INestedService> nestedServiceFactory)
    {
    NestedServiceFactory = nestedServiceFactory;
    }
    
    
    public void DoSomething(string connectionString)
    {
    var nestedService = nestedServiceFactory(connectionString);
    
    
    // implement
    }
    }
    

I devised this little pattern to help me resolve objects that require runtime parameters ,but also have dependencies which the DI container is able to resolve - I implemented this using the MS DI Container for a WPF App.

I already had a Service Locator (yes I know its a code smell - but I attempt to resolve that by the end of the example) that I used in specific scenarios to get access to objects in the DIC:

public interface IServiceFactory
{
T Get<T>();
}

Its implementation takes a func<> in the constructor to decouple the fact it relies on MS DI.

public class ServiceFactory : IServiceFactory
{
private readonly Func<Type, object> factory;


public ServiceFactory(Func<Type, object> factory)
{
this.factory = factory;
}


// Get an object of type T where T is usually an interface
public T Get<T>()
{
return (T)factory(typeof(T));
}
}

This was created in the composition root like so:

services.AddSingleton<IServiceFactory>(provider => new ServiceFactory(provider.GetService));

This pattern was extended to not only 'Get' objects of type T, but 'Create' objects of type T with parameters P:

public interface IServiceFactory
{
T Get<T>();


T Create<T>(params object[] p);
}

The implementation took another func<> to decouple the creation mechanism:

public class ServiceFactory : IServiceFactory
{
private readonly Func<Type, object> factory;
private readonly Func<Type, object[], object> creator;


public ServiceFactory(Func<Type, object> factory, Func<Type, object[], object> creator)
{
this.factory = factory;
this.creator = creator;
}


// Get an object of type T where T is usually an interface
public T Get<T>()
{
return (T)factory(typeof(T));
}


// Create (an obviously transient) object of type T, with runtime parameters 'p'
public T Create<T>(params object[] p)
{
IService<T> lookup = Get<IService<T>>();
return (T)creator(lookup.Type(), p);
}
}

The creation mechanism for the MS DI container is in the ActivatorUtilities extensions, here's the updated composition root:

        services.AddSingleton<IServiceFactory>(
provider => new ServiceFactory(
provider.GetService,
(T, P) => ActivatorUtilities.CreateInstance(provider, T, P)));

Now that we can create objects the problem becomes we have no way of determining the type of object we need without the DI container actually creating an object of that type, which is where the IService interface comes in:

public interface IService<I>
{
// Returns mapped type for this I
Type Type();
}

This is used to determine what type we are trying to create, without actually creating the type, its implementation is:

public class Service<I, T> : IService<I>
{
public Type Type()
{
return typeof(T);
}
}

So to pull it all together, in your composition root you can have objects that don't have runtime parameters which can be resolved by 'Get' and ones which do resolved by 'Create' e.g.:

services.AddSingleton<ICategorySelectionVM, CategorySelectionVM>();
services.AddSingleton<IService<ISubCategorySelectionVM>, Service<ISubCategorySelectionVM, SubCategorySelectionVM>>();
services.AddSingleton<ILogger, Logger>();

The CategorySelectionVM has only dependencies that can be resolved via the DIC:

public CategorySelectionVM(ILogger logger) // constructor

And this can be created by anyone with a dependency on the service factory like:

public MainWindowVM(IServiceFactory serviceFactory) // constructor
{
}


private void OnHomeEvent()
{
CurrentView = serviceFactory.Get<ICategorySelectionVM>();
}

Where as the SubCategorySelectionVM has both dependencies that the DIC can resolve, and dependencies only known at runtime:

public SubCategorySelectionVM(ILogger logger, Category c) // constructor

And these can be created like so:

private void OnCategorySelectedEvent(Category category)
{
CurrentView = serviceFactory.Create<ISubCategorySelectionVM>(category);
}

Update : I just wanted to add a little enhancement which avoided using the service factory like a service locator, so I created a generic service factory which could only resolve objects of type B:

public interface IServiceFactory<B>
{
T Get<T>() where T : B;


T Create<T>(params object[] p) where T : B;
}

The implementation of this depends on the original service factory which could resolve objects of any type:

public class ServiceFactory<B> : IServiceFactory<B>
{
private readonly IServiceFactory serviceFactory;


public ServiceFactory(IServiceFactory serviceFactory)
{
this.serviceFactory = serviceFactory;
}


public T Get<T>() where T : B
{
return serviceFactory.Get<T>();
}


public T Create<T>(params object[] p) where T : B
{
return serviceFactory.Create<T>(p);
}
}

The composition root adds the original service factory for all the generic typed factories to depend on, and any of the typed factories:

services.AddSingleton<IServiceFactory>(provider => new ServiceFactory(provider.GetService, (T, P) => ActivatorUtilities.CreateInstance(provider, T, P)));
services.AddSingleton<IServiceFactory<BaseVM>, ServiceFactory<BaseVM>>();

Now our main view model can be restricted to creating only objects that derive from BaseVM:

    public MainWindowVM(IServiceFactory<BaseVM> viewModelFactory)
{
this.viewModelFactory = viewModelFactory;
}


private void OnCategorySelectedEvent(Category category)
{
CurrentView = viewModelFactory.Create<SubCategorySelectionVM>(category);
}


private void OnHomeEvent()
{
CurrentView = viewModelFactory.Get<CategorySelectionVM>();
}

I know this is a bit old but thought i'd give my input since there is a easier way to do this in my opinion. This doesn't cover all the cases as shown in other posts. But this is a easy way of doing it.

public class MySingleton {
public MySingleton(string s, int i, bool b){
...
}
}

No lets create a service extention class to add easier and keep it neet

public static class ServiceCollectionExtentions
{
public static IServiceCollection RegisterSingleton(this IServiceCollection services, string s, int i, bool b) =>
services.AddSingleton(new MySingleton(s, i, b));
}

Now to call it from startup

services.RegisterSingleton("s", 1, true);

Further to @Tseng's extremely helpful answer, I found I could also adapt it to use delegates:

public delegate INestedService CreateNestedService(string connectionString);


services.AddTransient((provider) => new CreateNestedService(
(connectionString) => new NestedService(connectionString)
));

Implemented in RootService in the same way @Tseng suggested:

public class RootService : IRootService
{
public INestedService NestedService { get; set; }


public RootService(CreateNestedService createNestedService)
{
NestedService = createNestedService("ConnectionStringHere");
}


public void DoSomething()
{
// implement
}
}

I prefer this approach for cases where I need an instance of a factory in a class, as it means I can have a property of type CreateNestedService rather than Func<string, INestedService>.

IMHO, follow the options pattern. Define a strong type to hold your connection string, then an IConfigureOptions<T> to configure it from your user claim.

public class ConnectionString {
public string Value { get; set; }
}
public class ConfigureConnection : IConfigureOptions<ConnectionString> {
private readonly IHttpContextAccessor accessor;
public ConfigureConnection (IHttpContextAccessor accessor) {
this.accessor = accessor;
}
public void Configure(ConnectionString config) {
config.Value = accessor.HttpContext.User ...
}
}
public class NestedService {
...
public NestedService(IOptions<ConnectionString> connection) {
ConnectionString = connection.Value.Value;
}
...
}