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:
Several workarounds have come up to the problem of injecting the right connection in the factory:
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.