Click here to Skip to main content
15,438,113 members
Articles / Programming Languages / C#
Project
Posted 9 Aug 2022

Stats

2.8K views
9 downloads
3 bookmarked

Exploring the Microsoft.Extensions.DependencyInjection Machinery

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
9 Aug 2022CPOL7 min read
Core concepts and mechanisms of Microsoft.Extensions.DependencyInjection Dependency Injection
This article is an exploration of the core concepts and mechanisms of the Microsoft.Extensions.DependencyInjection Dependency Injection machinery.

The Exploring the Microsoft.Extensions.DependencyInjection machinery project proposes an exploration of the basic concepts and mechanisms of the Microsoft.Extensions.DependencyInjection Dependency Injection machinery.

This exploration is meant to be progressive, orderly, specifying the terms used, providing in the form of unit tests some as concise as possible examples illustrating the described mechanisms.

The documents used to write this document were mainly:

1) Implementation of a 'Dependency Injection Container

1.1) ServiceDescriptor Class, IServiceCollection and IServiceProvider Interfaces

The implementation of a 'Dependency Injection Container' (aka 'DI container', 'container', 'ServiceProvider') requires:

  • the definition of a ServiceCollection, a list of ServiceDescriptors where each element defines the characteristics of one of the services making the container to be produced:

    • its type
    • the type implementing it
    • the lifetime of the instances implementing it (Singleton, Transient, Scoped)
  • the production of a ServiceProvider from this list.

A ServiceCollection takes the form of an object instance exposing the IServiceCollection interface.

C#
public interface IServiceCollection: ICollection<ServiceDescriptor>, 
                                    IEnumerable<ServiceDescriptor>, 
                                    IList<ServiceDescriptor>

A ServiceProvider takes the form of an object instance exposing the IServiceProvider interface.

C#
public interface IServiceProvider
{
    public object? GetService (Type serviceType);
}

The Microsoft.Extensions.DependencyInjection package provides the ServiceCollection and ServiceProvider classes as default implementations of these interfaces.

1.2) Producing a ServiceProvider from this ServiceCollection list: IServiceCollection.BuildServiceProvider

The ServiceCollectionContainerBuilderExtensions class provides a list of BuildServiceProvider extensions methods for the IServiceCollection interface:

C#
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionContainerBuilderExtensions
    {
        public static ServiceProvider BuildServiceProvider
                       (this IServiceCollection services);
	    ...

1.3) Examples: class UnitTests.ServiceProviderCreationTests

The UnitTests.ServiceProviderCreationTests unit test class provides examples of various ways to create a ServiceCollection and then transform it into a ServiceProvider.

These examples make use of some of the extensions provided by the ServiceCollectionServiceExtensions class described below:

Example: ServiceProviderCreationTests.Test0

C#
/***
 * - creating a ServiceCollection and then a ServiceProvider from it
 * - registering Singleton services in 2 different ways
 * - testing their resolution
 * - testing the resolution of a non registered service
***/
public void Test0()
{
    ServiceProvider? _serviceProvider = null;
    try
    {
        this.Log("(-)");

        // ServiceCollection
        IServiceCollection _collection = new ServiceCollection();

        _collection.Add(new ServiceDescriptor(typeof(InterfaceA), 
                        typeof(ClassA), ServiceLifetime.Singleton));
        _collection.AddSingleton<InterfaceB, ClassB>();

        // ServiceProvider
        _serviceProvider = _collection.BuildServiceProvider();

        // GetService / Singleton
        {
            InterfaceA? _interfaceA0 = _serviceProvider.GetService
                                       (typeof(InterfaceA)) as InterfaceA;
            InterfaceA? _interfaceA1 = _serviceProvider.GetService
                                       (typeof(InterfaceA)) as InterfaceA;
            Assert.NotNull(_interfaceA0);
            Assert.Equal(_interfaceA0, _interfaceA1);
        }
        {
            InterfaceB? _interfaceB = _serviceProvider.GetService<InterfaceB>();
            Assert.NotNull(_interfaceB);
        }

        // GetService / unregistered service
        {
            ClassD? _classC = _serviceProvider.GetService<ClassD>();
            Assert.Null(_classC);
        }
    }
    finally
    {
        this.Log("_serviceProvider?.Dispose(-)");
        _serviceProvider?.Dispose();
        this.Log("_serviceProvider?.Dispose(+)");
    }
}

