享受Microsoft用于.NET Core的IOC容器的乐趣

2020-08-22 12:01:00

这篇文章的目的是从头开始配置和使用微软的默认依赖注入容器,以了解它们在运行时是如何挂在一起的。还有许多其他很棒的文章解释了ASP.NET Core中的依赖注入和控制反转(从现在开始,DI&;IoC)。本文假设您理解这些原则。所以我不会去报道那些。

我们将从一个简单的控制台应用程序开始,配置一个IoC容器,然后深入研究.NET Core DI扩展的源代码,从中获得一些乐趣。

.NET Core IOC容器位于Microsoft.Extensions.DependencyInjection命名空间中。让我们看一下在我们的应用程序中使用它所涉及的步骤。

我们将从一个简单的控制台应用程序开始,看看如何实现上述步骤。

在您最喜欢的IDE中打开它,您就可以开始学习了。为了使本教程简单,我不会使用任何其他依赖项(如日志记录)。

让我们向项目中添加两个类:MyService和MyDependency。这是这两节课要讲的内容。

公共类{private readonly_myDependency;public MyService(MyDependency){Console。WriteLine(";构造的MyService";);_myDependency=myDependency;}public DoSomething(){_myDependency。DoWork();}}。

公共类{public MyDependency(){Console。WriteLine(";Constructed MyDependency";);}public DoWork(){控制台。WriteLine(";在MyDependency";中做一些工作);}}

正如我们所看到的,myservice对myDependency有一个(恰当地命名为😅)依赖。为了调用我们的服务,我们需要将所需依赖项的实例传递给它的构造函数。在没有任何IOC内容的驱动程序代码(Program.cs)中,我们可以做这样的事情。

这是Baaaaad💀。问题是,现在我们负责管理依赖项、它们的生存期(也违反了可靠的原则)等。让我们使用IoC容器来解决这个问题。我们将简单地给它们一个有作用域的生命周期。

//将我们的类型注册到容器CONTAINER=new();CONTAINER中。AddScoped();容器。AddScoped();//构建IOC,获取Provider Provider=CONTAINER。BuildServiceProvider();

如果您不熟悉服务生命周期,最好在继续之前参考官方文档。

💡请注意,您不必在ASP.NET Core Web或Worker Service应用程序中实例化ServiceCollection。理想情况下,您应该在Startup类中使用IServiceCollection注册服务。

设置好之后,我们需要调用容器上的BuildServiceProvider方法。结果,这将返回一个ServiceProvider。请记住,我们首先需要在容器(ServiceCollection)中注册我们的类型,然后使用提供程序检索它们。请容忍我在这个问题上的观点,因为我们使用的是具体实现,而不是最初的接口。

现在,当我们运行它时,它应该创建一个MyDependency实例,构造一个MyService实例,并运行DoSomething()方法。

请注意,我们从未构造过MyDependency,因为DI框架解决了它,并为我们做了构造函数注入。好的!

查看我们的代码,ServiceCollection包含一组ServiceDescriptor,并提供一些实用方法来操作它。ServiceDescriptor实际上是描述(类型、实现、生存期等)的对象。我们的服务注册。

