[ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费

包含服务注册信息的IServiceCollection集合最终被用来创建作为依赖注入容器的IServiceProvider对象。当需要消费某个服务实例的时候,我们只需要指定服务类型调用IServiceProvider的GetService方法即可,IServiceProvider对象就会根据对应的服务注册提供所需的服务实例。

一、IServiceProvider

如下面的代码片段所示,IServiceProvider接口定义了唯一的GetService方法根据指定的类型来提供对应的服务实例。当利用包含服务注册的IServiceCollection对象创建出IServiceProvider对象之后,我们只需要将服务注册的服务类型(对应于ServiceDescriptor的ServiceType属性)作为参数调用GetService方法,该方法就能根据服务注册信息为我们提供对应的服务实例。

public interface IServiceProvider
{
    object GetService(Type serviceType);
}

针对IServiceProvider对象的创建体现在IServiceCollection接口的三个BuildServiceProvider扩展方法重载上。如下的代码片段所示,这三个扩展方法提供的都是一个类型为ServiceProvider的对象,该对象根据提供的配置选项来创建。配置选项类型ServiceProviderOptions提供了两个属性,其中ValidateScopes属性表示是否需要开启针对服务范围的验证,而ValidateOnBuild属性则表示是否需要预先检验作为服务注册的每个ServiceDescriptor对象能否提供对应的服务实例。默认情况下这两种类型的检验都是关闭的。

public class ServiceProviderOptions
{
    public bool ValidateScopes { get; set; }
    public bool ValidateOnBuild { get; set; }
    internal static readonly ServiceProviderOptions Default  = new ServiceProviderOptions();
}

public static class ServiceCollectionContainerBuilderExtensions
{
    public static ServiceProvider BuildServiceProvider( this IServiceCollection services)
        => BuildServiceProvider(services, ServiceProviderOptions.Default);
    public static ServiceProvider BuildServiceProvider( this IServiceCollection services, bool validateScopes)
        => services.BuildServiceProvider(new ServiceProviderOptions {  ValidateScopes = validateScopes });

    public static ServiceProvider BuildServiceProvider( this IServiceCollection services, ServiceProviderOptions options)
        => new ServiceProvider(services, options);
}

虽然调用IServiceCollection的BuildServiceProvider扩展方法返回总是一个ServiceProvider对象,但是我并不打算详细介绍这个类型,这是因为ServiceProvider涉及到一系列内部类型和接口,并且实现在该类型中针对服务实例的提供机制一直在不断的变化,而且这个变化趋势在未来版本更替过程中可能还将继续下去。

除了定义在IServiceProvider接口中的GetService方法,该接口还具有如下这些扩展方法来提供服务实例。GetService<T>方法以泛型参数的形式指定了服务类型,返回的服务实例也会作对应的类型转换。如果指定服务类型的服务注册不存在,GetService方法会返回Null,如果调用GetRequiredService或者GetRequiredService<T>方法则会抛出一个InvalidOperationException类型的异常。如果所需的服务实例是必需的,我们一般会调用这两个扩展方法。

public static class ServiceProviderServiceExtensions
{
    public static T GetService<T>(this IServiceProvider provider);

    public static T GetRequiredService<T>(this IServiceProvider provider);
    public static object GetRequiredService(this IServiceProvider provider,  Type serviceType);
    
    public static IEnumerable<T> GetServices<T>(this IServiceProvider provider);
    public static IEnumerable<object> GetServices(this IServiceProvider provider,  Type serviceType);
}

正如前面多次提到的,如果针对某个类型添加了多个服务注册,那么GetService方法总是会采用最新添加的服务注册来提供服务实例。如果希望利用所有的服务注册来创建一组服务实例列表,我们可以调用GetServices或者GetServices<T>方法,也可以调用GetService<IEnumerable<T>>方法。

二、服务实例的创建

对于通过调用IServiceCollection集合的BuildServiceProvider方法创建的IServiceProvider对象来说,当我们通过指定服务类型调用其GetService方法以获取对应的服务实例的时候,它总是会根据提供的服务类型从服务注册列表中找到对应的ServiceDescriptor对象,并根据它来提供所需的服务实例。

ServiceDescriptor具有三个不同的构造函数,分别对应着服务实例最初的三种提供方式,我们可以提供一个Func<IServiceProvider, object>对象作为工厂来创建对应的服务实例,也可以直接提供一个创建好的服务实例。如果我们提供的是服务的实现类型,那么最终提供的服务实例将通过调用该类型的某个构造函数来创建,那么构造函数是通过怎样的策略被选择出来的呢?

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

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

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IGux {}

public class Foo : IFoo {}
public class Bar : IBar {}
public class Baz : IBaz {}
public class Gux : IGux
{
    public Gux(IFoo foo)  => Console.WriteLine("Selected constructor: Gux(IFoo)");
    public Gux(IFoo foo, IBar bar)  => Console.WriteLine("Selected constructor: Gux(IFoo, IBar)");
    public Gux(IFoo foo, IBar bar, IBaz baz)  => Console.WriteLine("Selected constructor: Gux(IFoo, IBar, IBaz)");
}

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

class Program
{
    static void Main()
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

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

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

4-6

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

class Program
{
    static void Main()
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IBaz, Baz>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

public class Gux : IGux
{
    public Gux(IFoo foo, IBar bar) {}
    public Gux(IBar bar, IBaz baz) {}
}

对于Gux的两个构造函数,虽然它们的参数均能够由IServiceProvider对象来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以IServiceProvider无法选择出一个最佳的构造函数。运行该程序后会抛出如下图所示的InvalidOperationException异常,并提示无法从两个候选的构造函数中选择出一个最优的来创建服务实例。(S409)

4-7

[ASP.NET Core 3框架揭秘] 依赖注入[1]:控制反转
[ASP.NET Core 3框架揭秘] 依赖注入[2]:IoC模式
[ASP.NET Core 3框架揭秘] 依赖注入[3]:依赖注入模式
[ASP.NET Core 3框架揭秘] 依赖注入[4]:一个迷你版DI框架
[ASP.NET Core 3框架揭秘] 依赖注入[5]:利用容器提供服务
[ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
[ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
[ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
[ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配

posted @ 2019-11-13 08:16  Artech  阅读(3468)  评论(10编辑  收藏  举报