ASP.NET Core 依赖注入

ASP.Net Core 的底层设计支持和使用依赖注入。APS.Net Core 应用程序可以利用内置的框架服务将他们注入到启动类的方法中,并且应用程序服务能够配置注入。由APS.Net Core 提供的默认服务容器提供了最小功能集,并不是要取代其他容器。依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术。
依赖注入(Dependency injection,DI)在.NET Core里面被提到了一个非常重要的位置。

一、什么是依赖注入(Denpendency Injection)

到底依赖注入是什么? 为什么要用它? 初学者特别容易对控制反转IOC(Iversion of Control),DI等概念搞晕。

1、首先我们得知道什么是依赖

当一个类需要另一个类协作来完成工作的时候就产生了依赖。比如我们在AccountController这个控制器需要完成和用户相关的注册、登录 等事情。其中的登录我们由EF结合Idnetity来完成,所以我们封装了一个EFLoginService。这里AccountController就有一个ILoginService的依赖。


image.png

这里有一个设计原则:依赖于抽象,而不是具体的实现。我们需要给EFLoginService定义了一个接口,抽象LoginService的行为。

2、什么是注入

注入体现的是一个IOC(控制反转的的思想)。在反转之前 ,我们先看看正转。

private ILoginService<ApplicationUser> _loginService;
public AccountController()
{
  _loginService = new EFLoginService()
}

大师说,这样不好。你不应该自己创建它,而是应该由你的调用者给你。于是你通过构造函数让外界把这两个依赖传给你。

