With the release of ASP.NET Core, Microsoft introduced its own dependency injection container, Microsoft.Extensions.DependencyInjection (MS.DI), as an integral part of the Core framework. Microsoft designed MS.DI to simplify dependency management for framework and third-party component developers targeting ASP.NET Core. The intent was to establish a minimal, lowest-common-denominator feature set that other DI containers could adopt.
MS.DI can be used independently despite being integrated into ASP.NET Core. However, as we'll explore, its limited feature set makes it unsuitable for developing anything beyond trivial applications that embrace loose coupling and follow the patterns described throughout this book. The reason for dedicating coverage to MS.DI is that it appears similar to other DI containers at first glance, and developers need to understand its limitations to make informed decisions.
The usage pattern with MS.DI follows a two-step process: first configure the ServiceCollection, then build a ServiceProvider for component resolution. Unlike some containers, this two-step process is explicit in MS.DI.
Basic Object Resolution
At its core, any DI container's primary service is composing object graphs. MS.DI requires registration of all relevant components before resolution. Here's the fundamental usage pattern:
var serviceDescriptors = new ServiceCollection();
serviceDescriptors.AddTransient<HollandaiseSauce>();
ServiceProvider provider = serviceDescriptors.BuildServiceProvider(validateScopes: true);
using var scope = provider.CreateScope();
HollandaiseSauce sauce = scope.ServiceProvider.GetRequiredService<HollandaiseSauce>();
The ServiceCollection instance serves as MS.DI's configuration surface. You register components using methods like AddTransient. After configuration, you build a ServiceProvider that can resolve components. Importantly, you should always resolve from an IServiceScope rather than the root container to prevent memory leaks and concurrency issues.
Mapping Abstractions to Implementations
To achieve loose coupling, you typically map interfaces to concrete types:
var serviceDescriptors = new ServiceCollection();
serviceDescriptors.AddTransient<IIngredient, HollandaiseSauce>();
var provider = serviceDescriptors.BuildServiceProvider(true);
using var scope = provider.CreateScope();
IIngredient ingredient = scope.ServiceProvider.GetRequiredService<IIngredient>();
The AddTransient method creates a transient lifestyle mapping from the abstract type to its implementation. MS.DI also supports weak-typed resolution using GetRequiredService(Type) for scenarios where the type is only known at runtime.
Configuring the ServiceCollection
MS.DI primarily supports "configuration as code" without built-in support for configuration files or auto-registration. However, you can implement assembly scanning using .NET's LINQ and Reflection APIs.
Explicit Configuration
The most common configuration method uses extension methods:
serviceDescriptors.AddTransient<IIngredient, HollandaiseSauce>();
serviceDescriptors.AddTransient<IMeal, ThreeCourseMeal>();
When registering the same abstraction multiple times, the last registration wins for single resolution:
serviceDescriptors.AddTransient<IIngredient, HollandaiseSauce>();
serviceDescriptors.AddTransient<IIngredient, BeefSteak>();
For resolving all implementations, use GetServices or request IEnumerable<T>:
var ingredients = scope.ServiceProvider.GetServices<IIngredient>();
// or
var ingredients = scope.ServiceProvider.GetRequiredService<IEnumerable<IIngredient>>();
Convention-Based Registration
Despite lacking auto-registration APIs, you can implement assembly scanning:
Assembly ingredientAssembly = typeof(BeefSteak).Assembly;
var concreteIngredients = from type in ingredientAssembly.GetTypes()
where !type.IsAbstract
where typeof(IIngredient).IsAssignableFrom(type)
select type;
foreach (var type in concreteIngredients)
{
serviceDescriptors.AddTransient(typeof(IIngredient), type);
}
For generic abstractions like ICommandService<TCommand>, the scanning becomes more complex:
Assembly commandAssembly = typeof(UpdateInventoryCommandHandler).Assembly;
var commandMappings = from type in commandAssembly.GetTypes()
where !type.IsAbstract && !type.IsGenericType
from contract in type.GetInterfaces()
where contract.IsGenericType &&
contract.GetGenericTypeDefinition() == typeof(ICommandService<>)
select new { Service = contract, Implementation = type };
foreach (var mapping in commandMappings)
{
serviceDescriptors.AddTransient(mapping.Service, mapping.Implementation);
}
Lifetime Management
MS.DI supports three standard lifetimes:
- Transient: New instance per request
- Singleton: Single instance per container
- Scoped: Single instance per IServiceScope
Configuring Lifetimes
Lifetimes are configured during registration:
serviceDescriptors.AddSingleton<HollandaiseSauce>();
serviceDescriptors.AddScoped<IIngredient, HollandaiseSauce>();
serviceDescriptors.AddTransient<IMeal, ThreeCourseMeal>();
Scope Management
Always resolve from scopes to prevent memory leaks:
using (IServiceScope scope = provider.CreateScope())
{
IMeal meal = scope.ServiceProvider.GetRequiredService<IMeal>();
meal.Consume();
}
When the scope is disposed, all disposable components created within it are also disposed.
Advanced Registration Techniques
Handling Primitive Dependenceis
For primitive constructor parameters like strings or enums:
serviceDescriptors.AddSingleton(typeof(SpiceLevel), SpiceLevel.Medium);
serviceDescriptors.AddTransient<ICourse, ChiliConCarne>();
A better approach is extracting primitives into parameter objects:
public class Seasoning
{
public readonly SpiceLevel Heat;
public readonly bool ExtraSalt;
public Seasoning(SpiceLevel heat, bool extraSalt)
{
this.Heat = heat;
this.ExtraSalt = extraSalt;
}
}
var seasoning = new Seasoning(SpiceLevel.Medium, extraSalty: true);
serviceDescriptors.AddSingleton<Seasoning>(seasoning);
serviceDescriptors.AddTransient<ICourse, ChiliConCarne>();
Factory-Based Registration
For components requiring custom instantiation logic:
serviceDescriptors.AddTransient<IMeal>(sp =>
FastFoodFactory.CreateMeal("chicken combo"));
Working with Multiple Implementations
Resolving Ambiguous Dependencies
For constructors with multiple dependencies of the same type:
serviceDescriptors.AddTransient<IMeal>(sp => new ThreeCourseMeal(
appetizer: sp.GetRequiredService<Pate>(),
main: sp.GetRequiredService<ChickenCordonBleu>(),
dessert: sp.GetRequiredService<ChocolateMousse>()));
Alternatively, use ActivatorUtilities for partial auto-wiring:
serviceDescriptors.AddTransient<IMeal>(sp =>
ActivatorUtilities.CreateInstance<ThreeCourseMeal>(sp,
new object[] {
sp.GetRequiredService<Pate>(),
sp.GetRequiredService<ChickenCordonBleu>(),
sp.GetRequiredService<ChocolateMousse>() }));
Sequence Injection
MS.DI automatically injects all registered implementations when a constructor accepts IEnumerable<T>:
serviceDescriptors.AddTransient<ICourse, Appetizer>();
serviceDescriptors.AddTransient<ICourse, MainCourse>();
serviceDescriptors.AddTransient<ICourse, Dessert>();
serviceDescriptors.AddTransient<IMeal, MultiCourseMeal>();
For selective injection, manually compose the collection:
serviceDescriptors.AddTransient<IMeal>(sp => new MultiCourseMeal(
new ICourse[] {
sp.GetRequiredService<Appetizer>(),
sp.GetRequiredService<MainCourse>(),
sp.GetRequiredService<Dessert>()
}));
Decorator and Composite Patterns
Implementing Decorators
MS.DI lacks built-in decorator support, requiring manual composition:
serviceDescriptors.AddTransient<IIngredient>(sp =>
ActivatorUtilities.CreateInstance<BreadingDecorator>(sp,
ActivatorUtilities.CreateInstance<HamAndCheeseDecorator>(sp,
ActivatorUtilities.CreateInstance<VealCutlet>(sp))));
Generic Decorators
For generic abstractions, the complexity increases significantly:
Assembly commandAssembly = typeof(UpdateInventoryCommandHandler).Assembly;
var commandHandlers = from type in commandAssembly.GetTypes()
where !type.IsAbstract && !type.IsGenericType
from contract in type.GetInterfaces()
where contract.IsGenericType &&
contract.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
select new { Service = contract, Implementation = type };
foreach (var handler in commandHandlers)
{
Type commandType = handler.Service.GetGenericArguments()[0];
Type secureDecorator = typeof(SecurityDecorator<>).MakeGenericType(commandType);
Type transactionDecorator = typeof(TransactionDecorator<>).MakeGenericType(commandType);
Type auditDecorator = typeof(AuditDecorator<>).MakeGenericType(commandType);
serviceDescriptors.AddTransient(handler.Service, sp =>
ActivatorUtilities.CreateInstance(sp, secureDecorator,
ActivatorUtilities.CreateInstance(sp, transactionDecorator,
ActivatorUtilities.CreateInstance(sp, auditDecorator,
ActivatorUtilities.CreateInstance(sp, handler.Implementation)))));
}
Composite Pattern
For composites like CompositeNotificationService:
serviceDescriptors.AddTransient<EmailNotificationSender>();
serviceDescriptors.AddTransient<SmsNotificationSender>();
serviceDescriptors.AddTransient<PushNotificationSender>();
serviceDescriptors.AddTransient<INotificationService>(sp =>
new CompositeNotificationService(new INotificationService[] {
sp.GetRequiredService<EmailNotificationSender>(),
sp.GetRequiredService<SmsNotificationSender>(),
sp.GetRequiredService<PushNotificationSender>()
}));
Key Considerations
- Always resolve from IServiceScope, never from the root container
- Enable scope validation by using BuildServiceProvider(validateScopes: true)
- Be aware of torn lifestyles when registering the same type for multiple services
- MS.DI tracks most disposable components, including transients
- Generic decorator and composite implementations require significant boilerplate code
- Exception messages are often generic, making troubleshooting difficult
While MS.DI serves as a basic DI container for ASP.NET Core applications, its limitations become apparent when implementing advanced patterns. For production applications following SOLID principles and DI patterns, consider more feature-rich containers like Autofac or Simple Injector, or stick with Pure DI for better control and clarity.