Download latest Repository Archive
Download local copy
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:
Dependency inversion
Dependency injection in .NET
Tutorial: Use dependency injection in .NET
Dependency injection guidelines
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.
public
interface
IServiceCollection: ICollection<ServiceDescriptor>,
IEnumerable<ServiceDescriptor>,
IList<ServiceDescriptor>
A
ServiceProvider
takes the form of an object instance exposing the
IServiceProvider
interface.
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:
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
The
IServiceProvider
interface exposes a single method:
public
object
? GetService (Type serviceType);
The
ServiceProviderServiceExtensions
class provides many extensions to this interface as variations of
GetService
,
GetRequiredService
,
GetServices
,
CreateScope
,
CreateAsyncScope
:
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:
public
void
Test_GetServices()
ServiceProvider? _serviceProvider =
null
;
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:
public
interface
IServiceScope: IDisposable
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
ServiceProvider? _serviceProvider =
null
;
ClassD _classD =
new
ClassD();
_serviceProvider =
new
ServiceCollection()
.AddSingleton<InterfaceA, ClassA>()
.AddSingleton<ClassD>(_classD)
.BuildServiceProvider();
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
ServiceProvider? _serviceProvider =
null
;
_serviceProvider =
new
ServiceCollection()
.AddTransient<InterfaceB, ClassB>()
.BuildServiceProvider();
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
bool
validateScopes =
true
;
ServiceProvider? _serviceProvider =
null
;
ServiceProviderOptions serviceProviderOptions =
new
ServiceProviderOptions()
ValidateScopes = validateScopes,
ClassD _classD =
new
ClassD();
_serviceProvider =
new
ServiceCollection()
.AddScoped<ClassC>()
.BuildServiceProvider(options: serviceProviderOptions);
bool
_thrown =
false
;
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
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
;
this
.Log(
"
(-)"
);
_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}"
);
bool
_thrown =
false
;
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:
public
sealed
class
ServiceProvider: IServiceProvider, IDisposable, IAsyncDisposable
The
IServiceScope
interface is disposable:
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
ServiceProvider? _serviceProvider =
null
;
this
.Log(
"
(-)"
);
_serviceProvider =
new
ServiceCollection()
.AddTransient<DisposableClassA>()
.BuildServiceProvider();
DisposableClassA disposableClassA = _serviceProvider.GetService<DisposableClassA>();
this
.Log($
"
disposableClassA={disposableClassA}"
);
finally
this
.Log(
"
_serviceProvider?.Dispose(-)"
);
_serviceProvider?.Dispose();
this
.Log(
"
_serviceProvider?.Dispose(+)"
);
'scoped' container
The following code:
ServiceProvider? _serviceProvider =
null
;
this
.Log(
"
(-)"
);
_serviceProvider =
new
ServiceCollection()
.AddSingleton<DisposableClassA>()
.AddTransient<DisposableClassB>()
.AddScoped<DisposableClassC>()
.BuildServiceProvider();
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
class
DisposableClassA: Disposable0, IDisposableA