依赖注入
ABP的依赖注入系统基于微软的 依赖注入扩展库(Microsoft.Extensions.DependencyInjection NuGet包)开发。因此,其文档在ABP中同样适用。
虽然ABP核心不依赖任何第三方DI提供程序,但为了确保某些ABP功能正常工作,需要使用支持动态代理及其他高级特性的提供程序。启动模板默认安装了 Autofac。更多信息请参阅 Autofac 集成 文档。
模块化
由于ABP是模块化框架,每个模块在其独立的 模块类 中通过依赖注入定义并注册自身服务。示例:
public class BlogModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//在此处注册依赖项
}
}
约定式注册
ABP引入了约定式服务注册。您无需额外操作即可按约定注册服务,系统会自动完成。若需禁用此功能,可在模块类的构造函数中将SkipAutoServiceRegistration设为true。示例:
public class BlogModule : AbpModule
{
public BlogModule()
{
SkipAutoServiceRegistration = true;
}
}
禁用自动注册后,需手动注册服务。此时可使用AddAssemblyOf扩展方法按约定注册所有服务。示例:
public class BlogModule : AbpModule
{
public BlogModule()
{
SkipAutoServiceRegistration = true;
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAssemblyOf<BlogModule>();
}
}
以下章节详细解释约定与配置。
固有注册类型
部分特定类型默认注册到依赖注入中。例如:
- 模块类注册为单例。
- MVC控制器(继承
Controller或AbpController)注册为瞬时实例。 - MVC页面模型(继承
PageModel或AbpPageModel)注册为瞬时实例。 - MVC视图组件(继承
ViewComponent或AbpViewComponent)注册为瞬时实例。 - 应用服务(继承
ApplicationService类或其子类)注册为瞬时实例。 - 仓储(实现
BasicRepositoryBase类或其子类)注册为瞬时实例。 - 领域服务(实现
IDomainService接口或继承DomainService类)注册为瞬时实例。
示例:
public class BlogPostAppService : ApplicationService
{
}
BlogPostAppService因继承已知基类而自动以瞬时生命周期注册。
依赖接口
实现以下接口时,类会自动注册到依赖注入:
ITransientDependency注册为瞬时生命周期。ISingletonDependency注册为单例生命周期。IScopedDependency注册为作用域生命周期。
示例:
public class TaxCalculator : ITransientDependency
{
}
TaxCalculator因实现ITransientDependency而自动以瞬时生命周期注册。
依赖特性
另一种配置依赖注入服务的方法是使用DependencyAttribute。其属性包括:
Lifetime:注册的生命周期:Singleton、Transient或Scoped。TryRegister:设为true时仅在该服务未注册过时进行注册。使用IServiceCollection的TryAdd...扩展方法。ReplaceServices:设为true时替换已注册的服务。使用IServiceCollection的Replace扩展方法。
示例:
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
public class TaxCalculator
{
}
若Dependency特性定义了Lifetime属性,其优先级高于其他依赖接口。
暴露服务特性
ExposeServicesAttribute用于控制相关类提供哪些服务。示例:
[ExposeServices(typeof(ITaxCalculator))]
public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency
{
}
TaxCalculator类仅暴露ITaxCalculator接口。这意味着您只能注入ITaxCalculator,而不能注入TaxCalculator或ICalculator。
按约定暴露服务
若未指定暴露的服务,ABP按约定暴露服务。以上述TaxCalculator为例:
- 默认暴露类本身,即可通过
TaxCalculator类注入。 - 默认暴露默认接口。默认接口按命名约定确定。此例中
ICalculator和ITaxCalculator是TaxCalculator的默认接口,但ICanCalculate不是。满足命名约定的泛型接口(如ICalculator<string>)也被视为默认接口。 - 对于单例和作用域服务,若暴露多个服务,解析的实例将相同。此行为要求暴露类本身。
组合使用
只要有意义,可组合使用特性和接口。
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(ITaxCalculator))]
public class TaxCalculator : ITaxCalculator, ITransientDependency
{
}
暴露键控服务特性
ExposeKeyedServiceAttribute用于控制相关类提供哪些键控服务。示例:
[ExposeKeyedService<ITaxCalculator>("taxCalculator")]
[ExposeKeyedService<ICalculator>("calculator")]
public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency
{
}
上例中,TaxCalculator类以键taxCalculator暴露ITaxCalculator接口,以键calculator暴露ICalculator接口。这意味着可从IServiceProvider获取键控服务,如下所示:
var taxCalculator = ServiceProvider.GetRequiredKeyedService<ITaxCalculator>("taxCalculator");
var calculator = ServiceProvider.GetRequiredKeyedService<ICalculator>("calculator");
此外,可在构造函数中使用 FromKeyedServicesAttribute 解析特定键控服务:
public class MyClass
{
//...
public MyClass([FromKeyedServices("taxCalculator")] ITaxCalculator taxCalculator)
{
TaxCalculator = taxCalculator;
}
}
注意
ExposeKeyedServiceAttribute仅暴露键控服务。因此,若不使用FromKeyedServicesAttribute(如上例所示),无法在应用中注入ITaxCalculator或ICalculator接口。若需同时暴露键控和非键控服务,可组合使用ExposeServicesAttribute和ExposeKeyedServiceAttribute特性,如下所示:
[ExposeKeyedService<ITaxCalculator>("taxCalculator")]
[ExposeKeyedService<ICalculator>("calculator")]
[ExposeServices(typeof(ITaxCalculator), typeof(ICalculator))]
public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency
{
}
手动注册
某些情况下,可能需要手动向IServiceCollection注册服务,尤其是需要使用自定义工厂方法或单例实例时。此时,可直接按 微软文档 描述添加服务。示例:
public class BlogModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//注册实例为单例
context.Services.AddSingleton<TaxCalculator>(new TaxCalculator(taxRatio: 0.18));
//注册从IServiceProvider解析的工厂方法
context.Services.AddScoped<ITaxCalculator>(
sp => sp.GetRequiredService<TaxCalculator>()
);
}
}
替换服务
如需替换现有服务(由ABP或其他模块依赖定义),有两种选择:
- 使用上述ABP的
Dependency特性。 - 使用微软依赖注入库的
IServiceCollection.Replace方法。示例:
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//替换IConnectionStringResolver服务
context.Services.Replace(
ServiceDescriptor.Transient<
IConnectionStringResolver,
MyConnectionStringResolver
>());
}
}
注入依赖
使用已注册服务有三种常见方式。
构造函数注入
这是向类注入服务最常用的方式。例如:
public class TaxAppService : ApplicationService
{
private readonly ITaxCalculator _taxCalculator;
public TaxAppService(ITaxCalculator taxCalculator)
{
_taxCalculator = taxCalculator;
}
public async Task DoSomethingAsync()
{
//...使用_taxCalculator...
}
}
TaxAppService在构造函数中获取ITaxCalculator。依赖注入系统在运行时自动提供所需服务。
构造函数注入是向类注入依赖的首选方式。这样,除非提供所有构造函数注入的依赖项,否则无法构造类。因此,类明确声明了其所需服务。
属性注入
微软依赖注入库不支持属性注入。但ABP可与第三方DI提供程序(如 Autofac )集成以实现属性注入。示例:
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
public MyService()
{
Logger = NullLogger<MyService>.Instance;
}
public async Task DoSomethingAsync()
{
//...使用Logger写入日志...
}
}
对于属性注入依赖,需声明具有公共setter的公共属性。这允许DI框架在创建类后设置它。
属性注入的依赖通常被视为可选依赖。即服务可在没有它们的情况下正常工作。Logger就是此类依赖,MyService无需日志记录也能继续工作。
为使依赖真正可选,通常为依赖设置默认/回退值。此示例中使用NullLogger作为回退。因此,若DI框架或您在创建MyService后未设置Logger属性,MyService可工作但不写入日志。
属性注入的一个限制是无法在构造函数中使用依赖,因其在对象构造后设置。
当设计具有某些默认注入公共服务的基类时,属性注入也很有用。若使用构造函数注入,所有派生类也需在其构造函数中注入依赖服务,这会增加开发难度。但请注意,对非可选服务使用属性注入会使类的需求难以清晰可见,需非常谨慎。
禁用属性注入特性
可在类或其属性上使用[DisablePropertyInjection]特性,以禁用整个类或特定属性的属性注入。
// 禁用MyService类的所有属性
[DisablePropertyInjection]
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
public ITaxCalculator TaxCalculator { get; set; }
}
// 仅禁用TaxCalculator属性
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
[DisablePropertyInjection]
public ITaxCalculator TaxCalculator { get; set; }
}
IInjectPropertiesService
可使用IInjectPropertiesService服务注入对象的属性。通常用于DI之外的服务,如手动创建的服务。
var injectPropertiesService = serviceProvider.GetRequiredService<IInjectPropertiesService>();
var instance = new TestService();
// 设置instance上可由IServiceProvider解析的任何属性。
injectPropertiesService.InjectProperties(instance);
// 设置instance上未设置且可由IServiceProvider解析的任何属性。
injectPropertiesService.InjectUnsetProperties(instance);
从IServiceProvider解析服务
可能需要直接从IServiceProvider解析服务。此时,可向类注入IServiceProvider,并使用GetService或GetRequiredService方法,如下所示:
public class MyService : ITransientDependency
{
private readonly ITaxCalculator _taxCalculator;
public MyService(IServiceProvider serviceProvider)
{
_taxCalculator = serviceProvider.GetRequiredService<ITaxCalculator>();
}
}
处理多实现
可注册同一服务接口的多个实现。假设有一个IExternalLogger接口及两个实现:
public interface IExternalLogger
{
Task LogAsync(string logText);
}
public class ElasticsearchExternalLogger : IExternalLogger
{
public async Task LogAsync(string logText)
{
//TODO...
}
}
public class AzureExternalLogger : IExternalLogger
{
public Task LogAsync(string logText)
{
throw new System.NotImplementedException();
}
}
此例中,尚未向依赖注入系统注册任何实现类。因此,若尝试注入IExternalLogger接口,将收到未找到实现的错误。
若为IExternalLogger接口注册ElasticsearchExternalLogger和AzureExternalLogger服务,然后尝试注入IExternalLogger接口,则将使用最后注册的实现。
注入IExternalLogger接口的服务示例:
public class MyService : ITransientDependency
{
private readonly IExternalLogger _externalLogger;
public MyService(IExternalLogger externalLogger)
{
_externalLogger = externalLogger;
}
public async Task DemoAsync()
{
await _externalLogger.LogAsync("示例日志消息...");
}
}
如前所述,此处获取最后注册的实现。但如何确定最后注册的实现?
若实现了一个依赖接口(如ITransientDependency),则注册顺序不确定(可能取决于类的命名空间)。最后注册的实现可能不符合预期。因此,不建议使用依赖接口注册多实现。
可在模块的ConfigureServices方法中注册服务:
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<IExternalLogger, ElasticsearchExternalLogger>();
context.Services.AddTransient<IExternalLogger, AzureExternalLogger>();
}
此情况下,注入IExternalLogger接口时获取AzureExternalLogger实例,因为最后注册的实现是AzureExternalLogger类。
当接口有多个实现时,可能需处理所有这些实现。假设需向所有外部日志记录器写入日志。可修改MyService实现如下:
public class MyService : ITransientDependency
{
private readonly IEnumerable<IExternalLogger> _externalLoggers;
public MyService(IEnumerable<IExternalLogger> externalLoggers)
{
_externalLoggers = externalLoggers;
}
public async Task DemoAsync()
{
foreach (var externalLogger in _externalLoggers)
{
await externalLogger.LogAsync("示例日志消息...");
}
}
}
此例中,注入IEnumerable<IExternalLogger>而非IExternalLogger,因此拥有IExternalLogger实现的集合。然后使用foreach循环向所有IExternalLogger实现写入相同日志文本。
若使用IServiceProvider解析依赖,则使用其GetServices方法获取服务实现的集合:
IEnumerable<IExternalLogger> services = _serviceProvider.GetServices<IExternalLogger>();
释放/处置服务
若使用构造函数或属性注入,无需关心释放服务资源。但若从IServiceProvider解析服务,某些情况下可能需要处理释放服务资源。
ASP.NET Core在当前HTTP请求结束时释放所有服务,即使直接从IServiceProvider解析(假设注入了IServiceProvider)。但在以下情况下可能需要手动释放/处置解析的服务:
- 代码在ASP.NET Core请求外执行,且执行器未处理服务范围。
- 仅拥有根服务提供程序的引用。
- 可能需要立即释放和处置服务(例如,创建过多占用大量内存的服务,且不想过度使用内存)。
在任何情况下,可创建服务范围块以安全立即释放服务:
using (var scope = _serviceProvider.CreateScope())
{
var service1 = scope.ServiceProvider.GetService<IMyService1>();
var service2 = scope.ServiceProvider.GetService<IMyService2>();
}
两种服务在创建的范围被处置时(在using块结束时)释放。
缓存服务提供程序
ABP提供两种特殊服务以优化从IServiceProvider解析服务。ICachedServiceProvider和ITransientCachedServiceProvider均继承IServiceProvider接口,并在内部缓存解析的服务,因此即使多次解析服务,也会获取相同的服务实例。
主要区别在于ICachedServiceProvider本身注册为作用域,而ITransientCachedServiceProvider注册为瞬时实例。
以下示例注入ICachedServiceProvider服务,并在DoSomethingAsync方法中解析服务:
public class MyService : ITransientDependency
{
private readonly ICachedServiceProvider _serviceProvider;
public MyService(ICachedServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task DoSomethingAsync()
{
var taxCalculator = _serviceProvider.GetRequiredService<ITaxCalculator>();
// TODO: 使用taxCalculator
}
}
使用此方式时,无需处理创建服务范围和处置解析的服务(如上文释放/处置服务章节所述)。因为从ICachedServiceProvider解析的所有服务将在MyService实例的服务范围被处置时释放。此外,无需担心内存泄漏(因调用DoSomethingAsync过多而创建过多ITaxCalculator实例),因为仅创建一个ITaxCalculator实例并被重用。
由于ICachedServiceProvider和ITransientCachedServiceProvider扩展了标准IServiceProvider接口,可在其上使用IServiceProvider接口的所有扩展方法。此外,它们还提供其他方法,为未找到的服务(即未注册到依赖注入系统的服务)提供默认值或工厂方法。注意,默认值(或工厂方法返回的值)也会被缓存和重用。
除非需要按使用创建服务缓存,否则使用ICachedServiceProvider(而非ITransientCachedServiceProvider)。ITransientCachedServiceProvider确保创建的服务实例不与其他任何服务共享,即使它们在同一服务范围内。从ICachedServiceProvider解析的服务与同一服务范围内的其他服务共享(例如在同一HTTP请求中),因此可视为更优化。
ABP还提供
IAbpLazyServiceProvider服务。其存在是为了向后兼容,且与ITransientCachedServiceProvider服务工作方式完全相同。因此,请使用ITransientCachedServiceProvider,因为IAbpLazyServiceProvider可能在未来的ABP版本中移除。
使用
ICachedServiceProvider的另一优势是,在HTTP请求期间,若服务的构造函数需要注入许多依赖项,可能会对性能产生负面影响,因为注入的服务可能并非当前请求全部使用。通过按需解析服务,可有效避免性能下降。
高级特性
IServiceCollection.OnRegistered事件
可能希望对注册到依赖注入的每个服务执行操作。在模块的PreConfigureServices方法中,使用OnRegistered方法注册回调,如下所示:
public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistered(ctx =>
{
var type = ctx.ImplementationType;
//...
});
}
}
ImplementationType提供服务类型。此回调通常用于向服务添加拦截器。示例:
public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistered(ctx =>
{
if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true))
{
ctx.Interceptors.TryAdd<MyLogInterceptor>();
}
});
}
}
此示例简单检查服务类是否具有MyLogAttribute特性,若有则向拦截器列表添加MyLogInterceptor。
注意,若服务类暴露多个服务/接口,
OnRegistered回调可能被多次调用。因此,使用Interceptors.TryAdd方法而非Interceptors.Add方法是安全的。参见动态代理/拦截器的 文档 。
IServiceCollection.OnActivated事件
OnActivated事件在服务完全构造后触发。此处可执行依赖于服务完全构造的应用级任务——这种情况应很少见。
var serviceDescriptor = ServiceDescriptor.Transient<MyServer, MyServer>();
services.Add(serviceDescriptor);
if (setIsReadOnly)
{
services.OnActivated(serviceDescriptor, x =>
{
x.Instance.As<MyServer>().IsReadOnly = true;
});
}
注意,同一
ServiceDescriptor可多次注册OnActivated事件。
第三方提供程序
虽然ABP核心不依赖任何第三方DI提供程序,但为了确保某些ABP功能正常工作,需要使用支持动态代理及其他高级特性的提供程序。
启动模板默认安装了Autofac。更多信息请参阅 Autofac集成 文档。
抠丁客


