简体   繁体   中英

How to resolve service and inject additional constructor parameters at runtime using .NET Core dependency injection?

I have a use case in which I want to create repository instances using .NET Core dependency injection, but need to change one of the constructor parameters at runtime. To be precise, the parameter that should be decided at runtime is the "database connection", which will point to one or another database decided by the caller. This type, by the way, is not registered with the DI container, but all the others are.

The caller will use a repository factory type to create the repository with the desired connection.

It looks something like this:

class ARepository : IARepository
{
    public ARepository(IService1 svc1, IService2 svc2, IConnection connection) { }

    public IEnumerable<Data> GetData() { }
}

class RepositoryFactory : IRepositoryFactory
{
    public RepositoryFactory(IServiceProvider serviceProvider) =>
        _serviceProvider = serviceProvider;

    public IConnection CreateAlwaysFresh<TRepository>() =>
        this.Create<TRepository>(new FreshButExpensiveConnection());

    public IConnection CreatePossiblyStale<TRepository>() =>
        return this.Create<TRepository>(new PossiblyStaleButCheapConnection());

    private IConnection Create<TRepository>(IConnection conn)
    {
        // Fails because TRepository will be an interface, not the actual type
        // that I want to create (see code of AService below)
        return ActivatorUtilities.CreateInstance<TRepository>(_serviceProvider,conn);

        // Fails because IConnection is not registered, which is normal
        // because I want to use the instance held in parameter conn
        return _serviceProvider.GetService<TRepository>();
    }
}

The following types were registered:

services.AddTransient<IARepository, ARepository>();  // Probably not needed
services.AddTransient<IService1, Service1>();
services.AddTransient<IService2, Service2>();
services.AddTransient<IRepositoryFactory, RepositoryFactory>();

And the factory would be used as such:

class AService
{
    public AService(IRepositoryFactory factory)
    {
        _factory = factory;
    }

    public void ExecuteCriticalAction()
    {
        var repo = _factory.CreateAlwaysFresh<IARepository>();

        // Gets the freshest data because repo was created using
        // AlwaysFresh connection
        var data = repo.GetData();

        // Do something critical with data
    }

    public void ExecuteRegularAction()
    {
        var repo = _factory.CreatePossiblyStale<IARepository>();

        // May get slightly stale data because repo was created using 
        // PossiblyStale connection
        var data = repo.GetData();

        // Do something which won't suffer is data is slightly stale
    }
}

One of the reasons why I've kept all the code based on interfaces is, of course, for unit testing. However, as you can see from the pseudo-implementation of RepositoryFactory.Create<TRepository> , this is also a problem because I reach a point where I need to either :

  • determine the concret type associated to IARepository in the DI container to pass it to ActivatorUtilities in order to create an instance of it using the desired value of IConnection while resolving other constructor parameters with IServiceProvider , or

  • somehow tell IServiceProvider to use a particular instance of IConnection when getting a particular service

Is this at all possible using .NET Core DI?

(Bonus question: Should I have used another, simpler, approach?)

Update: I edited the sample code a little to hopefully make my intentions more clear. The idea is to allow the same repository, exact same code, to use different connections (which are configured during app startup) depending on the caller's specific needs. To summarise:

  • a Repository's responsibility is to execute the correct queries on a Connection when an action is requested.
  • the Caller will act on the data returned by the repository
  • however , the Caller might require the Repository to execute its queries on a particular Connection (which, in this example, controls data freshness)

Several workarounds have come up to the problem of injecting the right connection in the factory:

  • add a mutable Connection property to the Repositories and set it right after creation => what bothers me most with this solution is that it makes it very easy to forget to set a connection, for example in test code. It also leaves a door open to change a property of the repository which should be immutable.
  • do not inject the Connection in the class, but pass it as a method parameter instead => this makes for a less elegant API, since every method will now have an "extra" parameter, which could've been simply provided to the class to begin with, and the extra parameter is but an "implementation detail"

Since the IConnection will not be created by the DI, you could remove it from the repository constructor and have it as a property, then on your factory you can assign its value after the creation:

interface IARepository 
{ 
    IConnection Connection { set; }
}

class ARepository : IARepository
{
    public IConnection Connection { private get; set; }

    public ARepository(IService1 svc1, IService2 svc2)
    { /* ... */ }
}


class RepositoryFactory : IRepositoryFactory
{
    /* ... */
    private IConnection Create<TRepository>(IConnection conn) 
        where TRepository : IARepository
    {
        var svc = _serviceProvider.GetService<TRepository>();
        svc.Connection = conn;
        return svc;
    }
}

The issue is in registering and attempting to inject IRepository at all. By your own spec, the repo cannot be created via dependency injection because the connection passed into it will vary at runtime. As such, you should create a factory (which you've already done) and register and inject that instead. Then, you pass in the connection to your Create method.

public TRepository Create<TRepository>(IConnection conn)
    where TRepository : IRepository, new()
{
    return new TRepository(conn);
}

You'll likely want to do some sort of instance locator pattern here instead. For example, you might store the created instance in a ConcurrentDictionary keyed by the connection. Then, you'd return from the dictionary instead. It's probably not a huge deal if the repository actually gets instantiated multiple times in race conditions - should just be a fairly minor object allocation. However, you can employ SemaphoreSlim to create locks when accessing the ConcurrentDictionary , to prevent this.

You haven't given a ton of info about your specific use case, so I'll also add a potential alternate solution. For this to work, the connection has to be defined by config or something. If it truly is runtime -provided, this won't work, though. You can provide an action to the service registration, for example:

services.AddScoped<IRepository, ARepository>(p => {
    // create your connection here
    // `p` is an instance of `IServiceProvider`, so you can do service lookups
    return new ARepository(conn);
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM