Dependency injection has been a fairly standard part of development for a while. You’ve probably used Unity, Autofac, Ninject and others in the past.
Frameworks, such as ASP.NET core and MAUI use the Microsoft Dependency Injection package (Microsoft.Extensions.DependencyInjection) and we can use this with any other type of application.
For example if we create ourselves a Console application, then add the package Microsoft.Extensions.DependencyInjection. Now can then use the following code
var serviceCollection = new ServiceCollection();
// add our services
var serviceProvider = serviceCollection.BuildServiceProvider();
and it’s as simple as that.
The Microsoft.Extensions.DependencyInjection has most of the features we require for most dependency injection scenarios (Note: it does not support property injection for example). We can add services as…
- Transient an instance created for every request, for example
serviceCollection.AddTransient<IPipeline, Pipeline>();
// or
serviceCollection.AddTransient<Pipeline>();
- Singleton a single instance created and reused on every request, for example
serviceCollection.AddSingleton<IPipeline, Pipeline>();
// or
serviceCollection.AddSingleton<Pipeline>();
- Scoped when we create a scope we get the same instance within the scope. In ASP.NET core a scope is created for each request
serviceCollection.AddScoped<IPipeline, Pipeline>();
// or
serviceCollection.AddScoped<Pipeline>();
For the services registered as “scoped”, if no scope is created then the code will work more or less like a singleton, i.e. the scope is the whole application, but if we want to mimic ASP.NET (for example) we would create a scope per request and we would do this by using the following
using var scope = serviceProvider.CreateScope();
var pipeline1 = scope.ServiceProvider.GetRequiredService<Pipeline>();
var pipeline2 = scope.ServiceProvider.GetRequiredService<Pipeline>();
in the above code the same instance of the Pipeline is returned for each GetRequiredService call, but when the scope is disposed of or another scope created then a new instance for that scope will be returned.
The service provider is used to create/return instances of our services. We can use GetRequiredService which will throw and InvalidOperationException if the service is not registered or we might use GetService which will not throw an exception but will either return the instance or null.
Multiple services of the same type
If we register multiple implementations of our services like this
serviceCollection.AddTransient<IPipeline, Pipeline1>();
serviceCollection.AddTransient<IPipeline, Pipeline2>();
and we use the service provider and use GetRequiredService<IPipeline> we will get a Pipeline2 – it will be the the last registered type.
If we want to get all services registered for type IPipeline then we use GetServices<IPipeline> and we’ll get an IEnumerable of IPipelines, so if we have a service which take an IPipeline, we’d need to declare it as follows
public class Context(IEnumerable<IPipeline> pipeline)
{
}
Finally we have the keyed option, this is allows use to register multiple variations of an interface (for example) and give each a key/name, for example
serviceCollection.AddKeyedTransient<IPipeline, Pipeline1>("one");
serviceCollection.AddKeyedTransient<IPipeline, Pipeline2>("two");
Now these will not be returned when using GetServices<IPipeline> instead it’s expected that we get the service by the key, i.e.
var pipeline = serviceProvider.GetKeyedService<IPipeline>("one");
When declaring the requirement in our dependent classes we would use the FromKeyedServicesAttribute like this
public class Context([FromKeyedServices("one")] IPipeline pipeline)
{
}