Example: ServiceProviderCreationTests.Test1

C#
/***
 * A more straightforward way of creating a ServiceProvider
***/
[Fact]
public void Test1()
{
    ServiceProvider? _serviceProvider = null;
    try
    {
        // ServiceProvider
        _serviceProvider = new ServiceCollection()
            .AddSingleton<InterfaceA, ClassA>()
            .BuildServiceProvider();

        // GetService
        {
            InterfaceA? _interfaceA = _serviceProvider.GetService<InterfaceA>();
        }
    }
    finally
    {
        _serviceProvider?.Dispose();
    }

}

1.4) Organization and content of the IServiceCollection extensions offered by the ServiceCollectionDescriptorExtensions and ServiceCollectionServiceExtensions classes

The ServiceCollectionDescriptorExtensions and ServiceCollectionServiceExtensions classes each expose a set of extensions to the IServiceCollection interface intended to make the creation of the content of an IServiceCollection more readable and productive.

The ServiceCollectionDescriptorExtensions class exposes a set of extension methods of the IServiceCollection interface concerning its list capabilities: Add, Remove, RemoveAll, Replace, TryAdd, TryAddEnumerable, TryAddScoped/Singleton/Transient ...

The ServiceCollectionServiceExtensions class exposes a set of extension methods of the IServiceCollection interface interface, grouped by lifetime (Scope, Singleton, Transient) and by the way in which the types of services and their implementation are specified: generic, types passed as parameters.

C#
public static class ServiceCollectionServiceExtensions
{
    //
    // Scoped
    //

    // Type parameters
    public static IServiceCollection AddScoped (this IServiceCollection services, 
										Type serviceType);

    public static IServiceCollection AddScoped (this IServiceCollection services, 
										Type serviceType, 
										Func<IServiceProvider,object> 
                                             implementationFactory);

    public static IServiceCollection AddScoped (this IServiceCollection services, 
										Type serviceType, 
										Type implementationType);

    // Generic methods
    public static IServiceCollection AddScoped<TService,TImplementation> 
                                        (this IServiceCollection services) 
										where TService: class 
										where TImplementation: class, TService;

    public static IServiceCollection AddScoped<TService,TImplementation> 
                                        (this IServiceCollection services, 
										Func<IServiceProvider,TImplementation> 
                                        implementationFactory) 
										where TService: class 
										where TImplementation: class, TService;

    public static IServiceCollection AddScoped<TService> 
                                        (this IServiceCollection services) 
										where TService: class;

    public static IServiceCollection AddScoped<TService> 
                                        (this IServiceCollection services, 
									    Func<IServiceProvider,TService> 
                                        implementationFactory) 
									    where TService: class;

    //
    // Singleton, Transient
    //
    // ...

2) Service Resolution using a IServiceProvider: GetService, GetRequiredService, ...

2.1) The IServiceProvider interface and its ServiceProviderServiceExtensions extension class

  • The IServiceProvider interface exposes a single method:

    C#
    public object? GetService (Type serviceType);
  • The ServiceProviderServiceExtensions class provides many extensions to this interface as variations of GetService, GetRequiredService, GetServices, CreateScope, CreateAsyncScope:

    C#
    	public static T? GetService(this IServiceProvider provider);
    
    	public static object GetRequiredService(this IServiceProvider provider, 
                                                Type serviceType);
    
    	public static T GetRequiredService(this IServiceProvider provider) 
                                           where T: notnull;
    
    	public static IEnumerable GetServices(this IServiceProvider provider);
    
    	public static IEnumerable<object?> GetServices
               (this IServiceProvider provider, Type serviceType);
    
    	public static IServiceScope CreateScope(this IServiceProvider provider);
    
    	public static AsyncServiceScope CreateAsyncScope
                                        (this IServiceProvider provider);
    
    	public static AsyncServiceScope CreateAsyncScope
               (this IServiceScopeFactory serviceScopeFactory);

