依赖注入是 ASP.NET Core 里的核心概念之一,我们平常总是愉快地在Startup类的ConfigureServices方法里往IServiceCollection里注册各种类型,以致有一些同学可能误以为依赖注入是只有 ASP.NET Core 才有的特性。但实际上依赖注入也可以用于 .NET Core 的 Console app. 别忘了, ASP.NET Core 的应用本质上也只是一个 Console app而已。今天我们在Console app里试试依赖注入。
我们的目标是创建一个Console app,在其中引入依赖注入,注册不同生命周期的类型,然后创建几个线程,每个线程分别依靠依赖注入“创建”若干类型实例,然后观察不同生命周期下这些实例变量是否指向一个实例还是各不相同。
ServiceCollection 现在闭上眼睛想象一下(别睡着了),我们自己就是依赖注入的执行者,如果有一个漂亮的程序媛跟我们说她要某某类型的一个实例,我们应该怎么做?我们首先需要知道这某某类型是个什么东西以及如何创建对吧?我们如何知道呢?当然是她得提前告诉我们啊,而我们要有个地方把这些信息保留下来然后在需要的时候可以查阅。在 .NET Core里,可以依赖注入的类型叫Service,而记录这些Service信息的这地方就是ServiceCollection。
所以,当程序运行起来之后,我们第一件事情就是创建一个ServiceCollection,怎么创建呢? new 呗
1 2 ServiceCollection services = new ServiceCollection();
听起来高大上的ServiceCollection,其创建竟然如此简单。😓
IServiceProvider 看着 ServiceCollection里眼花缭乱的各种类型,我们心中充满自信,“妹子,说吧,你想要哪个类型的实例?”,妹子一脸不乐意“要你个头,我两手空空拿什么去取类型的实例?”……对啊,我们总得给人家一个什么东西,然后人家可以用它从ServiceCollection里获取实例啊。。。这东西就是IServiceProvider,我们的ServiceCollection可以生成一个IServiceProvider,而任何类型的对象,只要有这个IServiceProvider就可以从我们的ServiceCollection里获取实例。
1 2 3 4 5 6 7 ServiceCollection services = new ServiceCollection(); IServiceProvider sp = services.BuildServiceProvider();
有趣的是, IServiceProvider是System命名空间下的。
Service的生命周期 自脱离 ASP.NET Web Form 的世界以来,已经很少听到、看到“生命周期”这个词了。遥想当年无论是面试还是被面试,“ASP.NET 页面的生命周期”那简直是必备问题 —— 跑题了。
还是闭上眼睛(还是别睡着了),想象一下,还是那个漂亮的程序媛,她略带娇嗔地对我们说:“好哥哥,帮我把这个某某类型注册到依赖注入里吧,可以吗?”,既然我们现在有了ServiceCollection,注册当然不成问题~,但再仔细想想,当我们把某某类型添加到ServiceCollection,继而创建出一个IServiceProvider给程序媛妹子,接着程序媛妹子不停地从ServiceCollection里获取实例时,她得到的是同一个实例呢还是每次请求都给她一个新的实例?谁知道,得问她才知道。所以平常不擅言辞、从不废话的我们不能浪费这次交流的机会,在程序媛妹子让我们注册类型的时候我们还要问清楚她想怎样得到这个类型的实例,每次都给她一个新的,还是总给她同一个?换句话说,当一个Service被注册到ServiceCollection的时候,我们需要同时知道它的类型和实例生命周期。
ServiceCollection很体贴,我们可以直接用不同的注册方法注册不同生命周期的Service:
1 2 3 4 5 6 7 8 9 10 service.AddTransient<MyTransientClass>(); service.AddSingleton<MySingletonClass>(); service.AddScoped<MyScopedClass>();
以上3个生命周期类型基本上涵盖了所有可能的场景:
每次都要新实例。
永远都只需要同一个实例。
在一个范围之内只需要同一个实例,但是不同范围之内的实例要不同。
醒醒,无聊的理论时间过去了,Demo 上场了 说书者曰“闲话休提,且说正话”,咱们也到了“理论休提,且看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 里打开,然后我们就可以写代码了。
STEP 1: 添加对 Microsoft.Extensions.DependencyInjection 的引用 首先,我们需要添加一个引用:Microsoft.Extensions.DependencyInjection,依赖注入的默认实现都在里面。
打开 project.json,然后在dependencies里添加引用。添加完成之后,project.json应该看起来是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "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,那么这里的版本号会有所不同。
STEP2: 准备工作 在我们的示例程序引入依赖注入之前,有几项准备工作要做。
首先, 我们需要一个可以注册到依赖注入的类型,这个简单:
1 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>来做到。
1 2 3 4 5 6 7 8 9 10 11 12 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>上的扩展方法来简化比较操作。
1 2 3 4 5 6 7 8 9 10 11 12 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获取服务实例,插入到一个集合中并返回这个集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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>()); } }
以上的准备工作使我们接下来的验证操作变得容易了很多。
STEP 3: 验证 Singleton 生命周期 我们创建一个方法TryOutSingleton来验证 Singleton 生命周期
1 2 3 4 5 6 7 8 9 private static void TryOutSingleton ( ) { ServiceCollection services = new ServiceCollection(); services.AddSingleton<MyClass>(); ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); Console.WriteLine(bag.AreIdentical()); }
不出所料,最后输出的结果是:
STEP 4: 验证 Transient 生命周期 再创建一个 TryOutTransient 方法验证 Transient 生命周期
1 2 3 4 5 6 7 8 9 private static void TryOutTransient ( ) { ServiceCollection services = new ServiceCollection(); services.AddTransient<MyClass>(); ConcurrentBag<MyClass> bag = GetObjectsFromDI(services); Console.WriteLine(bag.AreDifferent()); }
同样不出意外,输出结果是:
STEP 5: Scoped 生命周期 前面提到过, Scoped 生命周期比较特别,同一个Scope里的实例是同一个,但是不同Scope里的实例是不同的。而Scope具体的含义取决于我们自己的定义。
具体到代码级别,当我们需要创建一个Scope的时候,我们需要用到我们之前得到的IServiceProvider,它有一个CreateScope方法可以创建一个类型为Microsoft.Extensions.DependencyInjection.IServiceScope的Scope,而这个Scope实例有一个IServiceProvider类型的属性ServiceProvider!自此,我们应该使用这个来自IServiceScope的IServiceProvider(取代之前我们得到的IServiceProvider)来获取服务实例,它会正确处理Singleton, Transient以及Scoped这3种生命周期!
1 2 3 4 5 6 7 8 9 10 11 ServiceCollection services = new ServiceCollection(); IServiceProvider serviceProvider = services.BuildServiceProvider(); IServiceScope scope = serviceProvider.CreateScope(); IServiceProvider newServiceProvider = scope.ServiceProvider; MyClass obj = newServiceProvider.GetRequiredService<MyClass>();
为了验证Scoped生命周期,我们现在定义Scope为线程空间。也就是说,每一个线程为一个Scope,对于Scoped生命周期的类型,在同一个线程之内获取的实例应该是同一个,但是不同线程获取的实例是不同的。
在演示代码中,我们注册3个不同的类型,分别对应3种不同的生命周期,看看来自IServiceScope的IServiceProvider能否正确处理每一种生命周期类型。
代码有些啰嗦,因为不想再拆分成更小的方法了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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(); 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)); 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(); IEnumerable<MySingleton> singletons = thread1SingletonBag.Concat(thread2SingletonBag); Console.WriteLine($"Singleton: {singletons.Count()} objects are IDENTICAL? {singletons.AreIdentical()} " ); IEnumerable<MyTransient> transients = thread1TransientBag.Concat(thread2TransientBag); Console.WriteLine($"Transient: {transients.Count()} objects are DIFFERENT? {transients.AreDifferent()} " ); 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())} " ); }
输出结果为:
1 2 3 4 5 6 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 上获取。