ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationInstance和ImplementationFactory属性均为Null,那么ServiceProvider最终会利用其ImplementationType属性返回的真实类型选择一个适合的构造函数来创建最终的服务实例。我们知道服务服务的真实类型可以定义了多个构造函数,那么ServiceProvider针对构造函数的选择会采用怎样的策略呢?

目录
一、构造函数的选择
二、生命周期管理
    ServiceScope与ServiceScopeFactory
    三种生命周期管理模式
    服务实例的回收

一、构造函数的选择

如果ServiceProvider试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,最终被选择出来的构造函数必须具备一个基本的条件:ServiceProvider能够提供构造函数的所有参数。为了让读者朋友能够更加真切地理解ServiceProvider在构造函数选择过程中采用的策略,我们不让也采用实例演示的方式来进行讲解。

我们在一个控制台应用中定义了四个服务接口(IFoo、IBar、IBaz和IGux)以及实现它们的四个服务类(Foo、Bar、Baz和Gux)。如下面的代码片段所示,我们为Gux定义了三个构造函数,参数均为我们定义了服务接口类型。为了确定ServiceProvider最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。

   1: public interface IFoo {}
   2: public interface IBar {}
   3: public interface IBaz {}
   4: public interface IGux {}
   5:  
   6: public class Foo : IFoo {}
   7: public class Bar : IBar {}
   8: public class Baz : IBaz {}
   9: public class Gux : IGux
  10: {
  11:     public Gux(IFoo foo)
  12:     {
  13:         Console.WriteLine("Gux(IFoo)");
  14:     }
  15:  
  16:     public Gux(IFoo foo, IBar bar)
  17:     {
  18:         Console.WriteLine("Gux(IFoo, IBar)");
  19:     }
  20:  
  21:     public Gux(IFoo foo, IBar bar, IBaz baz)
  22:     {
  23:         Console.WriteLine("Gux(IFoo, IBar, IBaz)");
  24:     }
  25: }

我们在作为程序入口的Main方法中创建一个ServiceCollection对象并在其中添加针对IFoo、IBar以及IGux这三个服务接口的服务注册,针对服务接口IBaz的注册并未被添加。我们利用由它创建的ServiceProvider来提供针对服务接口IGux的实例,究竟能否得到一个Gux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢?

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {       
   5:         new ServiceCollection()
   6:             .AddTransient<IFoo, Foo>()
   7:             .AddTransient<IBar, Bar>()
   8:             .AddTransient<IGux, Gux>()
   9:             .BuildServiceProvider()
  10:             .GetServices<IGux>();
  11:     }
  12: }

对于定义在Gux中的三个构造函数来说,ServiceProvider所在的ServiceCollection包含针对接口IFoo和IBar的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为IBaz的参数,这无法通过ServiceProvider来提供。根据我们上面介绍的第一个原则(ServiceProvider能够提供构造函数的所有参数),Gux的前两个构造函数会成为合法的候选构造函数,那么ServiceProvider最终会选择哪一个呢?

在所有合法的候选构造函数列表中,最终被选择出来的构造函数具有这么一个特征:每一个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。如果这样的构造函数并不存在,一个类型为InvalidOperationException的异常会被跑出来。根据这个原则,Gux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数仅仅具有一个类型为IFoo的参数,最终被选择出来的会是Gux的第二个构造函数,所有运行我们的实例程序将会在控制台上产生如下的输出结果。

   1: Gux(IFoo, IBar)

接下来我们对实例程序略加改动。如下面的代码片段所示,我们只为Gux定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo&IBar和IBar&IBaz。在Main方法中,我们将针对IBaz/Baz的服务注册添加到创建的ServiceCollection上。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {       
   5:         new ServiceCollection()
   6:             .AddTransient<IFoo, Foo>()
   7:             .AddTransient<IBar, Bar>()
   8:             .AddTransient<IBaz, Baz>()
   9:             .AddTransient<IGux, Gux>()
  10:             .BuildServiceProvider()
  11:             .GetServices<IGux>();
  12:     }
  13: }
  14:  
  15: public class Gux : IGux
  16: {
  17:     public Gux(IFoo foo, IBar bar) {}
  18:     public Gux(IBar bar, IBaz baz) {}
  19: }