2.2) GetService vs GetRequiredService

The difference between a GetService method and its GetRequiredService counterpart is that:

  • GetService returns null if the requested service cannot be resolved by the IServiceProvider interface.
  • GetRequiredService triggers an exception in this case.

It is illustrated by the ServiceProviderServiceExtensionsTests.Test_GetService unit test.

2.3) GetServices

Example: ServiceProviderServiceExtensionsTests.Test_GetServices

The following code:

C#
public void Test_GetServices()
{
    ServiceProvider? _serviceProvider = null;
    try
    {
        this.Log("(-)");

        _serviceProvider = new ServiceCollection()
            .AddSingleton<ClassA>(new ClassA())
            .AddSingleton<ClassA>(new ClassA())
            .AddSingleton<ClassA>(new ClassA()) // <<<<
            .BuildServiceProvider();

        ClassA classA = _serviceProvider.GetService<ClassA>(); // <<<<
        this.Log($"classA={classA}");

        foreach (ClassA _service in _serviceProvider.GetServices<ClassA>())
        {
            this.Log($"_service={_service}");
        }
    }
    finally
    {
        this.Log("(+)");
        _serviceProvider?.Dispose();
    }
}

produces the following debug output:

[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].(Test_GetServices) '(-)' 
[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].
                    (Test_GetServices) 'classA=ClassA[4]' 
[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].
                    (Test_GetServices) '_service=ClassA[2]' 
[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].
                    (Test_GetServices) '_service=ClassA[3]' 
[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].
                    (Test_GetServices) '_service=ClassA[4]' 
[16]UnitTests.Tests.ServiceProviderServiceExtensionsTests].(Test_GetServices) '(+)' 

3) Service Implementation Lifetime: Singleton, Transient, Scope

The rules presented in this paragraph are illustrated by the ServiceLifetimeTests class.

3.1) Terminology

  • Resolution of a service by a (DI) container:

    Calling the .GetService method of an IServiceProvider instance specifying the type of the service for which you wish to obtain an implementation

  • Resolution of a Singleton/Transient/Scope service by a (DI) container:

    Resolution of a service that has been registered as Singleton/Transient/Scope by the ServiceCollection from which the implemented DI Container (IServiceProvider) was produced (IServiceCollection.BuildServiceProvider).

  • 'root' container:

    An IServiceProvider instance produced from an instance of IServiceCollection, by a call to BuildServiceProvider.

  • 'scoped' container:

    The IServiceProvider instance exposed by an IServiceScope instance:

    C#
    public interface IServiceScope: IDisposable 
    { 
         // The System.IServiceProvider used to resolve dependencies from the scope.
         IServiceProvider ServiceProvider 
         { 
             get; 
         } 
    }

    See below.

3.2) Singleton

A single instance of the type implementing a 'Singleton' service is created by a ServiceProvider on the first resolution request (GetService).

A 'Singleton' service could also be associated to an implementation instance when the ServiceCollection from which the ServiceProvider originates was created. This implementation instance will then be returned by the ServiceProvider as a resolution of the 'Singleton' service.

In both cases, the resolution of a 'Singleton' service always provides the same answer.

Example

C#
ServiceProvider? _serviceProvider = null;
try
{
    ClassD _classD = new ClassD();

    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddSingleton<InterfaceA, ClassA>()
        .AddSingleton<ClassD>(_classD)
        .BuildServiceProvider();

    // Singleton
    {
        InterfaceA? _interface0 = _serviceProvider.GetService<InterfaceA>();
        Assert.NotNull(_interface0);
        //
        InterfaceA? _interface1 = _serviceProvider.GetService<InterfaceA>();
        Assert.Equal(_interface1, _interface0);
        //
        ClassD? _class0 = _serviceProvider.GetService<ClassD>();
        Assert.Equal(_class0, _classD);
    }

3.3) Transient

