ASP.NET Dependency injection in core: uncover the implementation of ServiceProvider [interpret ServiceCallSite]

Original text: ASP.NET Dependency injection in core (5): uncover the implementation of ServiceProvider [interpret ServiceCallSite]

adopt Previous We should have a general understanding of the overall design of the implementation in ServiceProvider, but we deliberately avoid an important topic, that is, which way the service instance is finally provided. The way in which ServiceProvider ultimately provides the service instance we need depends on which ServiceCallSite is finally selected, and the ServiceDescriptor used for service registration determines the type of ServiceCallSite. We divide many different types of ServiceCallSite into two groups, one is used to create the final service instance, the other is related to the management of life cycle.

1, ServiceCallSite for service creation

There are three main ways to create service instances, which correspond to the following three read-only attributes of ServiceDescriptor. Simply put, if the ImplementationInstance property returns a specific object, it will act directly as the provided service instance. If the property ImplementationFactory returns a specific delegate object, the delegate will act as the factory providing the service instance. In addition, ServiceProvider will use the true service type returned by the ImplementationType property to locate one of the best constructors to create the final provided service instance.

   1: public class ServiceDescriptor
   2: {
   3:     public Type                               ImplementationType {  get; }
   4:     public object                             ImplementationInstance {  get; }
   5:     public Func<IServiceProvider, object>     ImplementationFactory {  get; }     
   6: }

The three different creation methods of service instance are finally completed by three corresponding ServiceCallSite types. We name them InstanceCallSite, FactoryCallSite and ConstructorCallSite respectively. As shown in the following code snippet, the implementation of the first two service call sites (instance call site and factorycall site) is very simple, so we will not introduce them here.

   1: internal class InstanceCallSite : IServiceCallSite
   2: {
   3: public object Instance { get; private set; }
   4: 
   5:     public InstanceCallSite(object instance)
   6:     {
   7:         this.Instance = instance;
   8:     }
   9:     public Expression Build(Expression provider)
  10:     {
  11:         return Expression.Constant(this.Instance);
  12:     }
  13:     public object Invoke(ServiceProvider provider)
  14:     {
  15:         return Instance;
  16:     }
  17: }
  18: 
  19: internal class FactoryCallSite : IServiceCallSite
  20: {
  21:     public Func<IServiceProvider, object> Factory { get; private set; }
  22:     public FactoryCallSite(Func<IServiceProvider, object> factory)
  23:     {
  24:         this.Factory = factory;
  25:     }
  26:     public Expression Build(Expression provider)
  27:     {
  28:         Expression<Func<IServiceProvider, object>> factory = p => this.Factory(p);
  29:         return Expression.Invoke(factory, provider);
  30:     }
  31:     public object Invoke(ServiceProvider provider)
  32:     {
  33:         return this.Factory(provider);
  34:     }
  35: }

The ConstructorCallSite that creates the service instance by executing the specified constructor is a little more complex. As shown in the following code snippet, when creating a ConstructorCallSite object, we need to specify not only a ConstructorInfo object representing the constructor, but also a set of ServiceCallSite used to initialize the corresponding parameter list.

   1: internal class ConstructorCallSite : IServiceCallSite
   2: {
   3:     public ConstructorInfo ConstructorInfo { get; private set; }
   4:     public IServiceCallSite[] Parameters { get; private set; }
   5: 
   6:     public ConstructorCallSite(ConstructorInfo constructorInfo, IServiceCallSite[] parameters)
   7:     {
   8:         this.ConstructorInfo = constructorInfo;
   9:         this.Parameters = parameters;
  10:     }
  11: 
  12:     public Expression Build(Expression provider)
  13:     {
  14:         ParameterInfo[] parameters = this.ConstructorInfo.GetParameters();
  15:         return Expression.New(this.ConstructorInfo, this.Parameters.Select((p, index) => Expression.Convert(p.Build(provider),
  16:             parameters[index].ParameterType)).ToArray());
  17:     }
  18: 
  19:     public object Invoke(ServiceProvider provider)
  20:     {
  21:         return this.ConstructorInfo.Invoke(this.Parameters.Select(p => p.Invoke(provider)).ToArray());
  22:     }
  23: }

Although the logic for ConstructorCallSite to create its own service instance is simple, how to create the ConstructorCallSite object itself is relatively troublesome, because it involves how to select a final constructor. We have specifically introduced this problem above, and summed up two basic strategies for choosing constructors:

  • ServiceProvider can provide all the parameters of the constructor.
  • The parameter type collection of the target constructor is the super of all valid constructor parameter type collections.

