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

时间:2016-06-10 09:26:31

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

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

到目前为止一切都很好。

不幸的是,我还希望能够充分利用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'.

我有什么选择?

6 个答案:

答案 0 :(得分:31)

简单配置

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模式。

答案 1 :(得分:27)

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

  1. 工厂方法

    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
        }
    }
    
  2. 工厂类

    public class RootServiceFactory : IRootServiceFactory 
    {
        // in case you need other dependencies, that can be resolved by DI
        private readonly IServiceCollection services;
    
        public RootServiceCollection(IServiceCollection services)
        {
            this.services = services;
        }
    
        public CreateInstance(string connectionString) 
        {
            // instantiate service that needs runtime parameter
            var nestedService = new NestedService(connectionString);
    
            // resolve another service that doesn't need runtime parameter
            var otherDependency = services.GetService<IOtherService>()
    
            // pass both into the RootService constructor and return it
            return new RootService(otherDependency, nestedDependency);
        }
    }
    

    并注入IRootServiceFactory而不是IRootService

    IRootService rootService = rootServiceFactory.CreateIntsance(connectionString);
    

答案 2 :(得分:0)

我设计了这个小模式来帮助我解析需要运行时参数的对象,但也具有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类型的“获取”对象,而且扩展到带有参数P的T类型的“创建”对象。

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

答案 3 :(得分:0)

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

{__index = string}

否,请创建一个服务扩展类以使其更容易添加并保持简洁

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

答案 4 :(得分:0)

除了@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<string, INestedService> 的属性。

答案 5 :(得分:0)

恕我直言,请遵循选项模式。定义一个强类型来保存您的连接字符串,然后定义一个 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;
    }
    ...
}