The resolution of a 'Transient' service provides each time a new instance.

Example

C#
ServiceProvider? _serviceProvider = null;
try
{
    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddTransient<InterfaceB, ClassB>()
        .BuildServiceProvider();

    // Transient
    {
        InterfaceB? _interface0 = _serviceProvider.GetService<InterfaceB>();
        Assert.NotNull(_interface0);
        //
        InterfaceB? _interface1 = _serviceProvider.GetService<InterfaceB>();
        Assert.NotEqual(_interface1, _interface0);
    }

3.4) Scope

Rule: The resolution of a 'Scope' service must not be requested from a 'root' container but from a 'scoped' container.

This rule can be checked at runtime or not by a ServiceProvider depending on how it was produced (see below).

Example

C#
bool validateScopes = true; // <<<

ServiceProvider? _serviceProvider = null;
try
{
    ServiceProviderOptions serviceProviderOptions = new ServiceProviderOptions()
    {
        ValidateScopes = validateScopes, // <<<
    };

    ClassD _classD = new ClassD();

    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddScoped<ClassC>()
        .BuildServiceProvider(options: serviceProviderOptions);

    // Scoped, out of a scope: should not be done
    // If _serviceProvider was built (BuildServiceProvider) 
    // with 'options.ValidateScopes=true' 
    // an exception is thrown
    {
        bool _thrown = false;
        try
        {
            ClassC? _class0 = _serviceProvider.GetService<ClassC>();
            ClassC? _class1 = _serviceProvider.GetService<ClassC>();
            Assert.Equal(_class0, _class1);
        }
        catch (Exception E)
        {
            _thrown = true; // <<<
            this.Log(E);
        }
        Assert.Equal(_thrown, validateScopes); // <<<
    }

4) Choice by a Container of the Constructor of the Type Implementing a Service

The resolution of a service by a container can imply the creation of an instance of the type implementing this service. This is the case, among others, during the first resolution of a 'Singleton' service or during each resolution of a 'Transient' service.

It is possible that the class to be instantiated exposes several constructors: which one does a container choose when it instantiates the implementing class?

A container chooses the constructor whose parameter list contains the largest number of types resolved by itself. It is possible that several constructors quote the same number of resolved types: in this case, the container does not know how to choose a constructor and therefore does not instantiate the class and throws an exception.

The ServiceInstantiationTests.Test_ConstructorChoice test illustrates these mechanisms.

Example

C#
class ClassD: BaseClass
{
    public ClassD()
    {
        this.Log("");
    }

    public ClassD(InterfaceA interfaceA)
    {
        this.Log($"interfaceA={interfaceA}");
    }

    public ClassD(InterfaceA interfaceA, InterfaceB interfaceB)
    {
        this.Log($"interfaceA={interfaceA} interfaceB={interfaceB}");
    }
}

class ClassE: BaseClass
{
    public ClassE(InterfaceA interfaceA, InterfaceB interfaceB)
    {
        this.Log("");
    }

    public ClassE(InterfaceA interfaceA, InterfaceC interfaceC)
    {
        this.Log("");
    }
}