private ILoginService<ApplicationUser> _loginService;
public AccountController(ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

把依赖的创建丢给其它人,自己只负责使用,其它人丢给你依赖的这个过程理解为注入。

3、为什么要反转?

为了在业务变化的时候尽少改动代码可能造成的问题。比如我们现在要把从EF中去验证登录改为从Redis去读,于是我们加了一个 RedisLoginService。这个时候我们只需要在原来注入的地方改一下就可以了。


image.png

因为EFLoginService和RedisLoginService都实现了接口ILoginService,所以我们直接在调用方传入RedisLoginService即可。(即将EFLoginService替换为RedisLoginService)

4、什么是容器?

上面我们在使用AccountController的时候,我们自己通过代码创建了一个ILoggingServce的实例。想象一下,一个系统中如果有100个这样的地方,我们是不是要在100个地方做这样的事情? 控制是反转了,依赖的创建也移交到了外部。现在的问题是依赖太多,我们需要一个地方统一管理系统中所有的依赖,所以容器诞生了。

容器负责两件事情:

  • 绑定服务与实例之间的关系
  • 获取实例,并对实例进行管理(创建与销毁)


    image.png

二、.NET Core DI

在.NET Core中DI的核心分为两个组件:IServiceCollection和 IServiceProvider。

  • IServiceCollection 负责注册
  • IServiceProvider 负责提供实例

通过默认的 ServiceCollection(在Microsoft.Extensions.DependencyInjection命名空间下)有三个方法:

var serviceCollection = new ServiceCollection()
  .AddTransient<ILoginService, EFLoginService>() //每一次GetService都会创建一个新的实例
  .AddSingleton<ILoginService, EFLoginService>() //在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
  .AddScoped<ILoginService, EFLoginService>(); //整个应用程序生命周期以内只创建一个实例 

这三个方法都是将我们的实例注册进去,只不过实例的生命周期不一样。
ServiceCollection的默认实现是提供一个ServiceDescriptor的List

public interface IServiceCollection : IList<ServiceDescriptor>{}

上面的AddTransient、AddSignletone和AddScoped方法是IServiceCollection的扩展方法, 都是往这个List里面添加ServiceDescriptor。

private static IServiceCollection Add(
  IServiceCollection collection,
  Type serviceType,
  Type implementationType,
  ServiceLifetime lifetime)
{
  var descriptor =
  new ServiceDescriptor(serviceType, implementationType, lifetime);
  collection.Add(descriptor);
  return collection;
}

实例的生命周期

Transient: 每一次GetService都会创建一个新的实例
Scoped: 在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
Singleton :整个应用程序生命周期以内只创建一个实例

对应了Microsoft.Extensions.DependencyInjection.ServiceLifetime的三个枚举值

public enum ServiceLifetime
{
  Singleton,
  Scoped,
  Transient
}

我们用一个例子来理解这个生命周期

我们定义一个接口基本的IOperation里面有一个 OperationId的属性,IOperationSingleton也是一样,只不过是另外一个接口。分别引用IOperation

public interface IOperation
{
        Guid OperationId { get; }
}
public interface IOperationSingleton : IOperation { }
public interface IOperationTransient : IOperation{}
public interface IOperationScoped : IOperation{}

创建一个Operation实现类,很简单,可以在构造函数中传入一个Guid进行赋值,如果没有的话则自已New一个 Guid。

public class Operation :  IOperationSingleton,  IOperationTransient,  IOperationScoped
{
    private Guid _guid; 
    public Operation() {
        _guid = Guid.NewGuid();
    } 
    public Operation(Guid guid)
    {
        _guid = guid;
    } 
    public Guid OperationId => _guid;
}

实例生命周期之Singleton

在程序内我们可以多次调用ServiceProvider的GetService方法,获取到的都是同一个实例。

var services = new ServiceCollection();
// 默认构造
services.AddSingleton<IOperationSingleton, Operation>();
// 自定义传入Guid空值
services.AddSingleton<IOperationSingleton>(new Operation(Guid.Empty));
// 自定义传入一个New的Guid
services.AddSingleton <IOperationSingleton>(new Operation(Guid.NewGuid())); 
var provider = services.BuildServiceProvider(); 
// 输出singletone1的Guid
var singletone1 = provider.GetService<IOperationSingleton>();
Console.WriteLine($"signletone1: {singletone1.OperationId}"); 
// 输出singletone2的Guid
var singletone2 = provider.GetService<IOperationSingleton>();
Console.WriteLine($"signletone2: {singletone2.OperationId}");
Console.WriteLine($"singletone1 == singletone2 >= { singletone1 == singletone2 }");

查看结果(我们对IOperationSingleton注册了三次,获取了两次,注意到我们获取到的结果始终都是我们最后一次注册的那个给了一个Guid的实例,前面的会被覆盖。)

signletone1: ca9fd35d-2bdc-45e8-acf3-90a8e5154d3e
signletone2: ca9fd35d-2bdc-45e8-acf3-90a8e5154d3e
signletone1 == signletone2 >= True

实例生命周期之Tranisent

var services = new ServiceCollection();
// 默认构造
services.AddSingleton<IOperationTransient , Operation>();
// 自定义传入Guid空值
services.AddSingleton<IOperationTransient>(new Operation(Guid.Empty));
// 自定义传入一个New的Guid
services.AddSingleton <IOperationTransient>(new Operation(Guid.NewGuid())); 
var provider = services.BuildServiceProvider(); 
// 输出singletone1的Guid
var tranisent1 = provider.GetService<IOperationTransient>();
Console.WriteLine($"tranisent1 : {tranisent1 .OperationId}"); 
// 输出singletone2的Guid
var tranisent2 = provider.GetService<IOperationTransient>();
Console.WriteLine($"tranisent2 : {tranisent2 .OperationId}");
Console.WriteLine($"tranisent1 == tranisent2 >= { tranisent1 == tranisent2 }");

查看结果

tranisent1 : 04c970c1-bd8c-427a-9ac6-ecd2f4249cc5
tranisent2 : 3a7050a8-9e11-4a4b-98da-7f0d6ec87ad9
tranisent1 == tranisent2 >= False

实例生命周期之Scoped

.NET Core IServiceProvider提供CreateScope产生一个新的ServiceProvider范围,在这个范围下的Scope标注的实例将只会是同一个实例。换句话来说:用Scope注册的对象,在同一个ServiceProvider的 Scope下相当于单例。

同样我们先分别注册IOperationScoped、IOperationTransient和IOperationSingletone 这三个实例,用对应的Scoped、Transient、和Singleton生命周期。

var services = new ServiceCollection()
.AddScoped<IOperationScoped, Operation>()
.AddTransient<IOperationTransient, Operation>()
.AddSingleton<IOperationSingleton, Operation>();

然后我们用ServiceProvider.CreateScope方法创建一个Scope

var provider = services.BuildServiceProvider();
using (var scope1 = provider.CreateScope())
{
    var p = scope1.ServiceProvider; 
    var scopeobj1 = p.GetService<IOperationScoped>();
    var transient1 = p.GetService<IOperationTransient>();
    var singleton1 = p.GetService<IOperationSingleton>(); 
    var scopeobj2 = p.GetService<IOperationScoped>();
    var transient2 = p.GetService<IOperationTransient>();
    var singleton2 = p.GetService<IOperationSingleton>(); 
    Console.WriteLine(
        $"scope1: { scopeobj1.OperationId }," +
        $"transient1: {transient1.OperationId}, " +
        $"singleton1: {singleton1.OperationId}");
 
    Console.WriteLine($"scope2: { scopeobj2.OperationId }, " +
        $"transient2: {transient2.OperationId}, " +
        $"singleton2: {singleton2.OperationId}");
}

查看结果

scope1: 200d1e63-d024-4cd3-88c9-35fdf5c00956, 
transient1: fb35f570-713e-43fc-854c-972eed2fae52, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 200d1e63-d024-4cd3-88c9-35fdf5c00956, 
transient2: 2766a1ee-766f-4116-8a48-3e569de54259, 
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

如果再创建一个新的Scope运行

scope1: 29f127a7-baf5-4ab0-b264-fcced11d0729, 
transient1: 035d8bfc-c516-44a7-94a5-3720bd39ce57, 
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225

scope2: 29f127a7-baf5-4ab0-b264-fcced11d0729, 
transient2: 74c37151-6497-4223-b558-a4ffc1897d57, 
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225

大家注意到上面我们一共得到了 4个Transient实例,2个Scope实例,1个Singleton实例。
这有什么用?有一些对象在一个请求跨越多个Action或者多个Service、Repository的时候,比如最常用的DBContext它可以是一个实例。即能减少实例初始化的消耗,还能实现跨Service事务的功能。(注:在ASP.NET Core中所有用到EF的Service 都需要注册成Scoped ),而实现这种功能的方法就是在整个reqeust请求的生命周期以内共用了一个Scope。

三、DI在ASP.NET Core中的应用

在Startup类中初始化

ASP.NET Core可以在Startup.cs的 ConfigureService中配置DI,大家看到 IServiceCollection这个参数应该就比较熟悉了。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ILoginService<ApplicationUser>,EFLoginService>();
    services.AddMvc();
)