对于Gux的两个构造函数,虽然它们的参数均能够由ServiceProvider来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以ServiceProvider无法选择出一个最佳的构造函数。如果我们运行这个程序,一个InvalidOperationException异常会被抛出来,控制台上将呈现出如下所示的错误消息。

   1: Unhandled Exception: System.InvalidOperationException: Unable to activate type 'Gux'. The following constructors are ambigious:
   2: Void .ctor(IFoo, IBar)
   3: Void .ctor(IBar, IBaz)
   4: ...

二、生命周期管理

生命周期管理决定了ServiceProvider采用怎样的方式创建和回收服务实例。ServiceProvider具有三种基本的生命周期管理模式,分别对应着枚举类型ServiceLifetime的三个选项(Singleton、Scoped和Transient)。对于ServiceProvider支持的这三种生命周期管理模式,Singleton和Transient的语义很明确,前者(Singleton)表示以“单例”的方式管理服务实例的生命周期,意味着ServiceProvider对象多次针对同一个服务类型所提供的服务实例实际上是同一个对象;而后者(Transient)则完全相反,对于每次服务提供请求,ServiceProvider总会创建一个新的对象。那么Scoped又体现了ServiceProvider针对服务实例怎样的生命周期管理方式呢?

ServiceScope与ServiceScopeFactory

ServiceScope为某个ServiceProvider对象圈定了一个“作用域”,枚举类型ServiceLifetime中的Scoped选项指的就是这么一个ServiceScope。在依赖注入的应用编程接口中,ServiceScope通过一个名为IServiceScope的接口来表示。如下面的代码片段所示,继承自IDisposable接口的IServiceScope具有一个唯一的只读属性ServiceProvider返回确定这个服务范围边界的ServiceProvider。表示ServiceScope由它对应的工厂ServiceScopeFactory来创建,后者体现为具有如下定义的接口IServiceScopeFactory。

   1: public interface IServiceScope : IDisposable
   2: {
   3:     IServiceProvider ServiceProvider { get; }
   4: }
   5:  
   6: public interface IServiceScopeFactory
   7: {
   8:     IServiceScope CreateScope();
   9: }

若要充分理解ServiceScope和ServiceProvider之间的关系,我们需要简单了解一下ServiceProvider的层级结构。除了直接通过一个ServiceCollection对象创建一个独立的ServiceProvider对象之外,一个ServiceProvider还可以根据另一个ServiceProvider对象来创建,如果采用后一种创建方式,我们指定的ServiceProvider与创建的ServiceProvider将成为一种“父子”关系。

   1: internal class ServiceProvider : IServiceProvider, IDisposable
   2: {
   3:     private readonly ServiceProvider _root;
   4:     internal ServiceProvider(ServiceProvider parent)
   5:     {
   6:         _root = parent._root;
   7:     }
   8:     //其他成员
   9: }

3-11虽然在ServiceProvider在创建过程中体现了ServiceProvider之间存在着一种树形化的层级结构,但是ServiceProvider对象本身并没有一个指向“父亲”的引用,它仅仅会保留针对根节点的引用。如上面的代码片段所示,针对根节点的引用体现为ServiceProvider类的字段_root。当我们根据作为“父亲”的ServiceProvider创建一个新的ServiceProvider的时候,父子均指向同一个“根”。我们可以将创建过程中体现的层级化关系称为“逻辑关系”,而将ServiceProvider对象自身的引用关系称为“物理关系”,右图清楚地揭示了这两种关系之间的转化。