We define the creation of ConstructorCallSite in the CreateConstructorCallSite method of the Service class, which has two additional auxiliary methods, GetConstructor and GetParameterCallSites. The former is used to select the correct constructor, and the latter is used to create the ServiceCallSite list used to initialize parameters for the specified constructor.

   1: internal class Service : IService
   2: {
   3:     private ConstructorCallSite CreateConstructorCallSite(ServiceProvider provider, ISet<Type> callSiteChain)
   4:     {
   5:         ConstructorInfo constructor = this.GetConstructor(provider, callSiteChain);
   6:         if (null == constructor)
   7:         {
   8:             throw new InvalidOperationException("No avaliable constructor");
   9:         }
  10:         return new ConstructorCallSite(constructor, constructor.GetParameters().Select(p => provider.GetServiceCallSite(p.ParameterType, callSiteChain)).ToArray());                                             
  11: }
  12: 
  13:     private ConstructorInfo GetConstructor(ServiceProvider provider, ISet<Type> callSiteChain)
  14:     {
  15:         ConstructorInfo[] constructors = this.ServiceDescriptor.ImplementationType.GetConstructors()
  16:             .Where(c => (null != this.GetParameterCallSites(c, provider, callSiteChain))).ToArray();
  17: 
  18:         Type[] allParameterTypes = constructors.SelectMany(
  19:             c => c.GetParameters().Select(p => p.ParameterType)).Distinct().ToArray();
  20: 
  21:         return constructors.FirstOrDefault(
  22:             c => new HashSet<Type>(c.GetParameters().Select(p => p.ParameterType)).IsSupersetOf(allParameterTypes));
  23:     }
  24: 
  25:     private IServiceCallSite[] GetParameterCallSites(ConstructorInfo constructor,ServiceProvider provider,ISet<Type> callSiteChain)
  26:     {
  27:         ParameterInfo[] parameters = constructor.GetParameters();
  28:         IServiceCallSite[] serviceCallSites = new IServiceCallSite[parameters.Length];
  29: 
  30:         for (int index = 0; index < serviceCallSites.Length; index++)
  31:         {
  32:             ParameterInfo parameter = parameters[index];
  33:             IServiceCallSite serviceCallSite = provider.GetServiceCallSite(
  34:                 parameter.ParameterType, callSiteChain);
  35:             if (null == serviceCallSite && parameter.HasDefaultValue)
  36:             {
  37:                 serviceCallSite = new InstanceCallSite(parameter.DefaultValue);
  38:             }
  39:             if (null == serviceCallSite)
  40:             {
  41:                 return null;
  42:             }
  43:             serviceCallSites[index] = serviceCallSite;
  44:         }
  45:         return serviceCallSites;
  46:     }
  47: / / other members
  48: }


2, ServiceCallSite for managing the lifecycle

The final mode of service instance provision is also related to the life cycle management mode used in service registration. Three different life cycle management modes (Transient, Scoped and Singleton) correspond to three different types of ServiceCallSite. We named them transincallsite, ScopedCallSite and Singleton callsite respectively.

For TransientCallSite, the way it provides service instances is very straightforward, that is to create a new object directly. In addition, we need to consider the issue of service recycling, that is, if the type corresponding to the service instance implements the IDisposable interface, it needs to be added to the TransientDisposableServices property of the ServiceProvider. TransientCallSite has the following definition, and the read-only property ServiceCallSite represents the ServiceCallSite that is actually used to create the service instance.

   1: internal class TransientCallSite : IServiceCallSite
   2: {
   3:     public IServiceCallSite ServiceCallSite { get; private set; }
   4:     public TransientCallSite(IServiceCallSite serviceCallSite)
   5:     {
   6:         this.ServiceCallSite = serviceCallSite;
   7:     }
   8: 
   9:     public  Expression Build(Expression provider)
  10:     {
  11:         return Expression.Call(provider, "CaptureDisposable", null, this.ServiceCallSite.Build(provider));
  12:     }
  13: 
  14:     public  object Invoke(ServiceProvider provider)
  15:     {
  16:         return provider.CaptureDisposable(this.ServiceCallSite.Invoke(provider));
  17:     }
  18: }
  19: 
  20: internal class ServiceProvider : IServiceProvider, IDisposable
  21: {
  22:    
  23:     public object CaptureDisposable(object instance)
  24:     {
  25:         IDisposable disposable = instance as IDisposable;
  26:         if (null != disposable)
  27:         {
  28:             this.TransientDisposableServices.Add(disposable);
  29:         }
  30:         return instance;
  31:     }
  32: / / other members
  33: }

