如何将运行时参数作为依赖项解析的一部分传递?
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模式。
要在应用程序启动时传递未知的运行时参数,您必须使用工厂模式。您在这里有两个选择:
工厂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);
工厂方法
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;
}
...
}
我需要能够将连接字符串传递到我的一些服务实现中。我在构造函数中这样做。连接字符串可由用户配置,将添加 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模式。
要在应用程序启动时传递未知的运行时参数,您必须使用工厂模式。您在这里有两个选择:
工厂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);
工厂方法
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;
}
...
}