如何使用.NET Core依赖项注入在运行时解析服务并注入其他构造函数参数?

时间:2019-01-29 17:07:26

标签: c# asp.net-core dependency-injection .net-core

我有一个用例,其中我想使用.NET Core依赖项注入来创建存储库实例,但是需要在运行时更改构造函数参数之一。确切地说,应在运行时确定的参数是“数据库连接”,它将指向由调用者确定的一个或另一个数据库。顺便说一句,这种类型没有向DI容器注册,但是其他所有类型都已注册。

调用方将使用存储库工厂类型来创建具有所需连接的存储库。

它看起来像这样:

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>();
    }
}

已注册以下类型:

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

工厂将被这样使用

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

我之所以保留所有基于接口的代码的原因之一,当然是用于单元测试。但是,从RepositoryFactory.Create<TRepository>的伪实现中可以看出,这也是一个问题,因为我到达了需要两者之一的位置:

  • 在DI容器中确定与IARepository关联的concret类型,以将其传递给ActivatorUtilities,以便在解析时使用IConnection的期望值创建它的实例其他带有IServiceProvider

  • 的构造函数参数
  • 以某种方式告诉IServiceProvider在获得特定服务时使用IConnection的特定实例

使用.NET Core DI完全可行吗?

(奖励问题:我应该使用其他更简单的方法吗?)

更新:我对示例代码进行了一些编辑,以期使我的意图更加清楚。这个想法是允许相同的存储库,完全相同的代码根据调用方的特定需求使用不同的连接(在应用程序启动期间配置)。总结一下:

  • 存储库的责任是在请求操作时在Connection上执行正确的查询。
  • 调用方将对存储库返回的数据进行操作
  • 但是,调用方可能会要求存储库在特定的Connection上执行其查询(在此示例中,此连接控制数据的新鲜度)

有几种解决方法来解决在工厂中注入正确连接的问题:

  • 在存储库中添加一个可变的Connection属性,并在创建后立即设置它=>此解决方案让我最困扰的是,它很容易忘记设置连接,例如在测试代码中。
  • 还为更改存储库的属性打开了一扇门。
  • 不要将Connection注入类中,而应将其作为方法参数传递给=>,这使API显得不太优雅,因为每个方法现在都有一个“ extra”参数,可以将其简单地提供给开始的类,额外的参数只是一个“实现细节”

2 个答案:

答案 0 :(得分:0)

由于DI不会创建IConnection,因此您可以将其从存储库构造函数中删除,并将其作为属性,然后在工厂中,可以在创建后分配其值:

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;
    }
}

答案 1 :(得分:0)

问题在于根本没有注册并尝试注入IRepository。根据您自己的规范,无法通过依赖项注入来创建存储库,因为传递给它的连接在运行时会有所不同。这样,您应该创建一个工厂(已经完成),然后注册并注入它。然后,将连接传递到您的Create方法。

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

您可能想在此处执行某种实例定位器模式。例如,您可以将创建的实例存储在以连接为键的ConcurrentDictionary中。然后,您将从字典返回。如果存储库实际上在竞争条件下被实例化了多次,这可能不是什么大问题-应该只是相当小的对象分配。但是,访问SemaphoreSlim时可以使用ConcurrentDictionary创建锁,以防止这种情况。

您尚未提供有关您的特定用例的大量信息,因此我还将添加一个潜在的替代解决方案。为此,必须通过config或其他方式定义连接。如果确实是 runtime 提供的,则此方法将无效。您可以为服务注册提供操作,例如:

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);
});