码迷,mamicode.com
首页 > Web开发 > 详细

深入理解net core中的依赖注入、Singleton、Scoped、Transient(一)

时间:2020-05-26 01:04:07      阅读:79      评论:0      收藏:0      [点我收藏+]

标签:uil   根据   tle   解析   equal   一个起点   VID   invoke   eal   

原文:深入理解net core中的依赖注入、Singleton、Scoped、Transient(一)


 

在学习 Asp.Net Core 的过程中,注入可以说是无处不在,对于 .Net Core 来说,它是独立的一个程序集,没有复杂的依赖项和配置文件,所以对于学习 Asp.Net Core 源码的朋友来说,注入作为一个起点非常合适,园子里确实有许多关于注入的博客,不过 .Net Core2.0 已经出来了,注入这一块做了一些 更新,其实有不少 .net 开发人员对微软改来改去这一点不是很满意,加大了学习成本,其实改动分为两种,一种是 Asp.Net Core Mvc 常用 Api 接口的更改(或者配置的更改),这点在 2.0 以来很少有这样的情况了,也就是说 Asp.Net Core Mvc 基本趋于稳定了,另一类就是对代码的优化,前者对研发的跟进造成了很大的伤害值,而后者对于研发而言无关紧要,对于乐于学习源码的程序员而言或许能从中带来许多思考。

所以我打算重新分析 .Net Core2.0 的注入 ,实际发布版本为 .netstandard2.0 程序集为 Microsoft.Extensions.DependencyInjection.dll。

在 .Net Core 中,注入描述为为三个过程,注册服务->创建容器->创建对象,所以我也会分为三个模块来介绍

技术图片

注入元数据#

如果接触过 .Net Core 则或多或少已经接触过注入,下面的代码注册了具有三种生命周期的服务,然后创建一个容器,最后使用容器提供这三个服务的实例对象,我们观察他们的生命周期,看到输出结果基本对 AddTransient 以及 AddSingleton 这两种方式注册的服务具有怎样的生命周期都会有所判断,而 AddScoped 方式注册的服务就复杂一点。

我们看到通过 BuilderServiceProvider 方法创建了一个容器,而容器调用 CreateScope 就可以创建了两个具有范围的容器,而 AddScoped 方式注册的服务在不同范围内的生命周期是不一样的,而相同范围下的生命周期和 AddSingleton 是一致的。

技术图片
技术图片
interface ITransient { }
class Transient : ITransient { }
interface ISingleton { }
class Singleton : ISingleton { }
interface IScoped { }
class Scoped : IScoped { }
class Program
{
    static void Main(string[] args)
    {
        IServiceCollection services = new ServiceCollection();
        services = services.AddTransient<ITransient, Transient>();
        services = services.AddScoped<IScoped, Scoped>();
        services = services.AddSingleton<ISingleton, Singleton>();
        IServiceProvider serviceProvider = services.BuildServiceProvider();
         
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ITransient>(), serviceProvider.GetService<ITransient>()));
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IScoped>(), serviceProvider.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ISingleton>(), serviceProvider.GetService<ISingleton>()));

        IServiceProvider serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;
        IServiceProvider serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;

        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider1.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider2.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<ISingleton>(), serviceProvider2.GetService<ISingleton>()));

        /* False
         * True
         * True
         * True
         * False
         * True
         */
    }
}
技术图片
技术图片

IServiceCollection#

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

是一个集合,用来存放用户注册的服务元数据

ServiceDescriptor#

看上面的例子我们如何添加注入应该也能猜到 ServiceDescriptor 包含哪些属性了吧!至少包含一个接口类型、实现类型和生命周期,是的就是如此。

技术图片
技术图片
public class ServiceDescriptor
{
    public ServiceLifetime Lifetime { get; }
    public Type ServiceType { get; }
    public Type ImplementationType { get; }
    public object ImplementationInstance { get; }
    public Func<IServiceProvider, object> ImplementationFactory { get; }
}
技术图片
技术图片

在第一个代码块中,都是使用的是 IServiceCollection 如下签名拓展方法注册服务的,这里我把它称为“服务类型实例类型”(提供一个服务类型,一个实例类型)的注册方式,相应的服务类型和实例类型通过解析泛型参数传递给 ServiceDescriptor 的ServiceType、ImplementationInstance,值得注意的是,创建 ServiceDescriptor 并不会校验实例类型的可创建性(验证其是否是抽象类,接口)

