ASP.NET Dependency injection in core: service provider implementation uncovering [add missing details]

Original text: ASP.NET Dependency injection in core (5): service provider implementation uncovering [add missing details]

So far, the service provider we defined has implemented the basic service provision and recycling functions, but some necessary details are still missing. These features include how to provide a ServiceProvider object for IServiceProvider interface, how to create ServiceScope, and how to provide a collection of service instances.

1, Provide a ServiceProvider object

We know that when the Service type is specified as IServiceProvider interface and the GetService method of ServiceProvider is called, the ServiceProvider object itself will be returned as a Service instance. This feature can be implemented by using a custom Service. As shown in the following code snippet, the ServiceProviderService we defined is both a Service and a ServiceCallSite. It adopts the life cycle management mode of Scoped by default. In the Invoke and Build methods, it directly takes the current ServiceProvider as the provided Service instance. When initializing the ServiceTable, we add an additional ServideEntry for the ServiceProviderService.

   1: internal class ServiceProviderService : IService, IServiceCallSite
   2: {
   3:     public ServiceLifetime Lifetime => ServiceLifetime.Scoped;
   4:     public IService Next { get; set; }
   5: 
   6:     public Expression Build(Expression provider)
   7:     {
   8:         return provider;
   9:     }
  10: 
  11:     public IServiceCallSite CreateCallSite(ServiceProvider provider, ISet<Type> callSiteChain)
  12:     {
  13:         return this;
  14:     }
  15: 
  16:     public object Invoke(ServiceProvider provider)
  17:     {
  18:         return provider;
  19:     }
  20: }
  21: 
  22: internal class ServiceTable
  23: {
  24:     public ServiceTable(IServiceCollection services)
  25:     {
  26: / / parse ServiceCollection and add corresponding ServiceEntry
  27:         this.ServieEntries[typeof(IServiceProvider)] = new ServiceEntry(new ServiceProviderService());
  28:     }
  29: }


2, Create ServiceScope

The purpose of creating ServiceScope is to create another ServiceProvider as the son of the current ServiceProvider. The newly created ServiceProvider not only has the same root as the original ServiceProvider, but also shares all service registration information. The main purpose of using this new service provider to replace the existing service provider is to enable us to recycle the provided service instances in a timely manner. ServiceScope is created through its factory, ServiceScopeFactory. Therefore, the following ServiceScopeFactory class and the corresponding ServiceScope are created first. Their definitions are exactly the same as those described in the previous section.

   1: internal class ServiceScope : IServiceScope
   2: {
   3:     public IServiceProvider ServiceProvider { get; private set; }
   4: 
   5:     public ServiceScope(ServiceProvider serviceProvider)
   6:     {
   7:         this.ServiceProvider = serviceProvider;
   8:     }
   9: 
  10:     public void Dispose()
  11:     {
  12:         (this.ServiceProvider as IDisposable)?.Dispose();
  13:     }
  14: }
  15: 
  16: internal class ServiceScopeFactory : IServiceScopeFactory
  17: {
  18:     public ServiceProvider ServiceProvider { get; private set; }
  19: 
  20:     public ServiceScopeFactory(ServiceProvider serviceProvider)
  21:     {
  22:         this.ServiceProvider = serviceProvider;
  23:     }
  24: 
  25:     public IServiceScope CreateScope()
  26:     {
  27:         return new ServiceScope(this.ServiceProvider);
  28:     }
  29: }
  30: 
  31: internal class ServiceProvider : IServiceProvider, IDisposable
  32: {
  33:
  34:     public ServiceProvider(ServiceProvider parent)
  35:     {
  36:         this.Root = parent.Root;
  37:         this.ServiceTable = parent.ServiceTable;
  38:     }
  39: }

