Exploring ServiceProvider in .NET

Exploring ServiceProvider in .NET

Introduction

In the previous posts we had seen the SeviceDescriptor class and also the ServiceCollection class and its role in dependency injection in .NET. You can refer to my previous posts here -

ServiceCollection allows you to register services by specifying its details in the form of a ServiceDescriptor with the Service Type, Implementation Type and Service Lifetime.

What is a ServiceProvider ?

ServiceProvider is a fundamental component of dependency injection framework in .NET. While ServiceCollection provides a mechanism to register a service and ServiceDescriptor provides a mechanism to represent or describe the registered service as seen in our previous posts, the ServiceProvider acts as a container of the services injected in the ServiceCollection.

Registration of Services

The process of registering services in the ServiceCollection happens with the call to methods like AddSingleton, AddTransient, or AddScoped on the ServiceCollection, these methods add service descriptors to the collection. A service descriptor contains information about the service type, its implementation, and its lifetime (singleton, transient, scoped).

Building a ServiceProvider

The ServiceProvider is created after registration of services in the ServiceCollection by calling the extension method BuildServiceProvider().

The code below shows the registration of EmailService and NotificationService and then chaining the BuildServiceProvider() method to return the default implementation of the ServiceProvider.

This service provider instance can be further used to get the actual instance of the injected service.

 var serviceProvider = new ServiceCollection()
            .AddSingleton<IEmailService, EmailService>()
            .AddTransient<NotificationService>()
            .BuildServiceProvider();

The BuildServiceProvider method is an extension method present in the ServiceCollectionContainerBuilderExtensions class which is a static class that adds extension methods to IServiceCollection. The code below shows the details BuildServiceProvider method.

/// <summary>
/// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
/// <returns>The <see cref="ServiceProvider"/>.</returns>
public static ServiceProvider BuildServiceProvider(this IServiceCollection services)
{
    return BuildServiceProvider(services, ServiceProviderOptions.Default);
}

/// <summary>
/// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>
/// optionally enabling scope validation.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
/// <param name="validateScopes">
/// <c>true</c> to perform check verifying that scoped services never gets resolved from root provider; otherwise <c>false</c>.
/// </param>
/// <returns>The <see cref="ServiceProvider"/>.</returns>
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, bool validateScopes)
{
    return services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = validateScopes });
}

/// <summary>
/// Creates a <see cref="ServiceProvider"/> containing services from the provided <see cref="IServiceCollection"/>
/// optionally enabling scope validation.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> containing service descriptors.</param>
/// <param name="options">
/// Configures various service provider behaviors.
/// </param>
/// <returns>The <see cref="ServiceProvider"/>.</returns>
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
    if (services is null)
    {
        throw new ArgumentNullException(nameof(services));
    }
    if (options is null)
    {
        throw new ArgumentNullException(nameof(options));
    }

    return new ServiceProvider(services, options);
}

The ServiceProvider class has the internal implementation of how the instances of the injected services are internally maintained and returned when requested from the application.

If the requested service has dependencies, the ServiceProvider recursively resolves these dependencies by looking up their implementations. This process continues until all dependencies are resolved, and an instance of the requested service (or its implementation) is created.

Resolution of Services present in ServiceProvider

The services registered in the ServiceCollection and the specification of injected services by a SecrviceDescriptor in maintained inside the ServiceProvider with the help of a dictionary based implementation. This implementation also takes care of the Lifetime of the injected services.

The ServiceProvider class specifies few methods with the help of which the instances of the injected services are returned to the client. The code below shows some of the methods that helps in retrieving the instance of the injected services from the ServiceProvider.


/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <param name="serviceType">The type of the service to get.</param>
/// <returns>The service that was produced.</returns>
public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);

/// <summary>
/// Gets the service object of the specified type with the specified key.
/// </summary>
/// <param name="serviceType">The type of the service to get.</param>
/// <param name="serviceKey">The key of the service to get.</param>
/// <returns>The keyed service.</returns>
public object? GetKeyedService(Type serviceType, object? serviceKey)
    => GetKeyedService(serviceType, serviceKey, Root);

internal object? GetKeyedService(Type serviceType, object? serviceKey, ServiceProviderEngineScope serviceProviderEngineScope)
    => GetService(new ServiceIdentifier(serviceKey, serviceType), serviceProviderEngineScope);

/// <summary>
/// Gets the service object of the specified type. Will throw if the service not found.
/// </summary>
/// <param name="serviceType">The type of the service to get.</param>
/// <param name="serviceKey">The key of the service to get.</param>
/// <returns>The keyed service.</returns>
/// <exception cref="InvalidOperationException"></exception>
public object GetRequiredKeyedService(Type serviceType, object? serviceKey)
    => GetRequiredKeyedService(serviceType, serviceKey, Root);

internal object GetRequiredKeyedService(Type serviceType, object? serviceKey, ServiceProviderEngineScope serviceProviderEngineScope)
{
    object? service = GetKeyedService(serviceType, serviceKey, serviceProviderEngineScope);
    if (service == null)
    {
        throw new InvalidOperationException(SR.Format(SR.NoServiceRegistered, serviceType));
    }
    return service;
}

We can use the above methods to retrieve the intsance of the notification service we had injected earlier using the code below -

// Retrieving instances using dependency injection
var emailService = serviceProvider.GetService<IEmailService>();
var notificationService = serviceProvider.GetService<NotificationService>();

The internal implementation of the ServiceProvider has a lot of other things but the points covered above are the most important ones which gives an idea of the role of ServiceProvider class in the dependency injection container in .NET.

I hope the series of post related to dependency injection container might have helped you get a clear understanding of how the services are registered, described and provided in a .NET dependency injection container.

Thank you for reading the post and see you in the next post.

Buy a coffee for sudshekhar