由于ServiceProvider自身是一个内部类型,我们不能采用调用构造函数的方式根据一个作为“父亲”的ServiceProvider创建另一个作为“儿子”的ServiceProvider,但是这个目的可以间接地通过创建ServiceScope的方式来完成。如下面的代码片段所示,我们首先创建一个独立的ServiceProvider并调用其GetService<T>方法获得一个ServiceScopeFactory对象,然后调用后者的CreateScope方法创建一个新的ServiceScope,它的ServiceProvider就是前者的“儿子”。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider serviceProvider1 = new ServiceCollection().BuildServiceProvider();
   6:         IServiceProvider serviceProvider2 = serviceProvider1.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
   7:  
   8:         object root = serviceProvider2.GetType().GetField("_root", BindingFlags.Instance| BindingFlags.NonPublic).GetValue(serviceProvider2);
   9:         Debug.Assert(object.ReferenceEquals(serviceProvider1, root));        
  10:     }
  11: }

如果读者朋友们希望进一步了解ServiceScope的创建以及它和ServiceProvider之间的关系,我们不妨先来看看作为IServiceScope接口默认实现的内部类型ServiceScope的定义。如下面的代码片段所示,ServiceScope仅仅是对一个ServiceProvider对象的简单封装而已。值得一提的是,当ServiceScope的Dispose方法被调用的时候,这个被封装的ServiceProvider的同名方法同时被执行。

   1: {
   2:     private readonly ServiceProvider _scopedProvider;
   3:     public ServiceScope(ServiceProvider scopedProvider)
   4:     {
   5:         this._scopedProvider = scopedProvider;
   6:     }
   7:  
   8:     public void Dispose()
   9:     {
  10:         _scopedProvider.Dispose();
  11:     }
  12:  
  13:     public IServiceProvider ServiceProvider
  14:     {
  15:         get {return _scopedProvider; }
  16:     }
  17: }

IServiceScopeFactory接口的默认实现类型是一个名为ServiceScopeFactory的内部类型。如下面的代码片段所示,ServiceScopeFactory的只读字段“_provider”表示当前的ServiceProvider。当CreateScope方法被调用的时候,这个ServiceProvider的“子ServiceProvider”被创建出来,并被封装成返回的ServiceScope对象。

   1: internal class ServiceScopeFactory : IServiceScopeFactory
   2: {
   3:     private readonly ServiceProvider _provider;
   4:     public ServiceScopeFactory(ServiceProvider provider)
   5:     {
   6:         _provider = provider;
   7:     }
   8:  
   9:     public IServiceScope CreateScope()
  10:     {
  11:         return new ServiceScope(new ServiceProvider(_provider));
  12:     }
  13: }

三种生命周期管理模式

只有在充分了解ServiceScope的创建过程以及它与ServiceProvider之间的关系之后,我们才会对ServiceProvider支持的三种生命周期管理模式(Singleton、Scope和Transient)具有深刻的认识。就服务实例的提供方式来说,它们之间具有如下的差异:

  • Singleton:ServiceProvider创建的服务实例保存在作为根节点的ServiceProvider上,所有具有同一根节点的所有ServiceProvider提供的服务实例均是同一个对象。
  • Scoped:ServiceProvider创建的服务实例由自己保存,所以同一个ServiceProvider对象提供的服务实例均是同一个对象。
  • Transient:针对每一次服务提供请求,ServiceProvider总是创建一个新的服务实例。

为了让读者朋友们对ServiceProvider支持的这三种不同的生命周期管理模式具有更加深刻的理解,我们照例来做一个简单的实例演示。我们在一个控制台应用中定义了如下三个服务接口(IFoo、IBar和IBaz)以及分别实现它们的三个服务类(Foo、Bar和Baz)。

   1: public interface IFoo {}
   2: public interface IBar {}
   3: public interface IBaz {}
   4:  
   5: public class Foo : IFoo {}
   6: public class Bar : IBar {}
   7: public class Baz : IBaz {}