Public class:{private readonly_description ptors=new();public count=>;_description ptors.Count;public IsReadOnly=>;false;public this[index]{get{return_description ptors[index];}set{_description ptors[index]=value;}}public clear(){..。。}PUBLIC CONTAINS(项目){..。。}public Remove(Item){..。。}ICCollect<;ServiceDescriptor>;。添加(项目){..。}//为简明起见删除了其他方法}。

AddScoped(和其他服务注册扩展方法)方法只是将我们的服务注册(作为ServiceDescriptor)添加到OPER_DESCRIPTERS列表中。

查看BuildServiceProvider扩展方法的内部,我们可以看到它实际上使用我们给定的服务注册实例化了一个新的服务提供者。正如我们在前面看到的,这些服务注册作为ServiceCollection传入,如果您放置debug this,您将能够看到以下内容:

如果您感兴趣,我所做的一个观察就是了解它何时会实例化我们的类型。在我们的Program.cs中,如果您按如下方式更新,

看到“在My Dependency中做一些工作”之前的虚线了吗?我最初认为这些类型在我们构建服务提供程序时就已经构建好了。但是,它们似乎是在我们调用Provider.GetService<;MyService>;();时实例化的。但是,这不应与ASP.NET核心Web应用程序中的服务生存期混淆,在ASP.NET核心Web应用程序中,服务生存期在请求管道中扮演主要角色。

💡在调用getService()方法时,而不是在构建IoC容器时,IoC容器将实例化我们的实现类型。

查看ServiceProviderEngine代码的实现,您将看到保存所有类型注册的数据结构实际上是一个ConcurrentDictionary。如果深入研究GetService的调用链,您会在DI扩展的源代码中遇到以下类。

因此,我们对如何在IOC容器中注册服务有了一个想法。然而,这仍然可以通过映射接口而不是使用具体类来改进。我们将在下一节中研究它。

这里没什么有趣的。让我们只在相应的类中实现这两个接口。

公共类:{private readonly_myDependency;public MyService(MyDependency){Console。WriteLine(";构造的MyService";);_myDependency=myDependency;}public DoSomething(){_myDependency。DoWork();}}。

在我们的Program.cs中,我们将更新以下行以使用抽象而不是具体类。

现在,当我们运行应用程序时,我们将获得相同的结果。但是,为了让事情变得有趣,让我们添加另一个实现IMyDependency接口的MyDependency类。在不花太多时间在名称上的情况下,让我们将其命名为MyDependency2并将其注册到IOC容器中。

💡请记住,如果您为同一接口添加了多个具体实现,默认情况下,它将始终选择最后一个注册的实现。

那么,我们如何为同一服务类型注册多个实现呢?它不受支持,因为您可能已经知道或在Github上遇到过这个讨论。通过查看GetService()方法的内部结构,我们可以揭示为什么会发生这种情况。

如果调试ServiceCollection,您将能够看到我们的两种IMyDependency实现类型仍然存在。

因此,调用堆栈中必须有某种东西将最后注册的实现类型移交给服务提供者。如果您从ServiceProviderServiceExtensions类中的GetService()方法开始,那么您将结束于ServiceProviderEngine的GetService()方法的实现。

内部GetService(serviceType,serviceProviderEngineScope){if(_Disposed){ThrowHelper。ThrowObjectDisposedException();}realizedService=RealizedServices。GetOrAdd(serviceType,_createServiceAccessor);_callback?OnResolve(serviceType,serviceProviderEngineScope);DependencyInjectionEventSource.Log。ServiceResolved(ServiceType);返回realizedService。Invoke(ServiceProviderEngineScope);}。

RealizedServices是一个跟踪我们的服务类型的ConcurrentDictionary和一个通过访问依赖关系树中的相应调用站点来检索实现类型的函数。_createServiceAccessor实际上引用执行上述操作的CreateServiceAccessor。

要找到这一点的“原因”,我们需要深入研究GetCallSite方法。这有望引导您转到CallSiteFactory.cs类中的CreateCallSite方法。如果您找到以下几行,就可以提示您下一步要看哪里。在我们的示例中,它将调用TryCreateExact方法,因为它既不是Open泛型,也不是可枚举的。

看一看TryCreateExact内部,您将能够看到下面类似于我们正在寻找的服务类型的最后一个实现类型中传递的内容。在这里,您可以从更细粒度的细节中找到我们正在搜索的答案。

假设在极少数情况下,您希望在代码中的某个位置同时调用这两个依赖项;您将如何实现这一点?

我对MyService类做了一些修改,让您了解如何做到这一点。这里的关键是,我们需要将依赖项作为IEnumerable注入,这将使我们能够访问给定类型的服务注册。

公共类:{private readonly_myDependency;public MyService(MyDependency){Console。WriteLine(";Constructed MyService";);_myDependency=myDependency;}public DoSomething(){foreach(Dependency In_MyDependency){依赖关系。DoWork();}

或者更好的是,您可以通过执行如下操作,使用一点LINQ专门选择所需的依赖项:

💡请注意,只有当参数类型为IEnumerable而不是IList、数组等任何其他类型时,IoC容器才会解析和注入服务集合。否则,可能会出现InvalidOperationException。

这不是世界上最好的解决方案,但可以完成工作🙂另一个缺点是必须手动注册服务,而不是使用程序集扫描。您还可以尝试TryAddEnumerable扩展方法,看看如何根据您的用例改进此实现。

绕道而行,让我们看看在注入IEnumerable时如何同时获得MyDependency和MyDependency2。

当它执行服务查找时,根据服务的生存期,它将访问服务提供者引擎的不同缓存。这发生在CallSiteVisitor类的VisitCallSite中。在这个迂回过程中,您还将遇到一些使用监视器👀的令人印象深刻的锁定代码。一旦您经历了更多的循环,您将结束在VisitCallSiteMain方法中,该方法将调用VisitIEnumerable方法。

如果您仔细查看上面屏幕截图中的ServiceCallSite(以蓝色突出显示),您可以看到它在相应的ImplementationType params部分中引用了ImplementationType params下的MyDependency和MyDependency2类型。因为我们请求IEnumerable,所以它知道它需要解析我们在IMyDependency类型下注册的所有依赖项。

现在,如果您有一个Look VisitIEnumerable方法,您将看到它在for循环中访问每个实现类型的构造函数。

既然我们已经遍历了每个实现类型,我们将再次遍历VisitCallSite方法(在缓存中进行服务查找,等等)。与前面一样调用链,并在VisitCallSiteMain方法中结束。

似曾相识?我们再次像以前一样访问VisitCallSiteMain方法,因为现在我们正在通过访问上面循环中的ctor来解析IMyDependency类型的每个依赖项。在VisitConstructor方法中,通过使用反射(ConstructorInfo.Invoke()方法),它将调用我们实现的类型上的构造函数并返回它。请注意,这只会在初始调用查找服务期间发生,后续调用取决于生存期(通过使用内部缓存)。

正如我们所看到的,微软默认的IoC容器拥有所有必要的功能,但也缺乏一些高级功能,如程序集扫描、服务装饰器,这些都是您在Autofac、Scretor等框架中可以获得的。

总而言之,我们首先介绍了如何轻松地将Microsoft的IoC容器引入控制台应用程序。然后,我们了解了使用提供者进行服务注册和检索服务。最后,通过使用给定服务类型的多个实现以及在此过程中学到的一些知识,我们从中获得了一些乐趣。不过,这只是冰山一角。还有更多的东西可以深入研究,我希望在未来能够涵盖这些内容。