[ASP.NET Core 3框架揭秘] 依赖注入:一个Mini版的依赖注入框架
在前面的章节中,我们从纯理论的角度对依赖注入进行了深入论述,我们接下来会对.NET Core依赖注入框架进行单独介绍。为了让读者朋友能够更好地理解.NET Core依赖注入框架的设计与实现,我们按照类似的原理创建了一个简易版本的依赖注入框架,也就是我们在前面多次提及的Cat。
源代码下载
普通服务的注册与消费
泛型服务的注册与消费
多服务实例的提供
服务实例的生命周期
一、编程体验
虽然我们对这个名为Cat的依赖注入框架进行了最大限度的简化,但是与.NET Core框架内部使用的真实依赖注入框架相比,Cat不仅采用了一致的设计,而且几乎具备了后者所有的功能特性。为了让大家对Cat具有一个感官的认识,我们先来演示一下如何利用它来提供我们所需的服务实例。
作为依赖注入容器的Cat对象不仅仅作为服务实例的提供者,它同时还需要维护着服务实例的生命周期。Cat提供了三种生命周期模式,如果要了解它们之间的差异,就必须对多个Cat之间的层次关系有充分的认识。一个代表依赖注入容器的Cat对象用来创建其他的Cat对象,后者视前者为“父容器”,所以多个Cat对象通过其“父子关系”维系一个树形层次化结构。不过这仅仅是一个逻辑结构而已,实际上每个Cat对象只会按照下图所示的方式引用整棵树的根。
3-6_thumb2
在了解了多个Cat对象之间的关系之后,对于三种预定义的生命周期模式就很好理解了。如下所示的Lifetime枚举代表着三种生命周期模式,其中Transient代表容器针对每次服务请求都会创建一个新的服务实例,而Self则是将提供服务实例保存在当前容器中,它代表针对某个容器范围内的单例模式,Root则是将每个容器提供的服务实例统一存放到根容器中,所以该模式能够在多个“同根”容器范围内确保提供的服务是单例的。
public enum Lifetime
{
Root,
Self,
Transient
}
代表依赖注入容器的Cat对象之所以能够为我们提供所需服务实例,其根本前提是相应的服务注册在此之前已经添加到容器之中。服务总是针对服务类型(接口、抽象类或者具体类型)进行注册,Cat通过定义的扩展方法提供了如下三种注册方式。除了直接提供服务实例的形式外(默认采用Root模式),我们在注册服务的时候必须指定一个具体的生命周期模式。
指定具体的实现类型。
提供一个服务实例。
指定一个创建服务实例的工厂。
我们定义了如下的接口和对应的实现类型来演示针对Cat的服务注册。其中Foo、Bar、Baz和Gux分别实现了对应的接口IFoo、IBar、IBaz和IGux,其中Gux类型上标注了一个MapToAttribute特性注册了与对应接口IGux之间的映射。为了反映Cat对服务实例生命周期的控制,我们让它们派生于同一个基类Base。Base实现了IDisposable接口,我们在其构造函数和实现的Dispose方法中输出相应的文本以确定对应的实例何时被创建和释放。我们还定义了一个泛型的接口IFoobar和对应的实现类Foobar来演示Cat针对泛型服务实例的提供。
public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IGux {}
public interface IFoobar {}
public class Base : IDisposable
{
public Base() => Console.WriteLine($"Instance of {GetType().Name} is created.");
public void Dispose() => Console.WriteLine($"Instance of {GetType().Name} is disposed.");
}
public class Foo : Base, IFoo{ }
public class Bar : Base, IBar{ }
public class Baz : Base, IBaz{ }
[MapTo(typeof(IGux), Lifetime.Root)]
public class Gux : Base, IGux { }
public class Foobar: IFoobar
{
public IFoo Foo { get; }
public IBar Bar { get; }
public Foobar(IFoo foo, IBar bar)
{
Foo = foo;
Bar = bar;
}
}
在如下所示的代码片段中我们创建了一个Cat对象并采用上面提到的方式针对接口IFoo、IBar和IBaz注册了对应的服务,它们采用的生命周期模式分别为Transient、Self和Root。然后我们还调用了另一个将当前入口程序集作为参数的Register方法,该方法会解析指定程序集中标注了MapToAttribute特性的类型并作相应的服务注册,对于我们演示的程序来,该方法会完成针对IGux/Gux类型的服务注册。接下来我们利用Cat对象创建了它的两个子容器,并调用子容器的GetService方法提供相应的服务实例。
class Program
{
static void Main()
{
var root = new Cat()
.Register(Lifetime.Transient)
.Register(_=> new Bar(), Lifetime.Self)
.Register(Lifetime.Root)
.Register(Assembly.GetEntryAssembly());
var cat1 = root.CreateChild();
var cat2 = root.CreateChild();
void GetServices(Cat cat)
{
cat.GetService();
cat.GetService();
}
GetServices(cat1);
GetServices(cat1);
GetServices(cat1);
GetServices(cat1);
Console.WriteLine();
GetServices(cat2);
GetServices(cat2);
GetServices(cat2);
GetServices(cat2);
}
}
上面的程序运行之后会在控制台上输出如图3-7所示的结果,输出的内容不仅表明Cat能够根据添加的服务注册提供对应类型的服务实例,还体现了它对生命周期的控制。由于服务IFoo被注册为Transient服务,所以Cat针对该接口的服务提供四次请求都会创建一个全新的Foo对象。IBar服务的生命周期模式为Self,如果我们利用同一个Cat对象来提供对应的服务实例,该Cat对象只会创建一个Bar对象,所以整个过程中会创建两个Bar对象。IBaz和IGux服务采用Root生命周期,所以具有同根的两个Cat对象提供的总是同一个Baz/Gux对象,后者只会被创建一次。
3-7_thumb2
除了提供类似于IFoo、IBar和IBaz这样非泛型的服务实例之外,如果具有针对泛型定义(Generic Definition)的服务注册,Cat同样也能提供泛型服务实例。如下面的代码片段所示,在为创建的Cat对象添加了针对IFoo和IBar接口的服务注册之后,我们调用Register方法注册了针对泛型定义IFoobar<,>的服务注册,具体的实现类型为Foobar<,>。当我们利用Cat对象提供一个类型为IFoobar的服务实例的时候,它会创建并返回一个Foobar对象。
public class Program
{
public static void Main()
{
var cat = new Cat()
.Register(Lifetime.Transient)
.Register(Lifetime.Transient)
.Register(typeof(IFoobar<,>), typeof(Foobar<,>), Lifetime.Transient);
var foobar = (Foobar)cat.GetService>();
Debug.Assert(foobar.Foo is Foo);
Debug.Assert(foobar.Bar is Bar);
}
}
当我们在进行服务注册的时候,可以为同一个类型添加多个服务注册。虽然添加的所有服务注册均是有效的,不过由于扩展方法GetService总是返回一个唯一的服务实例,我们对该方法采用了“后来居上”的策略,即总是采用最近添加的服务注册来创建服务实例。如果我们调用另一个扩展方法GetServices,它将利用返回根据所有服务注册提供的服务实例。
如下面的代码片段所示,我们为创建的Cat对象添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们最后将Base作为泛型参数调用了GetServices 方法,该方法会返回包含三个Base对象的集合,集合元素的类型分别为Foo、Bar和Baz。
public class Program
{
public static void Main()
{
var services = new Cat()
.Register (Lifetime.Transient)
.Register (Lifetime.Transient)
.Register (Lifetime.Transient)
.GetServices ();
Debug.Assert(services.OfType().Any());
Debug.Assert(services.OfType().Any());
Debug.Assert(services.OfType().Any());
}
}
如果提供的服务实例实现了IDisposable接口,我们应该在适当的时候调用其Dispose方法释放该服务实例。由于服务实例的生命周期完全由作为依赖注入容器的Cat对象来管理,那么通过调用Dispose方法来释放服务实例自然也应该由它来负责。Cat针对提供服务实例的释放策略取决于采用的生命周期模式,具体的策略如下:
Transient和Self:所有实现了IDisposable接口的服务实例会被当前Cat对象保存起来,当Cat对象自身的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
Root:由于服务实例保存在作为根容器的Cat对象上,所以当这个Cat对象的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
上述的释放策略可以通过如下的演示实例来印证。我们在如下的代码片段中创建了一个Cat对象,并添加了相应的服务注册。我们接下来调用了CreateChild方法创建代表子容器的Cat对象,并用它提供了四个注册服务对应的实例。
class Program
{
static void Main()
{
using (var root = new Cat()
.Register(Lifetime.Transient)
.Register(_ => new Bar(), Lifetime.Self)
.Register(Lifetime.Root)
.Register(Assembly.GetEntryAssembly()))
{
using (var cat = root.CreateChild())
{
cat.GetService();
cat.GetService();
cat.GetService();
cat.GetService();
Console.WriteLine("Child cat is disposed.");
}
Console.WriteLine("Root cat is disposed.");
}
}
}
由于两个Cat对象的创建都是在using块中进行的,所以它们的Dispose方法都会在using块结束的地方被调用。为了确定方法被调用的时机,我们特意在控制台上打印了相应的文字。该程序运行之后会在控制台上输出如下图所示的结果,我们可以看到当作为子容器的Cat对象的Dispose方法被调用的时候,由它提供的两个生命周期模式分别为Transient和Self的两个服务实例(Foo和Bar)被正常释放了。至于生命周期模式为Root的服务实例Baz和Gux,它的Dispose方法会延迟到作为根容器的Cat对象的Dispose方法被调用的时候。
3-8_thumb2
二、设计与实现
在完成针对Cat的编程体验之后,我们来聊聊这个依赖注入容器的设计原理和具体实现。由于作为依赖注入容器的Cat对象总是利用预先添加的服务注册来提供对应的服务实例,所以服务注册至关重要。如下所示的就是表示服务注册的ServiceRegistry的定义,它具有三个核心属性(ServiceType、Lifetime和Factory),分别代表服务类型、生命周期模式和用来创建服务实例的工厂。最终用来创建服务实例的工厂体现为一个类型为Func的委托对象,它的两个输入分别代表当前使用的Cat对象以及提供服务类型的泛型参数,如果提供的服务类型并不是一个泛型类型,这个参数被会指定为一个空的数组。
public class ServiceRegistry
{
public Type ServiceType { get; }
public Lifetime Lifetime { get; }
public Func actory { get; }
internal ServiceRegistry Next { get; set; }
public ServiceRegistry(Type serviceType, Lifetime lifetime, Func factory)
{
ServiceType = serviceType;
Lifetime = lifetime;
Factory = factory;
}
internal IEnumerable AsEnumerable()
{
var list = new List();
for (var self = this; self!=null; self= self.Next)
{
list.Add(self);
}
return list;
}
}
我们将针对同一个服务类型(ServiceType属性相同)的多个ServiceRegistry组成一个链表,作为相邻节点的两个ServiceRegistry对象通过Next属性关联起来。我们为ServiceRegistry定义了一个AsEnumerable方法使它返回由当前以及后续节点组成的ServiceRegistry集合。如果当前ServiceRegistry为链表头,那么这个方法会返回链表上的所有ServiceRegistry对象。下图体现了服务注册核心三要素和链表结构。
3-9_thumb2
在了解了表示服务注册的ServiceRegistry之后,我们来着重介绍表示依赖注入容器的Cat类型。如下面的代码片段所示,Cat同时实现了IServiceProvider和IDisposable接口,定义在前者中的GetService方法用于提供服务实例。作为根容器的Cat对象通过公共构造函数创建,另一个内部构造函数则用来创建作为子容器的Cat对象,指定的Cat对象将作为父容器。
public class Cat : IServiceProvider, IDisposable
{
internal readonly Cat _root;
internal readonly ConcurrentDictionary _registries;
private readonly ConcurrentDictionary _services;
private readonly ConcurrentBag _disposables;
private volatile bool _disposed;
public Cat()
{
_registries = new ConcurrentDictionary();
_root = this;
_services = new ConcurrentDictionary();
_disposables = new ConcurrentBag();
}
internal Cat(Cat parent)
{
_root = parent._root;
_registries = _root._registries;
_services = new ConcurrentDictionary();
_disposables = new ConcurrentBag();
}
private void EnsureNotDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException("Cat");
}
}
...
}
作为根容器的Cat对象通过_root字段表示。_registries字段返回的ConcurrentDictionary对象用来存储所有添加的服务注册,该字典对象的Key和Value分别表示服务类型和ServiceRegistry链表,下图体现这一映射关系。由于需要负责完成对提供服务实例的释放工作,所以我们需要将实现了IDisposable接口的服务实例保存在通过_disposables字段表示的集合中。
3-10_thumb2
由当前Cat对象提供的非Transient服务实例保存在由_services字段表示的一个ConcurrentDictionary对象上,该字典对象的键类型为如下所示的Key,它相当于是创建服务实例所使用的ServiceRegistry对象和泛型参数类型数组的组合。
internal class Key : IEquatable
{
public ServiceRegistry Registry { get; }
public Type[] GenericArguments { get; }
public Key(ServiceRegistry registry, Type[] genericArguments)
{
Registry = registry;
GenericArguments = genericArguments;
}
public bool Equals(Key other)
{
if (Registry != other.Registry)
{
return false;
}
if (GenericArguments.Length != other.GenericArguments.Length)
{
return false;
}
for (int index = 0; index < GenericArguments.Length; index++)
{
if (GenericArguments[index] != other.GenericArguments[index])
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
var hashCode = Registry.GetHashCode();
for (int index = 0; index < GenericArguments.Length; index++)
{
hashCode ^= GenericArguments[index].GetHashCode();
}
return hashCode;
}
public override bool Equals(object obj) => obj is Key key ? Equals(key) : false;
}
虽然我们为Cat定义了若干扩展方法来提供多种不同的服务注册,但是这些方法最终都会调用如下这个Register方法,该方法会将提供的ServiceRegistry添加到_registries字段表示的字典对象中。值得注意的是,不论我们是调用哪个Cat对象的Register方法,指定的ServiceRegistry都会被添加到作为根容器的Cat对象上。
public class Cat : IServiceProvider, IDisposable
{
public Cat Register(ServiceRegistry registry)
{
EnsureNotDisposed();
if (_registries.TryGetValue(registry.ServiceType, out var existing))
{
_registries[registry.ServiceType] = registry;
registry.Next = existing;
}
else
{
_registries[registry.ServiceType] = registry;
}
return this;
}
...
}
用来提供服务实例的核心操作实现在如下这个GetServiceCore方法中。如下面的代码片段所示,我们在调用该方法的时候需要指定对应的ServiceRegistry对象的服务类型的泛型参数。当该方法被执行的时候,对于Transient的生命周期模式,它会直接利用ServiceRegistry提供的工厂来创建服务实例。如果服务实例的类型实现了IDisposable接口,该对象会被添加到_disposables字段表示的待释放服务实例列表中。对于Root和Self生命周期模式,该方法会先根据提供的ServiceRegistry判断是否对应的服务实例已经存在,存在的服务实例会直接返回。
public class Cat : IServiceProvider, IDisposable
{
private object GetServiceCore(ServiceRegistry registry, Type[] genericArguments)
{
var key = new Key(registry, genericArguments);
var serviceType = registry.ServiceType;
switch (registry.Lifetime)
{
case Lifetime.Root: return GetOrCreate(_root._services, _root._disposables);
case Lifetime.Self: return GetOrCreate(_services, _disposables);
default:
{
var service = registry.Factory(this, genericArguments);
if (service is IDisposable disposable && disposable != this)
{
_disposables.Add(disposable);
}
return service;
}
}
object GetOrCreate(ConcurrentDictionary services, ConcurrentBag disposables)
{
if (services.TryGetValue(key, out var service))
{
return service;
}
service = registry.Factory(this, genericArguments);
services[key] = service;
if (service is IDisposable disposable)
{
disposables.Add(disposable);
}
return service;
}
}
}
GetServiceCore方法只有在指定ServiceRegistry对应的服务实例不存在的情况下才会利用提供的工厂来创建服务实例,创建的服务实例会根据生命周期模式保存到作为根容器的Cat对象或者当前Cat对象上。如果提供的服务实例实现了IDisposable接口,在采用Root生命周期模式下会被保存到作为根容器的Cat对象的待释放列表中。如果生命周期模式为Self,它会被添加到当前Cat对象的待释放列表中。
在实现的GetService方法中,Cat会根据指定的服务类型找到对应的ServiceRegistry对象,并最终调用GetServiceCore方法来提供对应的服务实例。GetService方法还会解决一些特殊服务的提供问题,比如若服务类型为Cat或者IServiceProvider,该方法返回的就是它自己。如果服务类型为IEnumerable,GetService方法会根据泛型参数类型T找到所有的ServiceRegistry并利用它们来创建对应的服务实例,最终返回的是由这些服务实例组成的集合。除了这些,针对泛型服务实例的提供也是在这个方法中解决的。
public class Cat : IServiceProvider, IDisposable
{
public object GetService(Type serviceType)
{
EnsureNotDisposed();
if (serviceType == typeof(Cat) || serviceType == typeof(IServiceProvider))
{
return this;
}
ServiceRegistry registry;
//IEnumerable
if (serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
var elementType = serv