这篇文章我们来深入探讨 ASP.NET CoreMVC Core 中的依赖注入, 我们将示范几乎所有可能的操作把依赖项注入到组件中
依赖注入是 ASP.NET Core 的核心, 它能让您应用程序中的组件增强可测试性, 还使您的组件只依赖于能够提供所需服务的某些组件
举个例子, 这里我们有一个接口和它的实现类:
- public interface IDataService
- {IList<DataClass> GetAll();
- }
- public class DataService : IDataService
- {
- public IList<DataClass> GetAll()
- {
- //Get data...
- return data;
- }
- }
如果另一个服务依赖于 DataService, 那么它们依赖于特定的实现, 测试这样的服务可能会非常困难如果该服务依赖于 IDataService, 那么它们只关心接口提供的契约实现什么并不重要, 它使我们能够通过一个模拟实现来测试服务的行为
1. 服务生命周期
在我们讨论如何在实践中进行注入之前, 了解什么是服务生命周期至关重要当一个组件通过依赖注入请求另一个组件时, 它所接收的实例是否对该组件的实例来说是唯一的, 这取决于它的生命周期设置生命周期从而决定组件实例化的次数, 以及组件是否共享
在 ASP.NET Core 中, 内置的 DI 容器有三种模式:
- Singleton
- Scoped
- Transient
Singleton 意味着只会创建一个实例, 该实例在需要它的所有组件之间共享因此始终使用相同的实例
Scoped 意味着每个作用域创建一个实例作用域是在对应用程序的每个请求上创建的, 因此, 任何注册为 Scoped 的组件每个请求都会创建一次
Transient 每次请求时都会创建瞬态组件, 并且永远不会共享
理解这一点非常重要, 如果将组件 A 注册为单例, 则它不能依赖于具有 Scoped 或 Transient 生命周期的组件总而言之:
组件不能依赖比自己的生命周期小的组件
违反这条规则的后果显而易见, 依赖的组件可能会在依赖项之前释放
通常, 您希望将组件 (如应用程序范围的配置容器) 注册为 Singleton 数据库访问类 (如 Entity Framework 上下文) 建议使用 Scoped, 以便可以重复使用连接但是如果您想并行运行任何东西, 请记住 Entity Framework 上下文不能由两个线程共享如果您需要这样做, 最好将上下文注册为 Transient, 这样每个组件都有自己的上下文实例而且可以并行运行
2. 服务注册
注册服务是在 Startup 类的
ConfigureServices(IServiceCollection)
方法中完成的
这是一个服务注册的例子:
services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));
这行代码将 DataService 添加到服务集合中服务类型设置为 IDataService, 因此如果请求了该类型的实例, 则它们将获得 DataService 的实例生命周期也设置为 Transient, 这样每次都会创建一个新实例
ASP.NET Core 提供了很多扩展方法, 使注册各种生命周期的服务和其他设置更加方便
下面是使用扩展方法的更简单的示例:
services.AddTransient<IDataService, DataService>();
是不是更简单一点? 封装后它当然更容易调用, 这样做更简单对于不同的生命周期, 也有类似的扩展方法, 你也许可以猜到它们的名字
如果愿意, 您也可以在使用单一类型注册(实现类型 = 服务类型):
services.AddTransient<DataService>();
但是呢, 当然组件必须取决于具体的类型, 所以这可能是不需要的
2.1. 实现工厂
在一些特殊情况下, 您可能想要接管某些服务的实例化在这种情况下, 您可以在服务描述符上注册一个实现工厂 (Implementation Factory) 这有一个例子:
- services.AddTransient<IDataService, DataService>((ctx) =>
- {
- IOtherService svc = ctx.GetService<IOtherService>();
- //IOtherService svc = ctx.GetRequiredService<IOtherService>();
- return new DataService(svc);
- });
它使用另一个组件 IOtherService 实例化 DataService 您可以使用 GetService<T>()或
GetRequiredService<T>()
来获取在服务集合中注册的依赖项
区别在于 GetService<T>()如果找不到 T 类型服务, 则返回 null;
GetRequiredService<T>()
如果找不到它, 则会引发
InvalidOperationException
异常
2.2. 单例作为常量注册
如果您想自己实例化一个单例, 你可以这样做:
services.AddSingleton<IDataService>(new DataService());
它允许一个非常有趣的场景, 假设 DataService 实现两个接口如果我们这样做:
- services.AddSingleton<IDataService, DataService>();
- services.AddSingleton<ISomeInterface, DataService>();
我们得到两个实例, 两个接口都有一个如果我们打算共享一个实例, 这是一种方法:
- var dataService = new DataService();
- services.AddSingleton<IDataService>(dataService);
- services.AddSingleton<ISomeInterface>(dataService);
如果组件具有依赖关系, 则可以从服务集合构建服务提供者并从中获取必要的依赖项:
- IServiceProvider provider = services.BuildServiceProvider();
- IOtherService otherService = provider.GetRequiredService<IOtherService>();
- var dataService = new DataService(otherService);
- services.AddSingleton<IDataService>(dataService);
- services.AddSingleton<ISomeInterface>(dataService);
请注意, 您应该在 ConfigureServices 的末尾执行此操作, 以便在此之前确保已经注册了所有依赖项
3. 注入
我们已经注册了我们的组件, 现在我们就可以实际使用它们了
在 ASP.NET Core 中注入组件的典型方式是构造函数注入, 针对不同的场景确实存在其他选项, 但构造器注入允许您定义在没有这些其他组件的情况下此组件不起作用
举个例子, 我们来做一个基本的日志记录中间件组件:
- public class LoggingMiddleware
- {
- private readonly RequestDelegate _next;
- public LoggingMiddleware(RequestDelegate next)
- {
- _next = next;
- }
- public async Task Invoke(HttpContext ctx)
- {
- Debug.WriteLine("Request starting");
- await _next(ctx);
- Debug.WriteLine("Request complete");
- }
- }
在中间件中注入组件有三种不同的方式:
构造函数
Invoke 方法参数
HttpContext.RequestServices
让我们使用三种全部方式注入我们的组件:
- public class LoggingMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly IDataService _svc;
- public LoggingMiddleware(RequestDelegate next, IDataService svc)
- {
- _next = next;
- _svc = svc;
- }
- public async Task Invoke(HttpContext ctx, IDataService svc2)
- {
- IDataService svc3 = ctx.RequestServices.GetService<IDataService>();
- Debug.WriteLine("Request starting");
- await _next(ctx);
- Debug.WriteLine("Request complete");
- }
- }
中间件在应用的整个生命周期中仅实例化一次, 因此通过构造函数注入的组件对于所有通过的请求都是相同的
作为 Invoke 方法的参数注入的组件是中间件绝对必需的, 如果它找不到要注入的 IDataService, 它将引发
InvalidOperationException
异常
The third one uses the RequestServices property on the HttpContext to get an optional dependency using GetService().
第三个通过使用 HttpContext 请求上下文的 RequestServices 属性的 GetService<T>()方法来获取可选的依赖项 RequestServices 属性的类型是 IServiceProvider, 因此它与实现工厂中的提供者完全相同如果您打算要求拿到这个组件, 可以使用
GetRequiredService<T>()
如果 IDataService 被注册为 Singleton, 我们会在它们中获得相同的实例
如果它被注册为 Scoped,svc2 和 svc3 将会是同一个实例, 但不同的请求会得到不同的实例
在 Transient 的情况下, 它们都是不同的实例
每种方法的用例:
构造函数: 所有请求都需要的单例 (Singleton) 组件
Invoke 参数: 在请求中总是必须的作用域 (Scoped) 和瞬时 (Transient) 组件
RequestServices: 基于运行时信息可能需要或可能不需要的组件
如果可能的话, 我会尽量避免使用 RequestServices, 并且只在中间件必须能够在缺少某些组件一样可以运行的情况下才使用它
3.1.Startup 类
在 Startup 类的构造函数中, 您至少可以注入
IHostingEnvironment
和 ILoggerFactory 它们是官方文档中提到的仅有两个接口可能有其他的, 但我不知道
- public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
- {
- ...
- }
- IHostingEnvironment
通常用于为应用程序设置配置您可以使用 ILoggerFactory 设置日志记录
Configure 方法允许您注入已注册的任何组件
- public void Configure(
- IApplicationBuilder app,
- IHostingEnvironment env,
- ILoggerFactory loggerFactory,
- IDataService dataSvc)
- {
- ...
- }
因此, 如果在管道配置过程中有需要的组件, 您可以在这里简单地要求它们
如果使用 app.Run()/app.Use()/app.UseWhen()/app.Map()在管道上注册简单中间件, 则不能使用构造函数注入事实上, 通过
ApplicationServices
/ RequestServices 是获取所需组件的唯一方法
这里有些例子:
- IDataService dataSvc2 = app.ApplicationServices.GetService<IDataService>();
- app.Use((ctx, next) =>
- {
- IDataService svc = ctx.RequestServices.GetService<IDataService>();
- return next();
- });
- app.Map("/test", subApp =>
- {
- IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
- subApp.Run((context =>
- {
- IDataService svc2 = context.RequestServices.GetService<IDataService>();
- return context.Response.WriteAsync("Hello!");
- }));
- });
- app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/test2"), subApp =>
- {
- IDataService svc1 = subApp.ApplicationServices.GetService<IDataService>();
- subApp.Run(ctx =>
- {
- IDataService svc2 = ctx.RequestServices.GetService<IDataService>();
- return ctx.Response.WriteAsync("Hello!");
- });
- });
因此, 您可以在配置时通过
IApplicationBuilder
上的
ApplicationServices
请求组件, 并在请求时通过 HttpContext 上的 RequestServices 请求组件
3.2. 在 MVC Core 中注入
在 MVC 中进行依赖注入的最常见方法是构造函数注入
您可以在任何地方做到这一点在控制器中, 您有几个选项:
- public class HomeController : Controller
- {
- private readonly IDataService _dataService;
- public HomeController(IDataService dataService)
- {
- _dataService = dataService;
- }
- [HttpGet]
- public IActionResult Index([FromServices] IDataService dataService2)
- {
- IDataService dataService3 = HttpContext.RequestServices.GetService<IDataService>();
- return View();
- }
- }
如果您希望稍后根据运行时决策获取依赖项, 则可以再次使用 Controller 基类 (技术上讲, ControllerBase 最好) 的 HttpContext 属性上可用的 RequestServices
您也可以通过在特定的 Action 上添加参数, 并使用
FromServicesAttribute
特性对其进行装饰来注入所需的服务, 这会指示 MVC Core 从服务集合中获取它, 而不是尝试对其进行模型绑定
Razor 视图
您还可以使用新的关键字 @inject 在 Razor 视图中注入组件:
- @using Microsoft.AspNetCore.Mvc.Localization
- @inject IViewLocalizer Localizer
在这里, 我们在
_ViewImports.cshtml
中注入了一个视图本地化器, 因此我们将它作为 Localizer 在所有视图中提供
请注意, 不应滥用此机制将本应该来自控制器的数据带入视图
Tag helper
构造函数注入也适用于 Tag Helper:
- [HtmlTargetElement("test")]
- public class TestTagHelper : TagHelper
- {
- private readonly IDataService _dataService;
- public TestTagHelper(IDataService dataService)
- {
- _dataService = dataService;
- }
- }
视图组件
视图组件也一样:
- public class TestViewComponent : ViewComponent
- {
- private readonly IDataService _dataService;
- public TestViewComponent(IDataService dataService)
- {
- _dataService = dataService;
- }
- public async Task<IViewComponentResult> InvokeAsync()
- {
- return View();
- }
- }
在视图组件中也可以获得 HttpContext, 因此有权访问 RequestServices
过滤器
MVC 过滤器也支持构造函数注入, 以及有权访问 RequestServices:
- public class TestActionFilter : ActionFilterAttribute
- {
- private readonly IDataService _dataService;
- public TestActionFilter(IDataService dataService)
- {
- _dataService = dataService;
- }
- public override void OnActionExecuting(ActionExecutingContext context)
- {
- Debug.WriteLine("OnActionExecuting");
- }
- public override void OnActionExecuted(ActionExecutedContext context)
- {
- Debug.WriteLine("OnActionExecuted");
- }
- }
但是, 通过构造函数注入我们不能像往常一样在控制器上添加特性, 因为它在运行的时候必须要获得依赖项
这里我们有两种方式可以将其添加到控制器或 Action 级别:
- [TypeFilter(typeof(TestActionFilter))]
- public class HomeController : Controller
- {
- }
- // or
- [ServiceFilter(typeof(TestActionFilter))]
- public class HomeController : Controller
- {
- }
以上这两种方式关键的区别是
TypeFilterAttribute
会先找出过滤器的依赖项并通过 DI 获取它们, 然后创建过滤器另一方面,
ServiceFilterAttribute
则是直接尝试从服务集合中寻找过滤器!
所以, 为了使
[ServiceFilter(typeof(TestActionFilter))]
正常工作, 我们需要多一点配置:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddTransient<TestActionFilter>();
- }
现在
ServiceFilterAttribute
就可以找到过滤器了
如果您想添加全局过滤器:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc(mvc =>
- {
- mvc.Filters.Add(typeof(TestActionFilter));
- });
- }
这样就不需要将过滤器添加到服务集合, 它的工作方式就好像您已经在每个控制器上添加了
TypeFilterAttribute
一样
HttpContext
我已经多次提到过 HttpContext 如果您想访问控制器 / 视图 / 视图组件之外的 HttpContext, 那怎么办? 例如, 要访问当前登录用户的声明?
您只要简单地注入
IHttpContextAccessor
, 如下所示:
- public class DataService : IDataService
- {
- private readonly HttpContext _httpContext;
- public DataService(IOtherService svc, IHttpContextAccessor contextAccessor)
- {
- _httpContext = contextAccessor.HttpContext;
- }
- //...
- }
这样可以让您的服务层直接访问 HttpContext, 而不需要通过调用方法来传递它
4. 结论
相对于 Ninject 或 Autofac 等较大较老的 DI 框架来说, ASP.NET Core 提供的依赖注入容器在功能上比较基本, 但它仍然非常适合大多数需求
您可以在任何需要的地方注入组件, 从而使组件在此过程中更具可测试性
来源: https://www.cnblogs.com/esofar/p/8625619.html