现在我们在作为程序入口的Main方法中创建了一个ServiceCollection对象,并采用不同的生命周期管理模式完成了针对三个服务接口的注册(IFoo/Foo、IBar/Bar和IBaz/Baz分别Transient、Scoped和Singleton)。我们接下来针对这个ServiceCollection对象创建了一个ServiceProvider(root),并采用创建ServiceScope的方式创建了它的两个“子ServiceProvider”(child1和child2)。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider root = new ServiceCollection()
   6:             .AddTransient<IFoo, Foo>()
   7:             .AddScoped<IBar, Bar>()
   8:             .AddSingleton<IBaz, Baz>()
   9:             .BuildServiceProvider();
  10:         IServiceProvider child1 = root.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
  11:         IServiceProvider child2 = root.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
  12:  
  13:         Console.WriteLine("ReferenceEquals(root.GetService<IFoo>(), root.GetService<IFoo>() = {0}",ReferenceEquals(root.GetService<IFoo>(), root.GetService<IFoo>()));
  14:         Console.WriteLine("ReferenceEquals(child1.GetService<IBar>(), child1.GetService<IBar>() = {0}",ReferenceEquals(child1.GetService<IBar>(), child1.GetService<IBar>()));
  15:         Console.WriteLine("ReferenceEquals(child1.GetService<IBar>(), child2.GetService<IBar>() = {0}",ReferenceEquals(child1.GetService<IBar>(), child2.GetService<IBar>()));
  16:         Console.WriteLine("ReferenceEquals(child1.GetService<IBaz>(), child2.GetService<IBaz>() = {0}",ReferenceEquals(child1.GetService<IBaz>(), child2.GetService<IBaz>()));
  17:     }
  18: }