ServiceProvider? _serviceProvider = null;
try
{
    this.Log("(-)");

    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddTransient<InterfaceA, ClassA>()
        .AddTransient<InterfaceB, ClassB>()
        .AddTransient<InterfaceC, ClassC>()
        .AddTransient<ClassD>()
        .AddTransient<ClassE>()
        .BuildServiceProvider();

    {
        this.Log("_serviceProvider.GetService<ClassD>(-)");
        ClassD? classD = _serviceProvider.GetService<ClassD>();
        this.Log($"_serviceProvider.GetService<ClassD>(+) classD={classD}");
        // the debug output shows: 
        //[16]UnitTests.Tests.ServiceInstantiationTests].(Test_ConstructorChoice)'(-)'
        //[16]UnitTests.Tests.ServiceInstantiationTests].
        //(Test_ConstructorChoice)'_serviceProvider.GetService<ClassD>(-)'
        //[16]ClassD[4]].(.ctor) 'interfaceA=ClassA[2] interfaceB=ClassB[3]'
        //[16]UnitTests.Tests.ServiceInstantiationTests].
        //(Test_ConstructorChoice)'_serviceProvider.GetService<ClassD>(+) 
        //classD=ClassD[4]'
    }

    // no constructor from ClassE can be unambiguously chosen 
    // by the container which then throws an exception
    {
        bool _thrown = false;
        try
        {
            this.Log("_serviceProvider.GetService<ClassE>(-)");
            ClassE? classE = _serviceProvider.GetService<ClassE>();
            this.Log($"_serviceProvider.GetService<ClassE>(+) classE={classE}");
        }
        catch (Exception E)
        {
            _thrown = true;
            this.Log(E);
        }
        Assert.True(_thrown);
    }

5) Registering and Destroying Disposable Instances Generated by a Container

As mentioned before, the resolution of a service by a container can imply the creation of an instance of the type implementing this service.

These instances may be registered by the container in an internal list to ensure their Singleton or Scope character, but also to explicitly destroy the 'disposable' instances (exposing the IDisposable interface) it creates, regardless of their Singleton/Transient/Scoped lifetime, when it is destroyed.

The ServiceProvider class is disposable:

C#
public sealed class ServiceProvider: IServiceProvider, IDisposable, IAsyncDisposable

The IServiceScope interface is disposable:

C#
public interface IServiceScope: IDisposable

A 'root' container is explicitly 'disposable'.

A 'scoped' container is destroyed when its scope is destroyed.

The explicit destruction of the 'disposable' instances produced and listed by a container occurs when the container is 'disposed'.

Examples

'root' container

C#
ServiceProvider? _serviceProvider = null;
try
{
    this.Log("(-)");

    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddTransient<DisposableClassA>()
        .BuildServiceProvider();

    DisposableClassA disposableClassA = _serviceProvider.GetService<DisposableClassA>();

    this.Log($"disposableClassA={disposableClassA}");
    // produces the debug output:
    //[16]UnitTests.Tests.ServiceInstantiationTests].(Test_DisposableImplementations0) 
    //'disposableClassA=DisposableClassA[2]' 

}
finally
{
    this.Log("_serviceProvider?.Dispose(-)");
    _serviceProvider?.Dispose(); // destruction de disposableClassA
    this.Log("_serviceProvider?.Dispose(+)");
    // produces the debug output:
    //[16]UnitTests.Tests.ServiceInstantiationTests].(Test_DisposableImplementations0) 
    //'_serviceProvider?.Dispose(-)' 
    //[16]DisposableClassA[2]].(dispose) 'disposing=True' 
    //[16]UnitTests.Tests.ServiceInstantiationTests].(Test_DisposableImplementations0) 
    //'_serviceProvider?.Dispose(+)' 
}

'scoped' container

The following code:

C#
ServiceProvider? _serviceProvider = null;
try
{
    this.Log("(-)");

    // ServiceProvider
    _serviceProvider = new ServiceCollection()
        .AddSingleton<DisposableClassA>()
        .AddTransient<DisposableClassB>()
        .AddScoped<DisposableClassC>()
        .BuildServiceProvider();

    // Creating a IServiceScope, making sure it is disposed when used
    using (IServiceScope scope = _serviceProvider.CreateScope())
    {
        DisposableClassA? _disposableClassA = 
                  _serviceProvider.GetService<DisposableClassA>();
        this.Log($"_disposableClassA={_disposableClassA}");

        DisposableClassB? _disposableClassB = 
                  _serviceProvider.GetService<DisposableClassB>();
        this.Log($"_disposableClassB={_disposableClassB}");

        DisposableClassC? _disposableClassC = 
                  _serviceProvider.GetService<DisposableClassC>();
        this.Log($"_disposableClassC={_disposableClassC}");
    }
}
finally
{
    this.Log("_serviceProvider?.Dispose(-)");
    _serviceProvider?.Dispose();
    this.Log("_serviceProvider?.Dispose(+)");
}

