依赖注入是 ASP.NET Core 里的核心概念之一,咱们日常老是愉快地在Startup
类的ConfigureServices
方法里往IServiceCollection
里注册各类类型,以至有一些同窗可能误觉得依赖注入是只有 ASP.NET Core 才有的特性。但实际上依赖注入也能够用于 .NET Core 的 Console app. 别忘了, ASP.NET Core 的应用本质上也只是一个 Console app而已。今天咱们在Console app里试试依赖注入。git
咱们的目标是建立一个Console app,在其中引入依赖注入,注册不一样生命周期的类型,而后建立几个线程,每一个线程分别依靠依赖注入“建立”若干类型实例,而后观察不一样生命周期下这些实例变量是否指向一个实例仍是各不相同。github
如今闭上眼睛想象一下(别睡着了),咱们本身就是依赖注入的执行者,若是有一个漂亮的程序媛跟咱们说她要某某类型的一个实例,咱们应该怎么作?咱们首先须要知道这某某类型是个什么东西以及如何建立对吧?咱们如何知道呢?固然是她得提早告诉咱们啊,而咱们要有个地方把这些信息保留下来而后在须要的时候能够查阅。在 .NET Core里,能够依赖注入的类型叫Service
,而记录这些Service
信息的这地方就是ServiceCollection
。面试
因此,当程序运行起来以后,咱们第一件事情就是建立一个ServiceCollection
,怎么建立呢? new
呗json
// using Microsoft.Extensions.DependencyInjection ServiceCollection services = new ServiceCollection();
听起来高大上的ServiceCollection
,其建立居然如此简单。😓浏览器
看着 ServiceCollection
里眼花缭乱的各类类型,咱们心中充满自信,“妹子,说吧,你想要哪一个类型的实例?”,妹子一脸不乐意“要你个头,我两手空空拿什么去取类型的实例?”……对啊,咱们总得给人家一个什么东西,而后人家能够用它从ServiceCollection
里获取实例啊。。。这东西就是IServiceProvider
,咱们的ServiceCollection
能够生成一个IServiceProvider
,而任何类型的对象,只要有这个IServiceProvider
就能够从咱们的ServiceCollection
里获取实例。安全
ServiceCollection services = new ServiceCollection(); // 向services注册各类类型 IServiceProvider sp = services.BuildServiceProvider(); //今后之后,任何握有 sp 的对象能够从ServiceCollection里获取实例。
有趣的是, IServiceProvider
是System
命名空间下的。多线程
自脱离 ASP.NET Web Form 的世界以来,已经不多听到、看到“生命周期”这个词了。遥想当年不管是面试仍是被面试,“ASP.NET 页面的生命周期”那简直是必备问题 —— 跑题了。app
仍是闭上眼睛(仍是别睡着了),想象一下,仍是那个漂亮的程序媛,她略带娇嗔地对咱们说:“好哥哥,帮我把这个某某类型注册到依赖注入里吧,能够吗?”,既然咱们如今有了ServiceCollection
,注册固然不成问题~,但再仔细想一想,当咱们把某某类型添加到ServiceCollection
,继而建立出一个IServiceProvider
给程序媛妹子,接着程序媛妹子不停地从ServiceCollection
里获取实例时,她获得的是同一个实例呢仍是每次请求都给她一个新的实例?谁知道,得问她才知道。因此日常不擅言辞、从不废话的咱们不能浪费此次交流的机会,在程序媛妹子让咱们注册类型的时候咱们还要问清楚她想怎样获得这个类型的实例,每次都给她一个新的,仍是总给她同一个?换句话说,当一个Service
被注册到ServiceCollection
的时候,咱们须要同时知道它的类型和实例生命周期。ide
ServiceCollection
很体贴,咱们能够直接用不一样的注册方法注册不一样生命周期的Service
:ui
// AddTransient 方法将一个类型注册为 Transient 生命周期。所以,每一次你从依赖注入中请求一个 MyTransientClass 实例,你都会获得一个全新实例化的实例。请求10次,就获得10个不一样的实例。 service.AddTransient<MyTransientClass>(); // AddSingleton 方法将一个类型注册为 Singleton 生命周期。单体你们都懂,就是不管请求多少次,你从依赖注入都会获得同一个 MySingletonClass 实例。请求10次,获得的倒是同一个实例。 service.AddSingleton<MySingletonClass>(); // AddScoped 方法将一个类型注册为 Scoped 生命周期。这个生命周期比较特别。若是你的程序里建立了若干个 "Scope",依赖注入会确保在同一个 Scope 里,你将获得同一个 MyScopedClass 实例,而不一样 Scope 里的 MyScopedClass 实例是不一样的 // 假设你有3个Scope,每一个Scope里请求10次,那么你将获得3个不一样的 MyScopedClass 实例。其中每一个 Scope 里一个。 // 至于 Scope 究竟是什么含义,这就因程序而异了。好比在 ASP.NET Core 里,一个Scope意味着一次浏览器请求往返。而在咱们的示例程序里,一个Scope表明一个线程内部。 service.AddScoped<MyScopedClass>();
以上3个生命周期类型基本上涵盖了全部可能的场景:
说书者曰“闲话休提,且说正话”,我们也到了“理论休提,且看Demo”的时候了。 .NET Core 的一大优势是命令行友好,而且不用特别依靠功能强大但臃肿的 Visual Studio来开发。个人Demo是在 MacOS + .NET Core CLI (v1.1) + Visual Studio Code 环境下建立和运行的。这套环境在其它平台下的体验几乎没什么区别。
首先打开一个命令行,建立一个目录,而后在新建立的目录里执行 dotnet new
命令。这将建立一个最简单的 Console App.
注意,利用 dotnet new
建立的文件居然加了可执行属性(因此显示为红色),这应该是个bug,而且会在将来的版本里修复。最后运行 code .
会把当前目录在 Visual Studio Code 里打开,而后咱们就能够写代码了。
首先,咱们须要添加一个引用:Microsoft.Extensions.DependencyInjection
,依赖注入的默认实现都在里面。
打开 project.json
,而后在dependencies
里添加引用。添加完成以后,project.json
应该看起来是这样的:
{ "version": "1.0.0-*", "buildOptions": { "debugType": "portable", "emitEntryPoint": true }, "dependencies": { "Microsoft.Extensions.DependencyInjection": "1.1.0" }, "frameworks": { "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.1.0" } }, "imports": "dnxcore50" } } }
添加好引用以后,保存,这时 VS Code的顶部应该会有个提示,说"There are unresolved dependencies from 'project.json'. Please execute the restore command to continue.",你能够直接点“Restore”按钮或者手工在命令行里运行 dotnet restore
命令来还原依赖。
若是你观察新建立的 ASP.NET Core 程序的 project.json 文件,你可能会发现依赖列表里并无Microsoft.Extensions.DependencyInjection
,那为何咱们在这里须要添加这个引用呢?这是由于你的 project.json 文件里有 ASP.NET Core 的引用,好比 Microsoft.AspNetCore.Mvc
,而它或者它依赖的引用里有对Microsoft.Extensions.DependencyInjection
的引用。所以你的 ASP.NET Core程序实际上是间接的引用了Microsoft.Extensions.DependencyInjection
。咱们的示例程序里“干净”得很,因此必须直接添加对Microsoft.Extensions.DependencyInjection
的引用。
注意,我是使用 1.1 版本的 .NET Core,因此引用的版本号都是“1.1.0”,若是你使用的是1.0.0或1.0.1版本的 .NET Core,那么这里的版本号会有所不一样。
在咱们的示例程序引入依赖注入以前,有几项准备工做要作。
首先, 咱们须要一个能够注册到依赖注入的类型,这个简单:
public class MyClass { }
其次, 咱们须要某种方法来检测从依赖注入中获得的类型实例是相同的仍是不一样的。什么叫相同?就是这些实例都指向内存里的同一个对象。对此,咱们能够利用Object
类型的静态方法ReferenceEquals
来检测。顾名思义,无需解释。可是这个方法自己只能针对2个实例进行检测,咱们的示例程序想一次获得10个实例引用,怎么检测这10个实例引用是相同仍是不一样?记得写SQL语句的时候,有个关键字叫Distinct
,它能够剔除集合中的重复项。而咱们引觉得傲的LINQ一样支持Distinct
,咱们能够把全部实例放到一个集合,而后对集合进行Distinct
操做,若是结果是1
,说明集合里全部的实例其实指向同一个对象;若是结果等于集合本来的元素个数,那说明集合里每个对象都是互不相同的。
鉴于咱们使用多个线程向集合里插入数据,咱们须要一个多线程安全的集合类型:System.Collections.Concurrent.ConcurrentBag<T>
。
而调用Distinct
方法的时候,咱们但愿它能够明确地以ReferenceEquals
的方式比较,这一点能够经过建立一个实现IEqualityComparer<T>
接口的类ReferenceEqualComparer<T>
来作到。
public class ReferenceEqualComparer<T> : IEqualityComparer<T> { public bool Equals(T x, T y) { return Object.ReferenceEquals(x, y); } public int GetHashCode(T obj) { return obj.GetHashCode(); } }
而后, 咱们建立两个IEnumerable<T>
上的扩展方法来简化比较操做。
public static class IEnumerableExtensions { public static bool AreIdentical<T>(this IEnumerable<T> bag) { return bag.Distinct(new ReferenceEqualComparer<T>()).Count() == 1; } public static bool AreDifferent<T>(this IEnumerable<T> bag) { return bag.Distinct(new ReferenceEqualComparer<T>()).Count() == bag.Count(); } }
最后, 咱们建立一个统一的方法,这个方法能够传入一个ServiceCollection
对象,而后咱们从中获取IServiceProvider
,再建立10个线程分别利用IServiceProvider
获取服务实例,插入到一个集合中并返回这个集合。
public static ConcurrentBag<MyClass> GetObjectsFromDI(ServiceCollection services) { int threadCount = 10; IServiceProvider sp = services.BuildServiceProvider(); ConcurrentBag<MyClass> bag = new ConcurrentBag<MyClass>(); Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { Thread thread = new Thread(RunPerThread); threads[i] = thread; thread.Start(new Tuple<IServiceProvider, ConcurrentBag<MyClass>>(sp, bag)); } // 确保全部线程都执行完毕以后再继续 for (int i = 0; i < threadCount; i++) { threads[i].Join(); } return bag; } public static void RunPerThread(object threadParam) { Tuple<IServiceProvider, ConcurrentBag<MyClass>> args = threadParam as Tuple<IServiceProvider, ConcurrentBag<MyClass>>; IServiceProvider sp = args.Item1; ConcurrentBag<MyClass> bag = args.Item2; for (int i = 0; i < 10; i++) { bag.Add(sp.GetRequiredService<MyClass>()); } }
以上的准备工做使咱们接下来的验证操做变得容易了不少。
咱们建立一个方法TryOutSingleton
来验证 Singleton 生命周期
private static void TryOutSingleton() { ServiceCollection services = new ServiceCollection(); // 准备好咱们的容器 services.AddSingleton<MyClass>(); //把MyClass注册为 Singleton 生命周期 ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); // 调用咱们准备好的方法,用若干线程从 IServiceProvider 中获取 MyClass 实例,并加入到集合 Console.WriteLine(bag.AreIdentical()); // 验证集合中的全部元素是否指向内存中的同一个对象。 }
不出所料,最后输出的结果是:
True
再建立一个 TryOutTransient
方法验证 Transient 生命周期
private static void TryOutTransient() { ServiceCollection services = new ServiceCollection(); // 准备好咱们的容器 services.AddTransient<MyClass>(); //把MyClass注册为 Transient 生命周期 ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); // 调用咱们准备好的方法,用若干线程从 IServiceProvider 中获取 MyClass 实例,并加入到集合 Console.WriteLine(bag.AreDifferent()); // 验证集合中的全部元素是否各不相同 }
一样不出意外,输出结果是:
True
前面提到过, Scoped 生命周期比较特别,同一个Scope里的实例是同一个,可是不一样Scope里的实例是不一样的。而Scope具体的含义取决于咱们本身的定义。
具体到代码级别,当咱们须要建立一个Scope的时候,咱们须要用到咱们以前获得的IServiceProvider
,它有一个CreateScope
方法能够建立一个类型为Microsoft.Extensions.DependencyInjection.IServiceScope
的Scope,而这个Scope实例有一个IServiceProvider
类型的属性ServiceProvider
!自此,咱们应该使用这个来自IServiceScope
的IServiceProvider
(取代以前咱们获得的IServiceProvider
)来获取服务实例,它会正确处理Singleton, Transient以及Scoped这3种生命周期!
ServiceCollection services = new ServiceCollection(); // ... IServiceProvider serviceProvider = services.BuildServiceProvider(); IServiceScope scope = serviceProvider.CreateScope(); IServiceProvider newServiceProvider = scope.ServiceProvider; // 之后靠它来正确处理 Singleton, Transient 和 Scoped 生命周期的实例 MyClass obj = newServiceProvider.GetRequiredService<MyClass>(); // 不管MyClass是哪一种生命周期类型,这里均可以获得正确的实例。
为了验证Scoped生命周期,咱们如今定义Scope为线程空间。也就是说,每个线程为一个Scope,对于Scoped生命周期的类型,在同一个线程以内获取的实例应该是同一个,可是不一样线程获取的实例是不一样的。
在演示代码中,咱们注册3个不一样的类型,分别对应3种不一样的生命周期,看看来自IServiceScope
的IServiceProvider
可否正确处理每一种生命周期类型。
代码有些啰嗦,由于不想再拆分红更小的方法了:
/* public class MySingleton { } public class MyTransient { } public class MyScoped { } */ private static void TryOutScoped() { Console.WriteLine($"RUNNING {nameof(TryOutScoped)}"); ServiceCollection services = new ServiceCollection(); services.AddSingleton<MySingleton>(); services.AddTransient<MyTransient>(); services.AddScoped<MyScoped>(); IServiceProvider sp = services.BuildServiceProvider(); // 线程1执行 ConcurrentBag<MySingleton> thread1SingletonBag = new ConcurrentBag<MySingleton>(); ConcurrentBag<MyTransient> thread1TransientBag = new ConcurrentBag<MyTransient>(); ConcurrentBag<MyScoped> thread1ScopedBag = new ConcurrentBag<MyScoped>(); Thread thread1 = new Thread(RunPerThreadWithScopedLifetime); thread1.Start(new Tuple<IServiceProvider, ConcurrentBag<MySingleton>, ConcurrentBag<MyTransient>, ConcurrentBag<MyScoped>>(sp, thread1SingletonBag, thread1TransientBag, thread1ScopedBag)); // 线程2执行 ConcurrentBag<MySingleton> thread2SingletonBag = new ConcurrentBag<MySingleton>(); ConcurrentBag<MyTransient> thread2TransientBag = new ConcurrentBag<MyTransient>(); ConcurrentBag<MyScoped> thread2ScopedBag = new ConcurrentBag<MyScoped>(); Thread thread2 = new Thread(RunPerThreadWithScopedLifetime); thread2.Start(new Tuple<IServiceProvider, ConcurrentBag<MySingleton>, ConcurrentBag<MyTransient>, ConcurrentBag<MyScoped>>(sp, thread2SingletonBag, thread2TransientBag, thread2ScopedBag)); // 等待执行完毕 thread1.Join(); thread2.Join(); // 验证全部 MySingleton 的实例都指向内存里同一个对象 IEnumerable<MySingleton> singletons = thread1SingletonBag.Concat(thread2SingletonBag); Console.WriteLine($"Singleton: {singletons.Count()} objects are IDENTICAL? {singletons.AreIdentical()}"); // 验证全部 MyTransient 的实例都各不相同 IEnumerable<MyTransient> transients = thread1TransientBag.Concat(thread2TransientBag); Console.WriteLine($"Transient: {transients.Count()} objects are DIFFERENT? {transients.AreDifferent()}"); // 对于Scoped生命周期,每一个线程集合内的实例应该指向内存里同一个对象,而2个线程集合里的实例应该是不一样的。 Console.WriteLine($"collection of thread 1 has {thread1ScopedBag.Count} objects and they are IDENTICAL: {thread1ScopedBag.AreIdentical()}"); Console.WriteLine($"collection of thread 2 has {thread2ScopedBag.Count} objects and they are IDENTICAL: {thread2ScopedBag.AreIdentical()}"); Console.WriteLine($"the first object from thread 1 and the first object from thread 2 are IDENTICAL: {Object.ReferenceEquals(thread1ScopedBag.First(), thread2ScopedBag.First())}"); }
输出结果为:
RUNNING TryOutScoped Singleton: 20 objects are IDENTICAL? True Transient: 20 objects are DIFFERENT? True collection of thread 1 has 10 objects and they are IDENTICAL: True collection of thread 2 has 10 objects and they are IDENTICAL: True the first object from thread 1 and the first object from thread 2 are IDENTICAL: False
演示代码能够从Github上获取。