ASP.NET Core依赖注入深入讨论

这篇文章我们来深入探讨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,那么它们只关心接口提供的契约。实现什么并不重要,它使我们能够通过一个模拟实现来测试服务的行为。

1.服务生命周期

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

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

  • Singleton

  • Scoped

  • Transient

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

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

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

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

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

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

通常,您希望将组件(如应用程序范围的配置容器)注册为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类型服务,则返回nullGetRequiredService<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异常。

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

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

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

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

每种方法的用例:

  • 构造函数:所有请求都需要的单例(Singleton)组件

  • Invoke参数:在请求中总是必须的作用域(Scoped)和瞬时(Transient)组件

  • RequestServices:基于运行时信息可能需要或可能不需要的组件

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

3.1.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()在管道上注册简单中间件,则不能使用构造函数注入。事实上,通过ApplicationServicesRequestServices是获取所需组件的唯一方法。

这里有些例子:

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提供的依赖注入容器在功能上比较基本,但它仍然非常适合大多数需求。

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

5.链接

  • 在 ASP.NET Core 依赖注入 | Microsoft Docs

  • 控制器中的依赖关系注入 | Microsoft Docs

  • 视图中的依赖关系注入 | Microsoft Docs


原文:http://www.cnblogs.com/esofar/p/8625619.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321741.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

15、java中的集合(2)

说一下单列集合&#xff0c;java中的单列集合的顶级接口是Collection&#xff0c;它有两个子接口&#xff1a;List、Set&#xff0c;本篇介绍一下List接口及其实现类的功能方法和基本实现原理。 List集合是有序集合&#xff0c;这里的有序并不是指存入List集合的元素会被自动排…

P2467-[SDOI2010]地精部落【dp】

正题 题目链接:https://www.luogu.org/problem/P2467 题目大意 求长度为nnn的波动序列的个数。 解题思路 我们先考虑第一个是上升的&#xff0c;然后乘2即可。 设fi,jf_{i,j}fi,j​表示填1∼i1\sim i1∼i个&#xff0c;最前面的是jjj的个数。然后我们只要是1∼i−j11\sim i…

双向广搜 8数码问题

转载自&#xff1a;http://blog.sina.com.cn/s/blog_8627bf080100ticx.html Eight 题目链接&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1043讲到双向广搜&#xff0c;那就不能不讲经典的八数码问题&#xff0c;有人说不做此题人生不完整 。所谓双向广搜&am…

使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

Hypermedia As The Engine Of Application State (HATEOAS)HATEOAS&#xff08;Hypermedia as the engine of application state&#xff09;是 REST 架构风格中最复杂的约束&#xff0c;也是构建成熟 REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约&…

U94222-循环往复【tarjan,DAGdp】

正题 题目链接:https://www.luogu.org/problem/U94222?contestId23574 题目大意 nnn个点若条有向边,求一条路径要求 经过一个酒店经过权值最大消费最小起点编号最小 按照顺序满足。 解题思路 将强连通分量缩成一个点&#xff0c;然后用gi,0/1g_{i,0/1}gi,0/1​到达第iii个…

16、java中的集合(3)

说一下双列集合&#xff0c;顶级接口是Map&#xff0c;实现类有HashMap、LinkedHashMap、TreeMap、HashTable等&#xff0c;使用键值对的格式存储数据&#xff0c;键不可以重复&#xff0c;值可以重复。接下来对实现类做一下详细介绍。 HashMap是最常用的Map集合&#xff0c;它…

搜索训练1 [8数码问题]

HDU1043、以及POJ1077上面都有这道题目&#xff0c;可以说是搜索里的非常经典的题目了。 poj上面的数据真的是弱&#xff0c;由于只有一组数据&#xff0c;简单bfs直接就可以过掉。 前前后后捣鼓了能有6个小时&#xff0c;才把这道题目在HDU上以4500ms的微弱优势通过。。。。…

【招聘(北京)】.NETCORE开发工程师(微服务方向)

组织&#xff1a;华汽集团北京研发中心位置&#xff1a;北京市朝阳区焦奥中心官网&#xff1a;www.sinoauto.com邮箱&#xff1a;taoxu.weisinoauto.com 项目&#xff1a;打造面向国内汽车后市场用户的一站式云服务平台&#xff08;华汽云&#xff09;&#xff0c;形态包括B2B、…

2017西安交大ACM小学期数据结构 [分块,区间修改,单点查询]