技术图片
技术图片
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
    where TService : class
    where TImplementation : class, TService
{
    if (services == null)
    {
        throw new ArgumentNullException(nameof(services));
    }

    return services.AddTransient(typeof(TService), typeof(TImplementation));
}
技术图片
技术图片

此外,微软还提供了“服务实例”(提供一个服务类型,一个实例对象)以及“服务实例工厂”(提供一个服务类型,一个实例对象工厂)的注册方式,前者只供单例服务使用,使用起来也很简单

services.AddTransient<ITransient>(_=>new Transient());
services.AddSingleton<ISingleton>(new Singleton());

关于 ServiceDescriptor,还有一个要说的就是服务的生命周期了,使用 AddSingleton、AddScoped、AddTransient 三种方式注册的服务在 ServiceDescriptor 中的 LifeTime 属性分别对应下面这个枚举类型

技术图片
技术图片
public enum ServiceLifetime
{
    Singleton,
    Scoped,
    Transient
}
技术图片
技术图片

1、Transient:每次从容器 (IServiceProvider)中获取的时候都是一个新的实例

2、Singleton:每次从同根容器中(同根 IServiceProvider)获取的时候都是同一个实例

3、Scoped:每次从同一个容器中获取的实例是相同的、

关于服务的生命周期,如果还不清楚也没关系,因为接下来会不断的学习它

自定义创建容器和创建对象的过程#

在文章的开头就介绍了该注入框架的三个过程,注册服务->创建容器->创建对象,然而注册服务的步骤是非常简单的,将一个个类似 AddTransient、AddSingleton 的方法提供的泛型参数或者实参转换成一个 ServiceDescriptor 对象存储在 IServiceCollection 中,而创建容器和床对象是否也是这样简单呢?如果是,想必很容易写出下面的代码

技术图片
技术图片
public class MyServiceProvider : IServiceProvider
{
    private List<ServiceDescriptor> serviceDescriptors = new List<ServiceDescriptor>();
    private Dictionary<Type, object> SingletonServices = new Dictionary<Type, object>();
    public MyServiceProvider(IEnumerable<ServiceDescriptor>  serviceDescriptors)
    {
        this.serviceDescriptors.AddRange(serviceDescriptors);
    }
    public object GetService(Type serviceType)
    {
        var descriptor = serviceDescriptors.FirstOrDefault(t => t.ServiceType == serviceType);

        if(descriptor == null)
        {
            throw new Exception($"服务‘{serviceType.Name}’未注册");
        }
        else
        {
            switch (descriptor.Lifetime)
            {
                case ServiceLifetime.Singleton:
                    if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))
                    {
                        return obj;
                    }
                    else
                    {
                        var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);
                        SingletonServices.Add(descriptor.ServiceType, singletonObject);
                        return singletonObject;
                    }
                case ServiceLifetime.Scoped:
                    throw new NotSupportedException($"创建失败,暂时不支持 Scoped");
                case ServiceLifetime.Transient:
                    var transientObject = Activator.CreateInstance(descriptor.ImplementationType);
                    return transientObject;
                default:
                    throw new NotSupportedException("创建失败,不能识别的 LifeTime");
            }
        }
    }
}
public static class ServiceCollectionContainerBuilderExtensions
{public static MyServiceProvider BuildeMyServiceProvider(this IServiceCollection services)
    {
        return new MyServiceProvider(services);
    }
}
技术图片
技术图片

由于 Scoped 的特殊性,部分人写到这里就戛然而止了,然而还有一个问题,我们知道注册服务的时候可能采取多种方式,这里只给出了"服务实例类型"的情形,稍作修改

技术图片
技术图片
case ServiceLifetime.Singleton:
    if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))
    {
        return obj;
    }
    else
    {
        if(descriptor.ImplementationType != null)
        {
            var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);
            SingletonServices.Add(descriptor.ServiceType, singletonObject);
            return singletonObject;
        }
        else if(descriptor.ImplementationInstance != null)
        {
            SingletonServices.Add(descriptor.ServiceType, descriptor.ImplementationInstance);
            return descriptor.ImplementationInstance;
        }
        else if(descriptor.ImplementationFactory != null)
        {
            var singletonObject = descriptor.ImplementationFactory.Invoke(this);
            SingletonServices.Add(descriptor.ServiceType, singletonObject);
            return singletonObject;
        }
        else
        {
            throw new Exception("创建服务失败,无法找到实例类型或实例");
        }
    }
