Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"
like image 693
madd0 Avatar asked Jan 29 '19 17:01

madd0


People also ask

How can dependency injection be resolved?

Resolve dependencies using IServiceProvider You can use the IServiceCollection interface to create a dependency injection container. Once the container has been created, the IServiceCollection instance is composed into an IServiceProvider instance. You can use this instance to resolve services.

How is dependency injection done in .NET Core?

The interface-based dependency injection can be achieved by creating the common interface and other classes are implements this interface to inject the dependency. In this type of DI, we can use either constructor injection or setter injection. There is a built-in support of dependency injection in ASP.net Core.

How can we inject the service dependency into the controller?

ASP.NET Core injects objects of dependency classes through constructor or method by using built-in IoC container. The built-in container is represented by IServiceProvider implementation that supports constructor injection by default.


1 Answers

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;
    }
}
like image 194
thepirat000 Avatar answered Sep 22 '22 10:09

thepirat000