[译]ASP.NET Core依赖注入深入讨论

简介: 原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog这篇文章我们来深入探讨ASP.NET Core、MVC Core中的依赖注入,我们将示范几乎所有可能的操作把依赖项注入到组件中。

原文链接:ASP.NET Core Dependency Injection Deep Dive - Joonas W's blog

这篇文章我们来深入探讨ASP.NET Core、MVC Core中的依赖注入,我们将示范几乎所有可能的操作把依赖项注入到组件中。

依赖注入是ASP.NET Core的核心,它能让您应用程序中的组件增强可测试性,还使您的组件只依赖于能够提供所需服务的某些组件。

举个例子,这里我们有一个接口和它的实现类:

public interface IDataService
{
    IList<DataClass> GetAll();
}

public class DataService : IDataService
{
    public IList<DataClass> GetAll()
    {
        //Get data...
        return data;
    }
}

如果另一个服务依赖于DataService,那么它们依赖于特定的实现,测试这样的服务可能会非常困难。如果该服务依赖于IDataService,那么它们只关心接口提供的契约。实现什么并不重要,它使我们能够通过一个模拟实现来测试服务的行为。

服务生命周期

在我们讨论如何在实践中进行注入之前,了解什么是服务生命周期至关重要。当一个组件通过依赖注入请求另一个组件时,它所接收的实例是否对该组件的实例来说是唯一的,这取决于它的生命周期。设置生命周期从而决定组件实例化的次数,以及组件是否共享。

在ASP.NET Core中,内置的DI容器有三种模式:

  • Singleton
  • Scoped
  • Transient

Singleton意味着只会创建一个实例,该实例在需要它的所有组件之间共享。因此始终使用相同的实例。

Scoped意味着每个作用域创建一个实例。作用域是在对应用程序的每个请求上创建的,因此,任何注册为Scoped的组件每个请求都会创建一次。

Transient每次请求时都会创建瞬态组件,并且永远不会共享。

理解这一点非常重要,如果将组件A注册为单例,则它不能依赖于具有ScopedTransient生命周期的组件。总而言之:

组件不能依赖比自己的生命周期小的组件。

违反这条规则的后果显而易见,依赖的组件可能会在依赖项之前释放。

通常,您希望将组件(如应用程序范围的配置容器)注册为Singleton。数据库访问类(如Entity Framework上下文)建议使用Scoped,以便可以重复使用连接。但是如果您想并行运行任何东西,请记住Entity Framework上下文不能由两个线程共享。如果您需要这样做,最好将上下文注册为Transient,这样每个组件都有自己的上下文实例而且可以并行运行。

服务注册

注册服务是在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>();

但是呢,当然组件必须取决于具体的类型,所以这可能是不需要的。

实现工厂