技术图片
技术图片

虽然这里只重写了 Singleton 方式,但是其他的也应如此,实际上可以一直这么写下去,但是作为 C# 开发者就显得有些不优雅,因为这是面向过程(或者说是基于对象)的开开发模式

此外,微软的注入是不支持属性注入的,但是别忘了,仍然是支持构造函数注入的,要不然这个注入那也太鸡助了吧!是的,按照上述的代码段我们可以继续写下去,在解析出实例类型的时候,我们找到它的构造函数,找到构造函数的所有参数,以同样的方式创建参数的实例,这是一个递归的过程,最后回调,仍然可以创建我们需要的对象,但是这一切如何健壮、优雅的实现呢?这就是学习源码原因所在吧!

微软是如何进一步处理元数据的?#

其实上面的代码最主要的问题就是创建容器和创建对象这两个过程过度耦合了,并且存在一个最大的问题,仔细想想每次创建对象的时候都要去翻一遍 ServiceDescriptor 判断它是以“服务实例类型”、“服务实例对象”、“服务实例对象工厂”中的哪种方式注册的,这样就进行了一些不必要的性能消耗,然而这个工作微软是在创建容器的时候完成的。跟随着创建容器的过程我们义无反顾的向源码走去!去哪?寻找微软和如何处理 ServiceDescriptor 的!

这里我们遇到的第一个拦路虎就是 ServiceProvider,我们创建的容器最终就是一个这样的类型,看看它是如何创建对象的?

技术图片
技术图片
public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback
{
    private readonly IServiceProviderEngine _engine;
    internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
    {
        //此处省略了一些代码
        switch (options.Mode)
        {
            case ServiceProviderMode.Dynamic:
                _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);
                break;
            //此处省略了一些代码
            default:
                throw new ArgumentOutOfRangeException(nameof(options.Mode));
        }
    }
    public object GetService(Type serviceType) => _engine.GetService(serviceType);
    public void Dispose() => _engine.Dispose();
}
技术图片
技术图片

这里我们知道,最终提供对象并非 ServiceProvide,而是它的一个字段  _engine 类型为 IServiceProviderEngine,在 switch 语句中,我只贴出了 Dynamic 这个分支的代码,因为该枚举变量 options 的默认值总是 Dynamic,这里我们仅仅需要知道 ServiceProvider 中提供对象的核心是一个 ServiceProviderEngine,并且它的默认实例是一个 DynamicServiceProviderEngine,因为这次探险我们是去分析微软是如何处理元数据的。这一切肯定在 DynamicServiceProviderEngine 创建过程中完成,所以我们只管寻找它的构造函数,终于,我们在父类 ServiceProviderEngine 找到了!

技术图片
技术图片
internal abstract class ServiceProviderEngine : IServiceProviderEngine, IServiceScopeFactory
{
    internal CallSiteFactory CallSiteFactory { get; }
    protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngineCallback callback)
    {
        //省略了一些代码
        CallSiteFactory = new CallSiteFactory(serviceDescriptors);
        CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
        CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
    }
}
技术图片
技术图片

CallSiteFactory#

这里只贴出了该类中三个字段,然而该类型也只有该三个字段,如果这三个字段具体的作用理解了,那么对于微软如何处理元数据这一问题也就知道答案了

技术图片
技术图片
internal class CallSiteFactory
{
    private readonly List<ServiceDescriptor> _descriptors;
    private readonly Dictionary<Type, IServiceCallSite> _callSiteCache = new Dictionary<Type, IServiceCallSite>();
    private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new Dictionary<Type, ServiceDescriptorCacheItem>();