produces the following debug output:

[16]UnitTests.Tests.ServiceInstantiationTests].(Test_DisposableImplementations1) '(-)' 
[16]UnitTests.Tests.ServiceInstantiationTests].
(Test_DisposableImplementations1) '_disposableClassA=DisposableClassA[2]' 
[16]UnitTests.Tests.ServiceInstantiationTests].
(Test_DisposableImplementations1) '_disposableClassB=DisposableClassB[3]' 
[16]UnitTests.Tests.ServiceInstantiationTests].
(Test_DisposableImplementations1) '_disposableClassC=DisposableClassC[4]' 
[16]UnitTests.Tests.ServiceInstantiationTests].
(Test_DisposableImplementations1) '_serviceProvider?.Dispose(-)' 
[16]DisposableClassC[4]].(dispose) 'disposing=True' 
[16]DisposableClassB[3]].(dispose) 'disposing=True' 
[16]DisposableClassA[2]].(dispose) 'disposing=True' 
[16]UnitTests.Tests.ServiceInstantiationTests].
(Test_DisposableImplementations1) '_serviceProvider?.Dispose(+)' 

Note: The 'non disposable' Transient instances produced by a container are not listed, they are released by the Garbage Collector.

Some important consequences of this operation:

  • a 'root' container resolving Singleton or Transient services as 'disposable' instances can be a source of memory leakage, especially for Transients since they won't be disposed until the container is itself disposed.

  • this is also true of Transient and Scope services resolved in the form of 'disposable' instances by a 'scoped' container, but this container will be destroyed at the same time as and by its parent scope, which is supposed to happen quickly.

  • services resolved as disposable instances should not be disposed by the client of the container that issued them: it will be done by the container itself.

This last point should encourage to avoid storing as class members the references of resolved services exposing the IDisposable interface.

Example

C#
class DisposableClassA: Disposable0, IDisposableA
{
}

/***
 * - an instance of DisposableClassE is built with an 
 * IDisposableA reference its stores in its _disposableA member.
 * - holding disposable members, DisposableClassE should be 
 * disposable itself and handle the destruction of its members when disposed.
***/
class DisposableClassE: Disposable0
{
    IDisposableA? _disposableA;

    public DisposableClassE(IDisposableA disposableA)
    {
        this._disposableA = disposableA;
    }

    protected override void doDispose(bool disposing)
    {
        try
        {
            this.Log($"(-) disposing={disposing} _disposableA={_disposableA}");
            if (disposing)
            {
                _disposableA?.Dispose();
            }
            this.Log($"(+) disposableA={_disposableA}");
        }
        finally
        {
            base.doDispose(disposing);
        }
    }
}

public void Test_DisposableService0()
{
    ServiceProvider? _serviceProvider = null;
    try
    {
        this.Log("(-)");

        // ServiceProvider
        _serviceProvider = new ServiceCollection()
            .AddTransient<IDisposableA, DisposableClassD>()
            .AddTransient<DisposableClassE>()
            .BuildServiceProvider();

        DisposableClassE disposableClassE = 
                  _serviceProvider.GetService<DisposableClassE>();
        this.Log($"disposableClassE={disposableClassE}");

    }
    finally
    {
        this.Log("_serviceProvider?.Dispose(-)");
        // This call will dispose the DisposableClassE disposableClassE instance 
        // as well as the DisposableClassD instance created 
        // by the container to build disposableClassE.
        // disposableClassE will itself dispose the DisposableClassD 
        // instance it received when constructed: 
        // this DisposableClassD instance will then be disposed twice ...
        _serviceProvider?.Dispose();
        this.Log("_serviceProvider?.Dispose(+)");
    }
}

Test_DisposableService0 produces the following debug output:

[16]UnitTests.Tests.ServiceInstantiationTests].(Test_DisposableService0) '(-)' 
[16]UnitTests.Tests.ServiceInstantiationTests].
    (Test_DisposableService0) 'disposableClassE=DisposableClassE[3]' 