ASP.NET Core的一些组件已经提供了一些实例的绑定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的扩展方法。

public static IMvcBuilder AddMvc(this IServiceCollection services)
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    } 
    var builder = services.AddMvcCore(); 
    builder.AddApiExplorer();
    builder.AddAuthorization();
    AddDefaultFrameworkParts(builder.PartManager);
    ...
}

在Controller中使用

一般可以通过构造函数或者属性来实现注入,但是官方推荐是通过构造函数。这也是所谓的显式依赖。

private ILoginService<ApplicationUser> _loginService;
public AccountController(ILoginService<ApplicationUser> loginService)
{
  _loginService = loginService;
}

我们只要在控制器的构造函数里面写了这个参数,ServiceProvider就会帮我们注入进来。这一步是在Mvc初始化控制器的时候完成的,我们后面再介绍到Mvc的时候会往细里讲。

在View中使用

在View中需要用@inject 再声明一下,起一个别名。

@using MilkStone.Services;
@model MilkStone.Models.AccountViewModel.LoginViewModel
@inject ILoginService<ApplicationUser>  loginService
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head></head>
<body>
  @loginService.GetUserName()
</body>
</html>

通过 HttpContext来获取实例

HttpContext下有一个RequestedService同样可以用来获取实例对象,不过这种方法一般不推荐。同时要注意GetService<>这是个范型方法,默认如果没有添加Microsoft.Extension.DependencyInjection的using,是不用调用这个方法的。

HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工...
    AI云栈阅读 1,092评论 0 10
  • 依赖注入   一个类需要调用另外一个类就是依赖,比如A类中需要调用B类才能完成功能,就是A类依赖B类。   通常完...
    GongZH丶阅读 2,769评论 0 3
  • 最近参加了熊猫书院的优势计划,该阅读课程根据每个人的测试结果定制书单。其实不是真的在读原书,而是阅读独家拆书概括出...
    顾小懒阅读 754评论 0 1
  • 8月28日 周二 晴 28-34℃ 人一旦下定决心做一件事情了吧,上天总会千方百计来考验你。 比如,早上拿着手机刷...
    方小延阅读 704评论 0 50
  • 冰冰,妈妈不知道你什么时候会看到这些话,但是妈妈希望你能真正的长大。世界上真心对待你的人永远就是那么几个,而越是真...
    叶子_简书作者阅读 159评论 0 0