为了验证ServiceProvider针对Transient模式是否总是创建新的服务实例,我们利用同一个ServiceProvider(root)获取针对服务接口IFoo的实例并进行比较。为了验证ServiceProvider针对Scope模式是否仅仅在当前ServiceScope下具有“单例”的特性,我们先后比较了同一个ServiceProvider(child1)和不同ServiceProvider(child1和child2)两次针对服务接口IBar获取的实例。为了验证具有“同根”的所有ServiceProvider针对Singleton模式总是返回同一个服务实例,我们比较了两个不同child1和child2两次针对服务接口IBaz获取的服务实例。如下所示的输出结构印证了我们上面的论述。

   1: ReferenceEquals(root.GetService<IFoo>(), root.GetService<IFoo>()         = False
   2: ReferenceEquals(child1.GetService<IBar>(), child1.GetService<IBar>()     = True
   3: ReferenceEquals(child1.GetService<IBar>(), child2.GetService<IBar>()     = False
   4: ReferenceEquals(child1.GetService<IBaz>(), child2.GetService<IBaz>()     = True

服务实例的回收

ServiceProvider除了为我们提供所需的服务实例之外,对于由它提供的服务实例,它还肩负起回收之责。这里所说的回收与.NET自身的垃圾回收机制无关,仅仅针对于自身类型实现了IDisposable接口的服务实例,所谓的回收仅仅体现为调用它们的Dispose方法。ServiceProvider针对服务实例所采用的收受策略取决于服务注册时采用的生命周期管理模式,具体采用的服务回收策略主要体现为如下两点:

  • 如果注册的服务采用Singleton模式,由某个ServiceProvider提供的服务实例的回收工作由作为根的ServiceProvider负责,后者的Dispose方法被调用的时候,这些服务实例的Dispose方法会自动执行。
  • 如果注册的服务采用其他模式(Scope或者Transient),ServiceProvider自行承担由它提供的服务实例的回收工作,当它的Dispose方法被调用的时候,这些服务实例的Dispose方法会自动执行。

我们照例使用一个简单的实例来演示ServiceProvider针对不同生命周期管理模式所采用的服务回收策略。我们在一个控制台应用中定义了如下三个服务接口(IFoo、IBar和IBaz)以及三个实现它们的服务类(Foo、Bar和Baz),这些类型具有相同的基类Disposable。Disposable实现了IDisposable接口,我们在Dispose方法中输出相应的文字以确定对象回收的时机。

   1: public interface IFoo {}
   2: public interface IBar {}
   3: public interface IBaz {}
   4:  
   5: public class Foo : Disposable, IFoo {}
   6: public class Bar : Disposable, IBar {}
   7: public class Baz : Disposable, IBaz {}
   8:  
   9: public class Disposable : IDisposable
  10: {
  11:     public void Dispose()
  12:     {
  13:         Console.WriteLine("{0}.Dispose()", this.GetType());
  14:     }
  15: }

我们在作为程序入口的Main方法中创建了一个ServiceCollection对象,并在其中采用不同的生命周期管理模式注册了三个相应的服务(IFoo/Foo、IBar/Bar和IBaz/Baz分别采用Transient、Scoped和Singleton模式)。我们针对这个ServiceCollection创建了一个ServiceProvider(root),以及它的两个“儿子”(child1和child2)。在分别通过child1和child2提供了两个服务实例(child1:IFoo, child2:IBar/IBaz)之后,我们先后调用三个ServiceProvider(child1=>child2=>root)的Dispose方法。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider root = new ServiceCollection()
   6:             .AddTransient<IFoo, Foo>()
   7:             .AddScoped<IBar, Bar>()
   8:             .AddSingleton<IBaz, Baz>()
   9:             .BuildServiceProvider();
  10:         IServiceProvider child1 = root.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
  11:         IServiceProvider child2 = root.GetService<IServiceScopeFactory>().CreateScope().ServiceProvider;
  12:  
  13:         child1.GetService<IFoo>();
  14:         child1.GetService<IFoo>();
  15:         child2.GetService<IBar>();
  16:         child2.GetService<IBaz>();
  17:  
  18:         Console.WriteLine("child1.Dispose()");
  19:         ((IDisposable)child1).Dispose();
  20:  
  21:         Console.WriteLine("child2.Dispose()");
  22:         ((IDisposable)child2).Dispose();
  23:  
  24:         Console.WriteLine("root.Dispose()");
  25:         ((IDisposable)root).Dispose();
  26:     }
  27: }

该程序运行之后会在控制台上产生如下的输出结果。从这个结果我们不难看出由child1提供的两个采用Transient模式的服务实例的回收实在child1的Dispose方法执行之后自动完成的。当child2的Dispose方法被调用的时候,对于由它提供的两个服务对象来说,只有注册时采用Scope模式的Bar对象被自动回收了,至于采用Singleton模式的Baz对象的回收工作,是在root的Dispose方法被调用之后自动完成的。

   1: child1.Dispose()
   2: Foo.Dispose()
   3: Foo.Dispose()
   4: child2.Dispose()
   5: Bar.Dispose()
   6: root.Dispose()
   7: Baz.Dispose()

了解ServiceProvider针对不同生命周期管理模式所采用的服务回收策略还会帮助我们正确的使用它。具体来说,当我们在使用一个现有的ServiceProvider的时候,由于我们并不能直接对它实施回收(因为它同时会在其它地方被使用),如果直接使用它来提供我们所需的服务实例,由于这些服务实例可能会在很长一段时间得不到回收,进而导致一些内存泄漏的问题。如果所用的是一个与当前应用具有相同生命周期(ServiceProvider在应用终止的时候才会被回收)的ServiceProvider,而且提供的服务采用Transient模式,这个问题就更加严重了,这意味着每次提供的服务实例都是一个全新的对象,但是它永远得不到回收。

为了解决这个问题,我想很多人会想到一种解决方案,那就是按照如下所示的方式显式地对提供的每个服务实例实施回收工作。实际上这并不是一种推荐的编程方式,因为这样的做法仅仅确保了服务实例对象的Dispose方法能够被及时调用,但是ServiceProvider依然保持着对服务实例的引用,后者依然不能及时地被GC回收。

   1: public void DoWork(IServiceProvider serviceProvider)
   2: {
   3:     using (IFoobar foobar = serviceProvider.GetService<IFoobar>())
   4:     {
   5:         ...
   6:     }
   7: }

或者

   1: public void DoWork(IServiceProvider serviceProvider)
   2: {
   3:     IFoobar foobar = serviceProvider.GetService<IFoobar>();
   4:     try
   5:     {
   6:         ...
   7:     }
   8:     finally
   9:     {
  10:         (foobar as IDisposable)?.Dispose();
  11:     }
  12: }

由于提供的服务实例总是被某个ServiceProvider引用着[1](直接提供服务实例的ServiceProvider或者是它的根),所以服务实例能够被GC从内存及时回收的前提是引用它的ServiceProvider及时地变成垃圾对象。要让提供服务实例的ServiceProvider成为垃圾对象,我们就必须创建一个新的ServiceProvider,通过上面的介绍我们知道ServiceProvider的创建可以通过创建ServiceScope的方式来实现。除此之外,为我们可以通过回收ServiceScope的方式来回收对应的ServiceProvider,进而进一步回收由它提供的服务实例(仅限Transient和Scoped模式)。下面的代码片段给出了正确的编程方式。

   1: public void DoWork(IServiceProvider serviceProvider)
   2: {
   3:     using (IServiceScope serviceScope = serviceProvider.GetService<IServiceScopeFactory>().CreateScope())
   4:     {
   5:         IFoobar foobar = serviceScope.ServiceProvider.GetService<IFoobar>();
   6:         ...
   7:     }
   8: }

接下来我们通过一个简单的实例演示上述这两种针对服务回收的编程方式之间的差异。我们在一个控制台应用中定义了一个继承自IDisposable的服务接口IFoobar和实现它的服务类Foobar。如下面的代码片段所示,为了确认对象真正被GC回收的时机,我们为Foobar定义了一个析构函数。在该析构函数和Dispose方法中,我们还会在控制台上输出相应的指导性文字。

   1: public interface IFoobar: IDisposable
   2: {}
   3:  
   4: public class Foobar : IFoobar
   5: {
   6:     ~Foobar()
   7:     {
   8:         Console.WriteLine("Foobar.Finalize()");
   9:     }
  10:  
  11:     public void Dispose()
  12:     {
  13:         Console.WriteLine("Foobar.Dispose()");
  14:     }
  15: }

在作为程序入口的Main方法中,我们创建了一个ServiceCollection对象并采用Transient模式将IFoobbar/Foobar注册其中。借助于通过该ServiceCollection创建的ServiceProvider,我们分别采用上述的两种方式获取服务实例并试图对它实施回收。为了强制GC试试垃圾回收,我们显式调用了GC的Collect方法。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         IServiceProvider serviceProvider = new ServiceCollection()
   6:             .AddTransient<IFoobar, Foobar>()
   7:             .BuildServiceProvider();
   8:  
   9:         serviceProvider.GetService<IFoobar>().Dispose();
  10:         GC.Collect();
  11:  
  12:         Console.WriteLine("----------------");
  13:         using (IServiceScope serviceScope = serviceProvider.GetService<IServiceScopeFactory>().CreateScope())
  14:         {
  15:             serviceScope.ServiceProvider.GetService<IFoobar>();
  16:         }
  17:         GC.Collect();
  18:  
  19:         Console.Read();
  20:     }
  21: }