[16]UnitTests.Tests.ServiceInstantiationTests].
    (Test_DisposableService0) '_serviceProvider?.Dispose(-)' 
[16]DisposableClassE[3]].(dispose) 'disposing=True' 
[16]DisposableClassE[3]].(doDispose) '(-) 
    disposing=True _disposableA=DisposableClassD[2]' 
[16]DisposableClassA[2]].(dispose) 'disposing=True' 
[16]DisposableClassE[3]].(doDispose) '(+) disposableA=DisposableClassD[2]' 
[16]DisposableClassA[2]].(dispose) 
    'disposing=True ALREADY DISPOSED' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
[16]UnitTests.Tests.ServiceInstantiationTests].
    (Test_DisposableService0) '_serviceProvider?.Dispose(+)' 

6) Open Generic Services

A container can register and resolve 'open generic services'.

Example

C#
class UnitTests.Tests.GenericServicesTests
{

    public void Test_GenericService0()
    {
        ServiceProvider services = null;
        try
        {
            services = new ServiceCollection()
                .AddScoped(typeof(InterfaceF<>), typeof(ClassF<>))
                .BuildServiceProvider();

            InterfaceF<int>? _instance0 = services.GetService<InterfaceF<int>>();
            Assert.NotNull(_instance0);

            InterfaceF<string>? _instance1 = services.GetService<InterfaceF<string>>();
            Assert.NotNull(_instance1);
        }
        finally
        {
            services?.Dispose();
        }
    }

7) Validation Capabilities of a Container

7.1) Preventing the Resolution of Scoped Services out of a Scope

Example: ServiceProviderValidationTests.TestScopeValidation0

C#
public class ServiceProviderValidationTests
{

  /***
   * - Testing the ability of a correctly configured container to validate scopes, 
   *   that is to prevent the resolution
   *   of scoped services out of a scope.
  ***/
  [Fact]
  public void TestScopeValidation0()
  {
      ServiceProvider? _serviceProvider = null;
      try
      {
          this.Log("(-)");

          // ServiceProvider: built to validate scopes
          _serviceProvider = new ServiceCollection()
              .AddScoped<ClassA>()
              .BuildServiceProvider(validateScopes: true);

          // Trying to resolve a Scope service out of a scope
          {
              bool _exception = false;
              try
              {
                  ClassA classA = _serviceProvider.GetService<ClassA>();
              }
              catch (Exception e)
              {
                  _exception = true;
                  this.Log(e);
              }
              Assert.True(_exception);
          }

          // Correctly resolving a Scope service using an IServiceScope scope
          using (IServiceScope scope = _serviceProvider.CreateScope())
          {
              bool _exception = false;
              try
              {
                  ClassA classA = scope.ServiceProvider.GetService<ClassA>();
                  Assert.NotNull(classA);
              }
              catch (Exception e)
              {
                  _exception = true;
                  this.Log(e);
              }
              Assert.False(_exception);
          }

      }
      finally
      {
          this.Log("(+)");
          _serviceProvider?.Dispose();
      }
  }

7.2) Preventing the Resolution of a Singleton Service Depending on Scope Services

Example: ServiceProviderValidationTests.TestScopeValidation1

C#
public class ServiceProviderValidationTests
{

    /***
     * - Testing the ability of a correctly configured container 
     *   to prevent the resolution of 
     *   a singleton service depending on scoped services.
    ***/
    [Fact]
    public void TestScopeValidation1()
    {
        ServiceProvider? _serviceProvider = null;
        try
        {
            this.Log("(-)");

            /**
             * The ClassE constructor to be invoked to resolve it:
             *      public ClassE(InterfaceA interfaceA)
             * mentions a scoped service: InterfaceA     
            **/
            // ServiceProvider
            _serviceProvider = new ServiceCollection()
                .AddSingleton<ClassE>()
                .AddScoped<InterfaceA, ClassA>()
                .BuildServiceProvider(validateScopes: true);

            {
                bool _exception = false;
                try
                {
                    ClassE classE = _serviceProvider.GetService<ClassE>();
                }
                catch (Exception e)
                {
                    _exception = true;
                    this.Log(e);
                }
                Assert.True(_exception);
            }

        }
        finally
        {
            this.Log("(+)");
            _serviceProvider?.Dispose();
        }
    }

7.3) Checking the Completeness of the Description of the Services Meant to Build a Container

