If you’ve built applications using ASP.NET Core then you’ve most likely used the built-in dependency injection container from Microsoft.Extensions.DependencyInjection. This package provides an implementation of the corresponding abstractions found in Microsoft.Extensions.DependencyInjection.Abstractions.

In the previous post, we learned about the IServiceCollection, including how service registrations are converted to ServiceDescriptors and added to the collection.

We’ll continue learning about ASP.NET Core dependency injection by focusing on what the IServiceProvider is and where it comes from.

WHAT IS AN ISERVICEPROVIDER?

The IServiceProvider is responsible for resolving instances of types at runtime, as required by the application. These instances can be injected into other services resolved from the same dependency injection container. The ServiceProvider ensures that resolved services live for the expected lifetime. Its implementation is designed to perform very efficiently so that the resolution of services is fast.

BUILDING AN ISERVICEPROVIDER

After populating the IServiceCollection with ServiceDescriptors for all of our services, the next phase is to create an IServiceProvider. The service provider will be capable of resolving instances of the types needed by our application. It essentially wraps the contains the knowledge represented in the IServiceCollection.

This stage is achieved by calling BuildServiceProvider, another extension method on the IServiceCollection.

varserviceCollection = newServiceCollection();serviceCollection.AddSingleton<ClassA>();serviceCollection.AddSingleton<IThing, ClassB>();varserviceProvider = serviceCollection.BuildServiceProvider();

In this sample, we use the most basic overload of this method which takes no additional arguments. This calls down to another extension method that accepts some ServiceProviderOptions.

A cached Default instance of ServiceProviderOptions is used when non is provided. Its two properties ValidateScopes and ValidateOnBuild are both false by default. You can, of course, create your own instance of this class, configured as necessary and pass it into an overload of the BuildServiceProvider extension method.

Here is what the options class looks like:

public class ServiceProviderOptions
{
    public bool ValidateScopes { get; set; }
    public bool ValidateOnBuild { get; set; }
}

We’ll see how these options are used when we look at the constructor of the ServiceProvider later in this post.

The BuildServiceProvider method contains the following code:

public static ServiceProvider BuildServiceProvider(this IServiceCollection services, 
     ServiceProviderOptions options)
 {
     if (services == null)
     {
         throw new ArgumentNullException(nameof(services));
     }
     if (options == null)
     {
         throw new ArgumentNullException(nameof(options));
     }
     IServiceProviderEngine engine;
 if !NETCOREAPP
 engine = new DynamicServiceProviderEngine(services);
 else
 if (RuntimeFeature.IsDynamicCodeCompiled) {     engine = new DynamicServiceProviderEngine(services); } else {     // Don't try to compile Expressions/IL if they are going to get interpreted     engine = new RuntimeServiceProviderEngine(services); }
 endif
 return new ServiceProvider(services, engine, options);
 }

The significant lines here highlight some of the implementation details for the ServiceProvider that will ultimately be created and returned. It’s not crucial to understand these as a library consumer, but I find it interesting to dig into it, so I will!

SERVICEPROVIDERENGINES

The above code determines which engine should be used by the ServiceProvider. The engine here is the component that decides how to instantiate services and how to inject those services into objects requiring those services.

There are four implementations of the ServiceProviderEngine abstract class from which these implementations derive.

  • Dynamic
  • Runtime
  • ILEmit
  • Expressions (System.Linq.Expressions)

From the above code, we can see that the DynamicServiceProviderEngine is used as the preferred engine in most cases. Only in cases where the target framework is .NET Core or .NET 5 and where the runtime does not support compilation of dynamic code is the RuntimeServiceProviderEngine used. This avoids attempting to compile expressions and IL if they are only ever going to be interpreted.

The DynamicServiceProviderEngine will use either ILEmit or Expressions for resolving services. ILEmit is preferred on target frameworks where it is supported, which is basically anything besides netstandard2.0.

The constructor of the abstract ServiceProviderEngine provides further insight into the inner workings of the dependency injection library.

protected ServiceProviderEngine(IEnumerable serviceDescriptors)
 {
     _createServiceAccessor = CreateServiceAccessor;
     Root = new ServiceProviderEngineScope(this);
     RuntimeResolver = new CallSiteRuntimeResolver();
     CallSiteFactory = new CallSiteFactory(serviceDescriptors);
     CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
     CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
     RealizedServices = new ConcurrentDictionary>();
 }

The constructor registers a Func<Type, Func<ServiceProviderEngineScope, object>>, a function which takes a Type and returns a function that given a ServiceProviderEngineScope can return an object. It registers a local private method matching this signature against the _createServiceAccessor field. We’ll see this used when we look at how services are resolved.

In this case, the root scope is the initial scope from which we expect services to be resolved. Singleton services are always returned from the root scope.

An instance of CallSiteRuntimeResolver is created, which we’ll see in action in a future post.

CALL SITES

Next, a new CallSiteFactory is created, passing in the service descriptors. Call sites derive from the base ServiceCallSite type. The ServiceCallSite type is used by the ServiceProvider to track information about services it can resolve. This includes information needed to support caching those instances for the appropriate lifetime. There are different call site implementations for the various ways a service may be resolved.

For example, the ConstructorCallSite is used for the most commonly registered services and contains information about the constructor of the implementation type and details of the calls sites used to resolve any of its dependencies. Don’t worry if this is a little confusing at this point; we’ll revisit this type when we look at how services are resolved when the inner workings will become more apparent.

For now, we’ll focus on the fact that two additional ServiceCallSite instances are added manually. The call sites which are added allow the IServiceProvider and IServiceScopeFactory to be resolved from the container.

Finally, in the above constructor, a new ConcurrentDictionary is created to hold information about realised services. The Service Provider uses an on-demand design, such that services realisation is deferred until the moment when those services are first needed. Some services you add to the container may never be required by the application at runtime, in which case, they are never realised.

CONSTRUCTING THE SERVICEPROVIDER

Let’s return to the final line in the BuildServiceProvider method – scroll up a bit to see the code again! It creates a new instance of the ServiceProvider class passing in the IServiceCollection, the chosen engine and the ServiceProviderOptions.

Here is the constructor of the ServiceProvider class.

internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngine engine, ServiceProviderOptions options)
{
    _engine = engine;
 
    if (options.ValidateScopes)
    {
        _engine.InitializeCallback(this);
        _callSiteValidator = new CallSiteValidator();
    }
 
    if (options.ValidateOnBuild)
    {
        List<Exception> exceptions = null;
        foreach (ServiceDescriptor serviceDescriptor in serviceDescriptors)
        {
            try
            {
                _engine.ValidateService(serviceDescriptor);
            }
            catch (Exception e)
            {
                exceptions = exceptions ?? new List<Exception>();
                exceptions.Add(e);
            }
        }
 
        if (exceptions != null)
        {
            throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray());
        }
    }
}

In the above code, we can see how the ServiceProviderOptions values are used within the constructor. When ValidateScopes is true, the ServiceProvider registers itself as a callback with the engine. It also creates a new CallSiteValidator.

The IServiceProviderEngineCallback interface defines two methods that the registered callback class must implement, OnCreate and OnResolve. The ServiceProvider explicitly implements this interface, using its CallSiteValidator to validate the call site or resolution accordingly. Here are the two methods from the ServiceProvider class.

void IServiceProviderEngineCallback.OnCreate(ServiceCallSite callSite)
{
    _callSiteValidator.ValidateCallSite(callSite);
}
 
void IServiceProviderEngineCallback.OnResolve(Type serviceType, IServiceScope scope)
{
    _callSiteValidator.ValidateResolution(serviceType, scope, _engine.RootScope);
}

VALIDATING CALL SITES AND SCOPES

