如何将运行时参数作为依赖项解析的一部分传递?

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

我需要能够将连接字符串传递到我的一些服务实现中。我在构造函数中这样做。连接字符串可由用户配置,将添加 ClaimsPrincipal 作为声明。

到目前为止一切正常。

不幸的是,我也希望能够充分利用 ASP.NET Core 中的依赖注入功能,并通过 DI 解决服务实现。

我有一个 POC 实现:

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

这些服务已在启动时注册,INestedService已添加控制器的构造函数。

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

不出所料,我收到错误 Unable to resolve service for type 'System.String' while attempting to activate 'Test.Dependency.Services.NestedService'.

我有哪些选择?

配置简单

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

与appSettings.json

如果您决定将连接字符串隐藏在 appSettings.json 中,例如:

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

然后假设您已将 appSettings.json 加载到 ConfigurationBuilder(通常位于 Startup class 的构造函数中),那么您的 ConfigureServices 将如下所示:

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

有扩展方法

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

那么您的 ConfigureServices 方法将如下所示

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

带有选项生成器

如果您需要更多参数,可以更进一步,创建一个选项 class,然后将其传递给 RootService 的构造函数。如果复杂了,可以使用Builder模式。

要在应用程序启动时传递未知的运行时参数,您必须使用工厂模式。您在这里有两个选择:

  1. 工厂class(类似于IHttpClientFactory的实现方式)

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

    并注入 IRootServiceFactory 而不是你的 IRootService

     IRootService rootService = rootServiceFactory.CreateInstance(connectionString);
    
  2. 工厂方法

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

    并将工厂方法注入您的服务而不是 INestedService

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

    或每次调用解决它

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

我设计了这个小模式来帮助我解析需要运行时参数的对象,但也有 DI 容器能够解析的依赖项 - 我使用 WPF 应用程序的 MS DI 容器实现了这个。

我已经有一个服务定位器(是的,我知道它有代码味道 - 但我试图在示例结束时解决它),我在特定场景中使用它来访问 DIC 中的对象:

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

它的实现在构造函数中使用 func<> 来解耦它依赖于 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));
    }
}

这是在组合根中创建的,如下所示:

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

此模式不仅扩展到 'Get' 类型 T 的对象,而且扩展到 'Create' 类型 T 的对象,参数 P:

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

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

实现采用了另一个 func<> 来解耦创建机制:

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

MS DI 容器的创建机制在 ActivatorUtilities 扩展中,这里是更新的组合根:

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

既然我们可以创建对象,问题就变成了如果没有 DI 容器实际创建该类型的对象,我们就无法确定我们需要的对象类型,这就是 IService 接口的用武之地:

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

这个用来判断我们要创建的是什么类型,而不实际创建类型,它的实现是:

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

因此,为了将它们整合在一起,在您的组合根中,您可以拥有没有运行时参数的对象,这些参数可以由 'Get' 解析,而那些对象可以由 'Create' 解析,例如:

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

CategorySelectionVM 只有可以通过 DIC 解析的依赖项:

public CategorySelectionVM(ILogger logger) // constructor

这可以由依赖于服务工厂的任何人创建,例如:

public MainWindowVM(IServiceFactory serviceFactory) // constructor
{
}

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

因为 SubCategorySelectionVM 既有 DIC 可以解决的依赖关系,也有只有在运行时才知道的依赖关系:

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

这些可以像这样创建:

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

更新:我只是想添加一点增强功能,避免像服务定位器一样使用服务工厂,所以我创建了一个只能解析类型 B 的对象的通用服务工厂:

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

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

这个的实现依赖于可以解析任何类型对象的原始服务工厂:

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

组合根添加原始服务工厂供所有通用类型工厂依赖,以及任何类型工厂:

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

现在我们的主视图模型可以限制为仅创建从 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>();
    }

我知道这有点旧,但我想我会提供我的意见,因为在我看来有更简单的方法可以做到这一点。这并不涵盖其他帖子中显示的所有情况。但这是一种简单的方法。

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

不,让我们创建一个服务扩展 class 以更轻松地添加并保持它

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

现在从启动时调用它

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

除了@Tseng 非常有帮助的回答之外,我发现我也可以调整它以使用委托:

public delegate INestedService CreateNestedService(string connectionString);

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

RootService中以同样的方式实现 @Tseng 建议:

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

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

    public void DoSomething()
    {
        // implement
    }
}

如果我需要 class 中的工厂实例,我更喜欢这种方法,因为这意味着我可以拥有 CreateNestedService 类型的 属性 而不是 Func<string, INestedService>.

恕我直言,请遵循选项模式。定义一个强类型来保存您的连接字符串,然后定义一个 IConfigureOptions<T> 以根据您的用户声明对其进行配置。

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