Problem A 发布时间: 2017年6月28日 09:29 最后更新: 2017年6月28日 13:03 时间限制: 1000ms 内存限制: 32M 描述 给定一个长度为n的序列a1, a2, ..., an给出q个操作, 操作分为两种 对于形如1xyz的操作, 将下标介于[x,y]的元素加上z, 满足1≤x≤y≤n, 1≤z≤105对于形如2…

17、java中的集合(4)

之前单列集合只说过了List系列的集合&#xff0c;接下来再说一下Set集合系列&#xff0c;Set集合是无序集合&#xff08;存取顺序不一致&#xff09;&#xff0c;不允许添加相同元素&#xff0c;Set的实现依赖于Map集合&#xff0c;可以将Set集合看作Map集合键的集合&#xff0…

U92904-画地为佬【二分,结论】

正题 题目链接:https://www.luogu.org/problem/U92904?contestId23574 题目大意 用mmm根长度为1的火柴求能够圈住的最多块的地。 解题思路 显然如果刚好能够围成一个正方形那么一定是最优的&#xff0c;那么我们先将能够围成的围成一个最大的正方形&#xff0c;然后剩下的在…

确保线程安全下使用Queue的Enqueue和Dequeue

场景是这样&#xff0c;假设有一台设备会触发类型为Alarm的告警信号&#xff0c;并把信号添加到一个Queue结构中&#xff0c;每隔一段时间这个Queue会被遍历检查&#xff0c;其中的每个Alarm都会调用一个相应的处理方法。问题在于&#xff0c;检查机制是基于多线程的&#xff0…

2017西安交大ACM小学期数据结构 [分块、二维矩阵]

Problem B 发布时间: 2017年6月28日 10:06 最后更新: 2017年6月28日 16:35 时间限制: 2000ms 内存限制: 32M 描述 给定一个nm的矩形, 其中第i行第j列的值为ai,j给出q个操作, 操作有两种 对于形如1x1y1x2y2z的操作, 将(x1,y1)-(x2,y2)这段矩形区域的所有元素加上z, 满足1≤…

18、java中的泛型

之前介绍集合时&#xff0c;可以看到有List<String>这样的写法&#xff0c;那么尖括号里的内容是什么呢&#xff1f;这是泛型&#xff0c;意思就是说声明的这个List集合只能存放String类型的元素。 泛型是什么&#xff1f; ‘泛’指一般、不深入&#xff0c;在这里可以认…

编写一个Java程序,其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)

编写一个Java程序&#xff0c;其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)。他们的行动如下: 厨师准备菜肴&#xff0c;每次准备一个。服务员等待菜肴准备好&#xff0c;然后将其送到顾客那里。顾客等待服务员送来菜看后才开始吃。所有三个角色应该循环进行…

U86650-群鸡乱舞【矩阵乘法】

正题 题目链接:https://www.luogu.org/problem/U86650?contestId23574 题目大意 第一年有nnn只鸡&#xff0c;每只大于等于两岁的鸡每年可以生一只&#xff0c;在ttt岁时不会生鸡而会暴毙。 现在给出每只鸡的年龄&#xff0c;求第mmm年鸡的总数量。 解题思路 用fif_{i}fi​…

2017西安交大ACM小学期数据结构 [线段树]

Problem B 发布时间: 2017年7月1日 02:08 最后更新: 2017年7月1日 02:10 时间限制: 1000ms 内存限制: 64M 描述 给定一个长度为n的序列a1, a2, ..., an, 满足这个序列是一个1~n的排列 如果一个序列满足: 将序列排序后, 任意两个相邻的元素的差为1, 那么就称这个序列为&qu…

19、java中枚举

枚举是什么&#xff1f; 枚举就是将一个有限集合中的所有元素列举出来&#xff0c;在java中使用可以使用enum关键字来声明一个枚举类。 为什么使用枚举&#xff1f; 之前当用到一些常量时&#xff0c;便临时声明一个&#xff0c;这样使得代码看起来很乱&#xff0c;这里一个…

Hangfire使用ApplicationInsigts监控

起因我司目前使用清真的ApplicationInsights来做程序级监控。&#xff08;ApplicationInsights相关文档: https://azure.microsoft.com/zh-cn/services/application-insights/ &#xff09;其实一切都蛮好的&#xff0c;但是我们基于Hangfire的Job系统却无法被Ai所监控到&#…

nssl1446-小智的旅行【dp】

正题 题目大意 求一条最大的权值严格上升的路径。 解题思路 将边权排序&#xff0c;然后从fxf_xfx​转移到fy1f_y1fy​1即可&#xff0c;要注意的是因为严格上升&#xff0c;所以此次转移用的fff不能是相同权值转移时转移的。 codecodecode #include<cstdio> #include…