When ValidateScopes is enabled, the code performs two primary checks. Firstly, that scoped services are not being resolved from the root service provider, and secondly that scoped services are not going to be injected into singleton services.

Returning to the ServiceProvider constructor above, if ValidateOnBuild is true, a check is performed to ensure that all services registered with the container can actually be created. The code loops over the service descriptors, calling ValidateService on each in turn. Any exceptions are caught and added to a list wrapped inside an AggregateException at the end of the method. This check aims to ensure that all registrations are valid and all dependencies in the dependency graph can be constructed, with all of their arguments satisfied by the container.

Enabling ValidateOnBuild ensures that most exceptions from missing or faulty service registrations can be caught early, when an application starts, rather than randomly at runtime when services are first resolved. This can be particularly useful during development to fail fast and allow developers to fix the issue.

There is one caveat to this build time validation; it cannot verify any open generic services registered with the container. Registering open generics is a reasonably advanced use case and rarely needed in most applications. If we view the code for ValidateService, defined on the ServiceProviderEngine, we can learn a little more.

public void ValidateService(ServiceDescriptor descriptor)
{
    if (descriptor.ServiceType.IsGenericType && !descriptor.ServiceType.IsConstructedGenericType)
    {
        return;
    }
 
    try
    {
        ServiceCallSite callSite = CallSiteFactory.GetCallSite(descriptor, new CallSiteChain());
        if (callSite != null)
        {
            _callback?.OnCreate(callSite);
        }
    }
    catch (Exception e)
    {
        throw new InvalidOperationException($"Error while validating the service descriptor '{descriptor}': {e.Message}", e);
    }
}

Immediately, we can see that first conditional, which excludes open generic service descriptors from further validation. Otherwise, the CallSiteFactory is used to attempt to create a call site from the service descriptor. Assuming a call site is returned, the OnCreate method of the _callback will be invoked, if an IServiceProviderEngineCallback was initialised. As we saw earlier, this will be the case if the ValidateScopes option is true. This method call will then also run the check to validate the scopes for the call site chain.

In any situations where GetCallSite cannot produce a valid and complete call site chain, an exception will be thrown where the message includes the name of the dependency, which could not be resolved as expected. The exception is caught and used to produce a more useful end-user exception is thrown which identifies the problematic descriptor and the invalid dependency. Build-time checks add a little extra up-front overhead but can help ensure that the majority of your service descriptors are valid.

Assuming all services are correctly registered and valid, the ServiceProvider constructor will return, and we have our built service provider.

SUMMARY

In this post, we focused on how the IServiceProvider is built from an IServiceCollection. We explored some of the implementation details to see how the ValidateScopes and ValidateOnBuild ServiceProviderOptions are applied. We touched on a lot of internal code in this post, and most of this, while interesting, is not a detail you need to worry about as a consumer of the library. We’ve been looking at the code for version 5.x of the Microsoft.Extensions.DependencyInjection library. Remember that any of this internal implementation may change in future releases.

The most important takeaway is that the default ServiceProvider is created after BuildServiceProvider is called on the IServiceCollection.

var serviceProvider = serviceCollection.BuildServiceProvider();

You can also build the service provider, passing in some ServiceProviderOptions to control the validation of services.

var serviceProviderWithOptions = serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
    ValidateOnBuild = true,
    ValidateScopes = true
});

When the ValidateOnBuild option is true, early checks will ensure that each required service can be created, with the exception of open generics. This is achieved by inspecting each descriptor and ensuring a call site chain can be created to provide an instance and any dependencies. When the option is set to false, the default, these checks will not occur upfront, and instead, any exceptions due to missing registrations will occur at runtime as services are resolved for the first time.

When ValidateScopes is enabled, additional checks occur to ensure that scoped services are not resolved as dependencies of singleton services created from the root scope. This would violate the intent of using the scoped services since any singleton instances would hold a reference to the scoped service instance for the life of the application.

Leave a comment

Your email address will not be published. Required fields are marked *