    private struct ServiceDescriptorCacheItem
    {
        private ServiceDescriptor _item;
        private List<ServiceDescriptor> _items;
        //省略了一些代码
    }
}
internal interface IServiceCallSite
{
    Type ServiceType { get; }
    Type ImplementationType { get; }
}
技术图片
技术图片

 第一个字段 _descriptors  是一个元数据集合,我们注册的服务都在这里,然后我们看第三个字段 _descriptorLookup,因为注册服务的时候第一没有验证实例类型的有效性(接口,抽象类等),此外我们可以针对同一个服务进行多册注册,对于多次注册的服务微软又是如何确定创建的对象呢?这对这些问题,微软设计了一个类概括了具体一个服务的所有注册的实例类型 ServiceDescriptorCacheItem,具体针对一个服务,第一次注册的元数据存在 _item 中,后续该服务的所有元数据都存在 _items,而默认的总是认同最后一个元数据。最后最难理解的就是 _callSiteCache 这个字段了,简单的说,它的值 IServiceCallSite 是创建服务实例的依据,包含了服务类型和实例类型。我们知道从 _descriptorLookup 获取的是确定的实例类型,然而这个实例类型的构造函数中的类型如何创建呢,这些都在 IServiceCallSite 中体现,既然说 IServiceCallSite 是创建实例的依据,通过观察这个接口的定义发现也并没有和生命周期相关的属性,有点失望!

我们回到创建 ServiceProviderEngine 创建 CallSiteFactory 的那一行代码,在创建CallSiteFactory 完成后,它调用了 Add 方法添加了两个键值对。第一行代码的键是啥? IServiceProvider,是的微软默认的允许 IServiceProvider 提供自己!

CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());

可以看到 Add 添加的键值对是存储在 _callSiteCache 中的

public void Add(Type type, IServiceCallSite serviceCallSite)
{
    _callSiteCache[type] = serviceCallSite;
}

接着我们观察 ServiceProviderCallSite、ServiceScopeFactoryCallSite 这两个类型,出了增加了两个不认识的类型,并没有其他收获

技术图片
技术图片
internal class ServiceProviderCallSite : IServiceCallSite
{
    public Type ServiceType { get; } = typeof(IServiceProvider);
    public Type ImplementationType { get; } = typeof(ServiceProvider);
}
internal class ServiceScopeFactoryCallSite : IServiceCallSite
{
    public Type ServiceType { get; } = typeof(IServiceScopeFactory);
    public Type ImplementationType { get; } = typeof(ServiceProviderEngine);
}
技术图片
技术图片

关于注入的一些猜想#

从上述的学习我们有了一个较为意外的收获,IServiceProvider 是可以提供自己的,这不得不使我们猜想,IServiceProvider 具有怎样的生命周期?如果不断的用一个 IServiceProvider 创建一个新的,如此下去,又是如何?

技术图片
技术图片
static void Main(string[] args)
{
    IServiceCollection services = new ServiceCollection();
    var serviceProvider = services.BuildServiceProvider();

    Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IServiceProvider>(), serviceProvider.GetService<IServiceProvider>()));

    var serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;
    var serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;

    Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IServiceProvider>(), serviceProvider2.GetService<IServiceProvider>()));

    var serviceProvider3 = serviceProvider.GetService<IServiceProvider>();
    var serviceProvider4 = serviceProvider.GetService<IServiceProvider>();

    var serviceProvider3_1 = serviceProvider3.GetService<IServiceProvider>();
    var serviceProvider4_1 = serviceProvider4.GetService<IServiceProvider>();

    Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider4));
    Console.WriteLine(ReferenceEquals(serviceProvider3_1, serviceProvider4_1));
    Console.WriteLine(ReferenceEquals(serviceProvider3, serviceProvider3_1));

    Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider));

    /* True
     * False
     * True
     * True
     * True
     * False
     */
}
技术图片
技术图片

这里对 CreateScope 我们仅需要知道它创建的是一个具有限定范围的容器即可,我们根据第一个输出结果为 True 和第二个输出结果为 False,从这点看 IServiceProvider 的生命周期和 Scoped 的定义一致,但是由于 IServiceProvider 的特殊性,它可以一直不断的创建自己,并且他们都是同一个对象,但是和最初的 ServiceProvider 都不一样。这让我们又怀疑 IServiceProvider 究竟是不是 Scoped。

小结

这一节主要介绍了服务的三种生命周期,以及服务是如何注册到元数据的,并且在创建容器的过程中,我们知道了微软是如何进一步处理元数据的,以及创建实例对象的最终依据是 IServiceCallSite,但是想要真正的搞明白 IServiceCallSite 还必须详细的了解创建容器和创建实例的过程。

【转:http://www.cnblogs.com/cheesebar/p/7675214.html】

深入理解net core中的依赖注入、Singleton、Scoped、Transient(一)

标签:uil   根据   tle   解析   equal   一个起点   VID   invoke   eal   

原文地址:https://www.cnblogs.com/lonelyxmas/p/12961791.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!