In order to enable the GetService method of ServiceProvider to automatically return the ServiceScopeFactory object defined above when the Service type is specified as iservicescope factory interface, we still create a custom Service as above and name it ServiceScopeFactory Service. Like ServiceProviderService, ServiceScopeFactoryService is also a ServiceCallSite, which returns a ServiceScopeFactory object in the Build and Invoke methods. For this to take effect, we still add a corresponding ServiceEntry automatically when the ServiceTable is initialized.

   1: internal class ServiceScopeFactoryService : IService, IServiceCallSite
   2: {
   3:     public ServiceLifetime Lifetime=> ServiceLifetime.Scoped;
   4:     public IService Next { get; set; }
   5: 
   6:     public IServiceCallSite CreateCallSite(ServiceProvider provider, ISet<Type> callSiteChain)
   7:     {
   8:         return this;
   9:     }
  10: 
  11:     public Expression Build(Expression provider)
  12:     {
  13:         return Expression.New(typeof(ServiceScopeFactory).GetConstructors().Single(), provider);
  14:     }
  15: 
  16:     public object Invoke(ServiceProvider provider)
  17:     {
  18:         return new ServiceScopeFactory(provider);
  19:     }
  20: }
  21: 
  22: internal class ServiceTable
  23: {
  24:     public ServiceTable(IServiceCollection services)
  25:     {
  26: / / parse ServiceCollection and add corresponding ServiceEntry
  27:         this.ServieEntries[typeof(IServiceProvider)] =  new ServiceEntry(new ServiceProviderService());
  28:         this.ServieEntries[typeof(IServiceScopeFactory)] = new ServiceEntry(new ServiceScopeFactoryService());
  29:     }
  30: }


3, Provides a collection of services

So far, our custom ServiceProvider does not have a feature of the native ServiceProvider, that is, when calling the GetService method, specifying the service type as IEnumerable < T > or directly calling the extension method GetServices, we get a collection of service instances. This feature can be done through a custom ServiceCallSite, which we named EnumerableCallSite.

   1: internal class EnumerableCallSite : IServiceCallSite
   2: {
   3:     public Type ElementType { get; private set; }
   4:     public IServiceCallSite[] ServiceCallSites { get; private set; }
   5: 
   6:     public EnumerableCallSite(Type elementType, IServiceCallSite[] serviceCallSites)
   7:     {
   8:         this.ElementType = elementType;
   9:         this.ServiceCallSites = serviceCallSites;
  10:     }
  11: 
  12:     public Expression Build(Expression provider)
  13:     {
  14:         return Expression.NewArrayInit(this.ElementType, this.ServiceCallSites.Select(
  15:             it => Expression.Convert(it.Build(provider), this.ElementType)));
  16:     }
  17: 
  18:     public object Invoke(ServiceProvider provider)
  19:     {
  20:         var array = Array.CreateInstance(this.ElementType, this.ServiceCallSites.Length);
  21:         for (var index = 0; index < this.ServiceCallSites.Length; index++)
  22:         {
  23:             array.SetValue(this.ServiceCallSites[index].Invoke(provider), index);
  24:         }
  25:         return array;
  26:     }
  27: }

As shown in the code snippet above, EnumerableCallSite has two read-only attributes (ElementType and ServiceCallSites), the former represents the element type of the returned service collection, and the latter returns a set of ServiceCallSites used to provide the collection elements. In the Invoke and Build methods, we only need to create an array based on the element type, and use this set of ServiceCallSite to create all elements. This EnumerableCallSite is finally applied to the GetServiceCallSite method of the ServiceProvider as follows.

   1: internal class ServiceProvider : IServiceProvider, IDisposable
   2: {
   3:     public IServiceCallSite GetServiceCallSite(Type serviceType, ISet<Type> callSiteChain)
   4:     {
   5:         try
   6:         {
   7:             if (callSiteChain.Contains(serviceType))
   8:             {
   9:                 throw new InvalidOperationException(string.Format("A circular dependency was detected for the service of type '{0}'",serviceType.FullName);
  10:             }
  11:             callSiteChain.Add(serviceType);
  12:             ServiceEntry serviceEntry;
  13:             if (this.ServiceTable.ServieEntries.TryGetValue(serviceType, out serviceEntry))
  14:             {
  15:                 return serviceEntry.Last.CreateCallSite(this, callSiteChain);
  16:             }
  17: 
  18:             if (serviceType.IsGenericType && serviceType.GetGenericTypeDefinition()== typeof(IEnumerable<>))
  19:             {
  20:                 Type elementType = serviceType.GetGenericArguments()[0];
  21:                 IServiceCallSite[] serviceCallSites = this.ServiceTable.ServieEntries.TryGetValue(elementType, out serviceEntry)
  22:                     ? serviceEntry.All.Select(it => it.CreateCallSite(this, callSiteChain)).ToArray()
  23:                     : new IServiceCallSite[0];
  24:                 return new EnumerableCallSite(elementType, serviceCallSites);
  25:             }
  26: 
  27:             return null;
  28:         }
  29:         finally
  30:         {
  31:             callSiteChain.Remove(serviceType);
  32:         }
  33:     }
  34: / / other members
  35: }

 

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 22:10:26 -0400 by craig1978