该程序执行之后会在控制台上产生如下所示的输出结果。从这个结果我们可以看出,如果我们使用现有的ServiceProvider来提供所需的服务实例,后者在GC进行垃圾回收之前并不会从内存中释放。如果我们利用现有的ServiceProvider创建一个ServiceScope,并利用它所在的ServiceProvider来提供我们所需的服务实例,GC是可以将其从内存中释放出来的。

   1: Foobar.Dispose()
   2: ----------------
   3: Foobar.Dispose()
   4: Foobar.Finalize()

[1] 对于分别采用 Scoped和Singleton模式提供的服务实例,当前ServiceProvider和根ServiceProvider分别具有对它们的引用。如果采用Transient模式,只有服务类型实现了IDisposable接口,当前ServiceProvider才需要对它保持引用以完成对它们的回收,否则没有任何一个ServiceProvider保持对它们的引用。

ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core中的依赖注入(2):依赖注入(DI)
ASP.NET Core中的依赖注入(3):服务注册与提取
ASP.NET Core中的依赖注入(4):构造函数的选择与生命周期管理
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【总体设计】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【解读ServiceCallSite】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】


作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
原文链接

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

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

相关文章

C# WPF布局控件LayoutControl介绍

Dev学习地址文档地址 &#xff1a;https://docs.devexpress.com/wpf&#xff1a;https://docs.devexpress.com/WPF/7875/wpf-controlswinform&#xff1a;https://docs.devexpress.com/WindowsForms/7874/winforms-controlsasp.NET: https://docs.devexpress.com/AspNet/7873/a…