在一些特殊情况下,您可能想要接管某些服务的实例化。在这种情况下,您可以在服务描述符上注册一个实现工厂(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类型服务,则返回nullGetRequiredService<T>()如果找不到它,则会引发InvalidOperationException异常。

单例作为常量注册

如果您想自己实例化一个单例,你可以这样做:

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的末尾执行此操作,以便在此之前确保已经注册了所有依赖项。

注入

我们已经注册了我们的组件,现在我们就可以实际使用它们了。

在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异常。

第三个通过使用HttpContext请求上下文的RequestServices属性的GetService<T>()方法来获取可选的依赖项。RequestServices属性的类型是IServiceProvider,因此它与实现工厂中的提供者完全相同。如果您打算要求拿到这个组件,可以使用GetRequiredService<T>()

如果IDataService被注册为Singleton,我们会在它们中获得相同的实例。

如果它被注册为Scopedsvc2svc3将会是同一个实例,但不同的请求会得到不同的实例。

Transient的情况下,它们都是不同的实例。

每种方法的用例:

  • 构造函数:所有请求都需要的单例(Singleton)组件
  • Invoke参数:在请求中总是必须的作用域(Scoped)和瞬时(Transient)组件
  • RequestServices:基于运行时信息可能需要或可能不需要的组件

如果可能的话,我会尽量避免使用RequestServices,并且只在中间件必须能够在缺少某些组件一样可以运行的情况下才使用它。

Startup类

Startup类的构造函数中,您至少可以注入IHostingEnvironmentILoggerFactory。它们是官方文档中提到的仅有两个接口。可能有其他的,但我不知道。

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请求组件。

在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,而不需要通过调用方法来传递它。

结论

相对于Ninject或Autofac等较大、较老的DI框架来说,ASP.NET Core提供的依赖注入容器在功能上比较基本,但它仍然非常适合大多数需求。

您可以在任何需要的地方注入组件,从而使组件在此过程中更具可测试性。

链接

目录
相关文章
|
17天前
|
消息中间件 前端开发 小程序
一个基于.NET Core构建的简单、跨平台、模块化的商城系统
今天大姚给大家分享一个基于.NET Core构建的简单、跨平台、模块化、完全开源免费(MIT License)的商城系统:Module Shop。
|
17天前
|
算法 C# 数据库
【干货】一份10万字免费的C#/.NET/.NET Core面试宝典
C#/.NET/.NET Core相关技术常见面试题汇总,不仅仅为了面试而学习,更多的是查漏补缺、扩充知识面和大家共同学习进步。该知识库主要由自己平时学习实践总结、网上优秀文章资料收集(这一部分会标注来源)和社区小伙伴提供三部分组成。该份基础面试宝典完全免费,发布两年来收获了广大.NET小伙伴的好评,我会持续更新和改进,欢迎关注我的公众号【追逐时光者】第一时间获取最新更新的面试题内容。
|
3天前
|
存储 JSON 算法
net core jwt的基本原理和实现
这篇文章介绍了.NET Core中JWT(JSON Web Token)的基本原理和实现。JWT是一种用于安全传输信息的开放标准,由头部、负载和签名三部分组成。在.NET Core中实现JWT,需要安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,然后在`Startup.cs`配置JWT认证服务,包括设置密钥和验证参数。生成JWT令牌后,客户端存储并将其包含在请求头中发送给服务器进行验证和授权。JWT提供了一种无需服务器存储会话数据的安全身份验证和授权机制。
|
17天前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
29 0
|
17天前
|
Linux API iOS开发
.net core 优势
.NET Core 的优势:跨平台兼容(Windows, macOS, Linux)及容器支持,高性能,支持并行版本控制,丰富的新增API,以及开源。
30 4
|
17天前
|
开发框架 人工智能 .NET
C#/.NET/.NET Core拾遗补漏合集(持续更新)
在这个快速发展的技术世界中,时常会有一些重要的知识点、信息或细节被忽略或遗漏。《C#/.NET/.NET Core拾遗补漏》专栏我们将探讨一些可能被忽略或遗漏的重要知识点、信息或细节,以帮助大家更全面地了解这些技术栈的特性和发展方向。
|
17天前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
75 0
|
17天前
|
开发框架 前端开发 .NET
进入ASP .net mvc的世界
进入ASP .net mvc的世界
34 0
|
17天前
|
开发框架 前端开发 .NET
C# .NET面试系列六:ASP.NET MVC
<h2>ASP.NET MVC #### 1. MVC 中的 TempData\ViewBag\ViewData 区别? 在ASP.NET MVC中,TempData、ViewBag 和 ViewData 都是用于在控制器和视图之间传递数据的机制,但它们有一些区别。 <b>TempData:</b> 1、生命周期 ```c# TempData 的生命周期是短暂的,数据只在当前请求和下一次请求之间有效。一旦数据被读取,它就会被标记为已读,下一次请求时就会被清除。 ``` 2、用途 ```c# 主要用于在两个动作之间传递数据,例如在一个动作中设置 TempData,然后在重定向到另
128 5
|
10月前
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
127 0