Microsoft.Extensions.DependencyInjection Container Essentials

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.

Tags: Dependency Injection Microsoft.Extensions.DependencyInjection ASP.NET Core Lifetime Management Decorator Pattern

Posted on Fri, 14 Aug 2026 16:04:22 +0000 by daven