Example: ServiceProviderValidationTests.TestScopeValidation2

C#
public class ServiceProviderValidationTests
{
    /***
     * - Checking the completeness of the description of the services meant 
     *   to build a container. 
    ***/
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void TestScopeValidation2(bool validateOnBuild)
    {
        ServiceProvider? _serviceProvider = null;
        try
        {
            this.Log($"(-) validateOnBuild={validateOnBuild}");

            /**
             * The ClassE constructor to be invoked to resolve it is:
             *      public ClassE(InterfaceA interfaceA)
             * it requires the resolution of the InterfaceA service    
            **/
            {
                bool _exception = false;
                try
                {
                    // ServiceProvider
                    _serviceProvider = new ServiceCollection()
                        .AddSingleton<ClassE>()
                        //.AddTransient<InterfaceA, ClassA>() << missing service 
                        // declaration
                        .BuildServiceProvider(new ServiceProviderOptions()
                        {
                            ValidateOnBuild = validateOnBuild
                        });

                    // _serviceProvider was built, but it is not able to 
                    // resolve InterfaceA
                    // and therefore ClassE
                    bool __exception = false;
                    try
                    {
                        ClassE classE = _serviceProvider.GetService<ClassE>();
                    }
                    catch (Exception e)
                    {
                        __exception = true;
                        this.Log(e);
                    }
                    Assert.True(__exception);
                }
                catch (Exception e)
                {
                    _exception = true;
                }
                finally
                {
                    Assert.Equal(_exception, validateOnBuild);
                }
            }
        }
        finally
        {
            this.Log("(+)");
            _serviceProvider?.Dispose();
        }
    }

8) The ActivatorUtilities Class

The ActivatorUtilities class allows to create instances of classes not resolved by a container but whose constructor requires arguments that can be resolved by this container.

This excellent article explains how: Activator utilities: activate anything!

9) The IServiceScopeFactory Interface

The IServiceScopeFactory interface exposes only one method:

C#
public Microsoft.Extensions.DependencyInjection.IServiceScope CreateScope ();

It is registered as a Singleton service by a container and can be resolved and used to produce IServiceScope scopes.

Example: IServiceScopeFactoryTests.Test0

C#
// Using an existing IServiceScope and its .ServiceProvider to resolve services
void test0(IServiceScope scope)
{
    InterfaceA? _pInterfaceA0 = scope.ServiceProvider.GetService<InterfaceA>();
    InterfaceA? _pInterfaceA1 = scope.ServiceProvider.GetService<InterfaceA>();
}

// Using an IServiceScopeFactory to produce a local IServiceScope,
// use its .ServiceProvider to resolve services and finally destroy that IServiceScope
// and therefore the instances it has produced
void test1(IServiceScopeFactory serviceScopeFactory)
{
    using (IServiceScope scope = serviceScopeFactory.CreateScope())
    {
        InterfaceA? _interfaceA0 = scope.ServiceProvider.GetService<InterfaceA>();
    }
}

[Fact]
public void Test0()
{
    ServiceProvider services = null;
    try
    {
        this.Log($"(-)");
        services = new ServiceCollection()
            .AddScoped<InterfaceA, ClassA>()
            .BuildServiceProvider(validateScopes: true);

        using (IServiceScope scope = services.CreateScope())
        {
            test0(scope);
        }

        test1(services.GetService<IServiceScopeFactory>());
    }
    catch (Exception E)
    {
        this.Log(E);
    }
    finally
    {
        this.Log("services?.Dispose(-)");
        services?.Dispose();
        this.Log("services?.Dispose(+)");
    }
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
France France
I am a freelance software engineer living in Paris, France.

Comments and Discussions

 
-- There are no messages in this forum --