《看聊天记录都学不会C语言?太菜了吧》(7)下一篇文章告诉你牛郎是谁

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我&#xff0c;若你是真心学习可以送你书籍&#xff0c;指导你学习&#xff0c;给予你目标方向的学习路线&#xff0c;无套路&#xff0c;博客为证。 本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖…

【遥感物候】30年物候始期空间分布特征(平均值)和变化趋势分析(Slope 一元线性回归分析)

问题分析:本文的数据为经过预处理和计算得到的30年(1983-2012年)物候参数始期遥感数据,共计30期影像,现在需要逐像元计算整个物候始期的空间分布特征(平均值)和变化趋势分析(Slope 一元线性回归分析)。最终的效果(左图为分布特征,右图为变化趋势): 一、方法原理 …

deb php7 fileinfo,linux安装php7.2扩展fileinfo

最简便的方法是使用pecl安装php的扩展&#xff0c;方便快捷&#xff0c;这里使用的是源码编译安装php扩展项目 中上传图片遇到的问题&#xff1a;明显是fileinfo不被支持&#xff0c;没有安装fileinfo。接下来开始安装因为我的linux服务器里比较干净&#xff0c;所以之前的php源…

使用keepalived实现双机热备

2019独角兽企业重金招聘Python工程师标准>>> 通常说的双机热备是指两台机器都在运行&#xff0c;但并不是两台机器都同时在提供服务。当提供服务的一台出现故障的时候&#xff0c;另外一台会马上自动接管并且提供服务&#xff0c;而且切换的时间非常短。下面来以kee…

《看聊天记录都学不会C语言?太菜了吧》(8)牛郎和织女竟有一个孩子?

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我&#xff0c;若你是真心学习可以送你书籍&#xff0c;指导你学习&#xff0c;给予你目标方向的学习路线&#xff0c;无套路&#xff0c;博客为证。 本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖…

技术贴:触摸屏(TP)技术交流

转载自&#xff1a;易触网科技 電容式TP的動作原理 PS:電容式TP動作原理是利用人體電流感應來進行的&#xff0c;當人的手指觸摸在TP上&#xff0c;与Panle上的ito電路形成一個耦合電容&#xff08;電容效應&#xff09;&#xff0c;於是手指從觸控點上吸走了一個微小的電流&am…

【遥感物候】植被物候与气候(气温和降水)条件的空间相关性分析

植被生长与气候的关系最为密切,通过计算植被各个生长季参数和气温、降水之间的相关系数可以分析生长季参数的变化与气温、降水之间的关系的程度。本文计算30年的植被物候参数和气候数据之间的相关性,最终效果如下: 目录 一、相关性分析原理

HttpContext.TraceIdentifier那严谨的设计

前言Asp.Net Core中有一个不受人重视的属性HttpContext.TraceIdentifier&#xff0c;它在链路追踪中非常有用&#xff0c;下面是官方的定义:在项目中一般会将该字段输出到每一条日志中&#xff0c;也可以将此Id作为通用响应字段返回前端&#xff0c;后续可以根据该属性和日志匹…

Android之实现多张图片点击预览(支持放缩)和滑动

1 需求 多张图片通过recycleView展示&#xff0c;然后点击具体一张图片支持预览(支持放缩)和滑动 2 解决办法 BaseRecyclerViewAdapterHelper com.github.chrisbanes.photoview.PhotoView ViewPage2 组合起来真香 https://github.com/CymChad/BaseRecyclerViewAdapt…