When providing a Service instance, ScopedCallSite needs to consider whether the provided Service instance already exists in the ResolvedServices property of the current ServiceProvider. If it already exists, it does not need to be created repeatedly. The newly created Service instance needs to be added to the ResolvedServices property of the current ServiceProvider. ScopedCallSite is defined as follows. The ServiceCallSite property still represents the ServiceCallSite that is actually used to create the Service strength. The Service property is used to obtain the Service instance saved in the ResolvedServices property of the current ServiceProvider.

   1: internal class ScopedCallSite : IServiceCallSite
   2: {
   3:     public IService Service { get; private set; }
   4:     public IServiceCallSite ServiceCallSite { get; private set; }
   5: 
   6:     public ScopedCallSite(IService service, IServiceCallSite serviceCallSite)
   7:     {
   8:         this.Service = service;
   9:         this.ServiceCallSite = serviceCallSite;
  10:     }
  11: 
  12:     public virtual Expression Build(Expression provider)
  13:     {
  14:         var service = Expression.Constant(this.Service);
  15:         var instance = Expression.Variable(typeof(object), "instance");
  16:         var resolvedServices = Expression.Property(provider, "ResolvedServices");
  17:         var tryGetValue = Expression.Call(resolvedServices, "TryGetValue", null, service, instance);
  18:         var index = Expression.MakeIndex(resolvedServices, typeof(ConcurrentDictionary<IService, object>).GetProperty("Item"), new Expression[] { service});
  19:         var assign = Expression.Assign(index, this.ServiceCallSite.Build(provider));
  20: 
  21:         return Expression.Block(typeof(object),new[] { instance },Expression.IfThen(Expression.Not(tryGetValue),assign),index);
  22:     }
  23: 
  24:     public virtual object Invoke(ServiceProvider provider)
  25:     {
  26:         object instance;
  27:         return provider.ResolvedServices.TryGetValue(this.Service, out instance)
  28:             ? instance
  29:             : provider.ResolvedServices[this.Service] = this.ServiceCallSite.Invoke(provider);
  30:     }
  31: }

In fact, singleton and Scope are essentially the same. They represent the "Singleton" mode in a certain ServiceScope. The difference between them is that the former's ServiceScope is for the root ServiceProvider, and the latter is for the current ServiceProvider. So SingletonCallSite is a derived class of ScopedCallSite, which is defined as follows.

   1: internal class SingletonCallSite : ScopedCallSite
   2: {
   3:     public SingletonCallSite(IService service, IServiceCallSite serviceCallSite) :
   4:     base(service, serviceCallSite)
   5:     { }
   6: 
   7:     public override Expression Build(Expression provider)
   8:     {
   9:         return base.Build(Expression.Property(provider, "Root"));
  10:     }
  11: 
  12:     public override object Invoke(ServiceProvider provider)
  13:     {
  14:         return base.Invoke(provider.Root);
  15:     }
  16: }


3, Create ServiceCallSite

The creation of ServiceCallSite is embodied in the CreateServiceSite method of IService interface. How is this method implemented in our Service class? As shown in the following code snippet, the ServiceCallSite that is actually used to create a Service instance is created according to the current ServiceDescriptor, and then the matching ServiceCallSite is created according to the life cycle management pattern.

   1: internal class Service : IService
   2: {
   3:     public IServiceCallSite CreateCallSite(ServiceProvider provider, ISet<Type> callSiteChain)
   4:     {
   5:         IServiceCallSite serviceCallSite =
   6:             (null != this.ServiceDescriptor.ImplementationInstance)
   7:             ? new InstanceCallSite(this.ServiceDescriptor.ImplementationInstance)
   8:             : null;
   9: 
  10:         serviceCallSite = serviceCallSite??
  11:             ((null != this.ServiceDescriptor.ImplementationFactory)
  12:             ? new FactoryCallSite(this.ServiceDescriptor.ImplementationFactory)
  13:             : null);
  14: 
  15:         serviceCallSite = serviceCallSite ?? this.CreateConstructorCallSite(provider, callSiteChain);
  16: 
  17:         switch (this.Lifetime)
  18:         {
  19:             case ServiceLifetime.Transient: return new TransientCallSite(serviceCallSite);
  20:             case ServiceLifetime.Scoped: return new ScopedCallSite(this, serviceCallSite);
  21:             default: return new SingletonCallSite(this, serviceCallSite);
  22:         }
  23:     }
  24: / / other members 
  25: }

ASP.NET Dependency injection in core (1): inversion of control (IoC)
ASP.NET Dependency injection in core (2): dependency injection (DI)
ASP.NET Dependency injection in core (3): service registration and extraction
ASP.NET Dependency injection in core (4): constructor selection and lifecycle management
ASP.NET Dependency injection in core (5): service provider implementation disclosure [overall design]
ASP.NET Dependency injection in core (5): uncover the implementation of serviceprovider [interpret ServiceCallSite]
ASP.NET Dependency injection in core (5): service provider implementation uncovering [add missing details]





Posted on Thu, 04 Jun 2020 23:06:13 -0400 by leon_zilber