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

Posted

技术标签:

【中文标题】如何将运行时参数作为依赖解析的一部分传递?【英文标题】:How can I pass a runtime parameter as part of the dependency resolution? 【发布时间】:2016-10-11 05:00:57 【问题描述】:

我需要能够将连接字符串传递到我的一些服务实现中。我在构造函数中这样做。连接字符串可由用户配置,将添加 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'.

我有什么选择?

【问题讨论】:

一般来说,运行时值不应该注入到组件的构造函数中,如here。但是请注意,在您的情况下,连接字符串 不是 运行时值,因为该值在启动时是已知的并且是常量。另一方面,通过 Web 请求传入的值被视为运行时值。 我的通用解决方案:gist.github.com/ReallyLiri/c669c60db2109554d5ce47e03613a7a9 @Mugen - 您能否提供一些关于在两种方法中传递/创建的名称/值对中值的用途的见解 public static void AddSingletonWithConstructorParams( this IServiceCollection services, object paramsWithNames) public static void AddSingletonWithConstructorParams( this IServiceCollection services, params object[] parameters) @SOHODeveloper 如果你想注入参数,我的解决方案有两个选项:要么按值传递它们,但每个具体类型最多只能有一个值,或者将它们传递为键值对,您可以在其中指定实际的构造函数参数名称。它可用于需要许多相同类型参数的更复杂的构造函数。键值对由object 表示,因为我发现它更方便。 @Mugen - 我今天好像有点密集。您能否提供一个仅使用一个参数注册服务的示例,然后展示如何检索服务的实例。 【参考方案1】:

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

    工厂类(类似于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);
    

    工厂方法

     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
         
     
    

【讨论】:

你有这方面的完整样本吗?我不清楚,我有一种感觉,而不是某些部分丢失了。 这是否意味着我必须手动解决所有依赖关系? @KishanVaishnav 使用上面的示例是的(但是您是在组合根目录中执行此操作的,您在其中设置了工厂)。在很多依赖项上,您可能需要考虑this answer 问题是我想传递一个动态参数而不是静态参数。你能看看这个问题吗?并告诉我这个问题有什么问题。 ***.com/questions/54925945/… @KishanVaishnav:在你的IConnectionStringsService 上有一个方法,它接受用户 ID(或 tentant id - 你需要知道如何将连接字符串分配给用户)作为参数并返回连接该用户的字符串【参考方案2】:

简单配置

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"

如果您已经在 ConfigurationBuilder 中加载了 appSettings.json(通常位于 Startup 类的构造函数中),那么您的 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);

使用选项生成器

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

【讨论】:

嗨,Ivan,感谢您的建议,但我知道如何在启动类中添加服务。这个答案不涉及连接字符串将来自当前用户的 Claims 集合的事实 - AFAIK 这些在启动时不可用? @AntSwift:创建一个工厂类,将 IServiceProvider 传递给它,实现“CreateMyService”方法并将参数传递给它,使用所需的参数创建/实例化您的类。将工厂注入您的控制器/服务而不是所需的服务。如果整个类对您的需求很大,请将Func&lt;string,IMyService&gt; 注册为依赖项并将其注入而不是IMyService,然后通过myServiceFactory(myConnectionString) 解决它。 @Tseng - 感谢您的建议。我现在正在尝试实现这一点,一切看起来都很好。您应该将评论提升为答案并获得所有荣耀。 我无法调试 var connectionString = Configuration["Data:ConnectionString"]; //用appSettings.json【参考方案3】:

我设计了这个小模式来帮助我解决需要运行时参数的对象,但也具有 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));

这种模式不仅被扩展为“获取”类型 T 的对象,而且“创建”类型为 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>();
    

【讨论】:

非常好。唯一的建议是添加为 Scoped vs Singleton。这样,它通过使用正确的提供程序来保证范围相关的依赖关系 =>【参考方案4】:

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

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

【讨论】:

【参考方案5】:

除了@Tseng 的非常有帮助的答案之外,我发现我还可以调整它以使用代表:

public delegate INestedService CreateNestedService(string connectionString);

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

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

public class RootService : IRootService

    public INestedService NestedService  get; set; 

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

    public void DoSomething()
    
        // implement
    

对于需要类中的工厂实例的情况,我更喜欢这种方法,因为这意味着我可以拥有CreateNestedService 类型的属性,而不是Func&lt;string, INestedService&gt;

【讨论】:

将代理命名为接口有什么好处?因为这非常令人困惑,并且违反了最小惊讶原则。 @IanKemp 感谢您指出这一点 - 我已经更正了。【参考方案6】:

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

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

不要让我们创建一个服务扩展类来更容易添加并保持它的整洁

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

【讨论】:

以上是关于如何将运行时参数作为依赖解析的一部分传递?的主要内容,如果未能解决你的问题,请参考以下文章

通过 Gradle 运行 Java 类时传递系统属性和参数的问题

通过 Gradle 运行 Java 类时传递系统属性和参数的问题

使用 Gradle 时如何排除传递依赖项的所有实例?

运行java jar时整数数组作为参数

Flink 如何解析与传递参数

Flink 如何解析与传递参数