【Envi风暴】Envi5.4经典安装图文教程

ENVI(The Environment for Visualizing Images)是一个完整的遥感图像处理平台,应用汇集中的软件处理技术覆盖了图像数据的输入/输出、图像定标、图像增强、纠正、正射校正、镶嵌、数据融合以及各种变换、信息提取、图像分类、基于知识的决策树分类、与GIS的整合、DEM及地形信…

错误: nknown column 'xxxx' in 'where clause'

nknown column sdsds in where clause 运行环境&#xff1a;jdk1.7.0_17tomcat 7 spring&#xff1a;3.2.0 mybatis&#xff1a;3.2.7 eclipse 错误&#xff1a;nknown column sdsds in where clause 错误原因&#xff1a;数据库查询无用&#xff0c;可能很多写sql语句都会遇到…

查缺补漏系统学习 EF Core 6 - 批量操作

推荐关注「码侠江湖」加星标&#xff0c;时刻不忘江湖事这是 EF Core 系列的第七篇文章&#xff0c;上一篇文章讲述了 EF Core 中的实体数据修改。这篇文章讲一讲 EF Core 如何进行批量操作。在众多的 ORM 框架中&#xff0c;EF Core 的功能并不是最强大的那个&#xff0c;性能…

半小时一篇文过完C语言基础知识点

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我&#xff0c;若你是真心学习可以送你书籍&#xff0c;指导你学习&#xff0c;给予你目标方向的学习路线&#xff0c;无套路&#xff0c;博客为证。 本文定位读者为小白读者&#xff0c;将使用最快的方法过完C语言基础知识…

php变量的判空和类型判断

&#xff08;1&#xff09;var_dump(); 判断一个变量是否已经声明并且赋值&#xff0c;并且打印类型和值 <?php $a; var_dump($a);//输出null<?php var_dump($a);//输出null<?php$a 10; var_dump($a);//输出 int 10&#xff08;2&#xff09;isset() 判断一个变量…

【Envi风暴】Envi插件大全:多波段拆分工具的巧妙使用

很多场合下需要做波段合成,比如波段432合成赋予红绿蓝,构造标准假彩色等等。合成后的文件通常包含多个单波段文件,在Envi中使用layer stacking工具将多个单波段数据合成为一个文件,如下所示: 那么问题来了,合成后的数据该怎样拆开为原来的单波段呢?今天我们就来学习一种…

php表格怎么合并单元格格式化,table标签的结构与合并单元格的实现方法

1.示例代码&#xff1a;复制代码 代码如下:#1234一个完整的例子&#xff1a;复制代码 代码如下:#FirstnameLastnamePhoneQQ1qianshou111111111111111111112qianshou111111111111111111113qianshou111111111111111111114qianshou111111111111111111112.合并上下的单元格(rowspan…

《看聊天记录都学不会C语言?太菜了吧》(9)老公饼真的有老公送?

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我&#xff0c;若你是真心学习可以送你书籍&#xff0c;指导你学习&#xff0c;给予你目标方向的学习路线&#xff0c;无套路&#xff0c;博客为证。 本系列文章将会以通俗易懂的对话方式进行教学&#xff0c;对话中将涵盖…

@Springboot搭建项目controller层接收json格式的对象失败

今天在使用swagger2测试的时候出错 1、requestBody注解常用来处理content-type不是默认的application/x-www-form-urlcoded编码的内容&#xff0c;比如说&#xff1a;application/json或者是application/xml等。一般情况下来说常用其来处理application/json类型。 2、 通过req…

『技术群里聊些啥』HttpClient 如何判断是同一终结点

前言官方文档对 HttpClientHandler.MaxConnectionsPerServer 属性有如下说明&#xff1a;获取或设置使用 HttpClient 对象发出请求时允许的最大并发连接数&#xff08;每个服务器终结点&#xff09;。请注意&#xff0c;该限制针对每个服务器终结点&#xff0c;例如&#xff0c…