Dependency injection guidelines

This article provides general guidelines and best practices for implementing dependency injection in .NET applications.

Design services for dependency injection

When designing services for dependency injection:

  • Avoid stateful, static classes and members. Avoid creating global state by designing apps to use singleton services instead.
  • Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation.
  • Make services small, well-factored, and easily tested.

If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP). Attempt to refactor the class by moving some of its responsibilities into new classes.

Disposal of services

The container is responsible for cleanup of types it creates, and calls Dispose on IDisposable instances. Services resolved from the container should never be disposed by the developer. If a type or factory is registered as a singleton, the container disposes the singleton automatically.

In the following example, the services are created by the service container and disposed automatically:

using System;

namespace ConsoleDisposable.Example
{
    public class TransientDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
    }
}

The preceding disposable is intended to have a transient lifetime.

using System;

namespace ConsoleDisposable.Example
{
    public class ScopedDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
    }
}

The preceding disposable is intended to have a scoped lifetime.

using System;

namespace ConsoleDisposable.Example
{
    public class SingletonDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
    }
}

The preceding disposable is intended to have a singleton lifetime.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDisposable.Example
{
    class Program
    {
        static Task Main(string[] args)
        {
            IHost host = CreateHostBuilder(args).Build();

            ExemplifyDisposableScoping(host.Services, "Scope 1");
            Console.WriteLine();

            ExemplifyDisposableScoping(host.Services, "Scope 2");
            Console.WriteLine();

            return host.RunAsync();
        }

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddTransient<TransientDisposable>()
                            .AddScoped<ScopedDisposable>()
                            .AddSingleton<SingletonDisposable>());

        static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
        {
            Console.WriteLine($"{scope}...");

            using IServiceScope serviceScope = services.CreateScope();
            IServiceProvider provider = serviceScope.ServiceProvider;

            _ = provider.GetRequiredService<TransientDisposable>();
            _ = provider.GetRequiredService<ScopedDisposable>();
            _ = provider.GetRequiredService<SingletonDisposable>();
        }
    }

The debug console shows the following sample output after running:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Services not created by the service container

Consider the following code:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new ExampleService());

    // ...
}

In the preceding code:

  • The ExampleService instance is not created by the service container.
  • The framework does not dispose of the services automatically.
  • The developer is responsible for disposing the services.

IDisposable guidance for Transient and shared instances

Transient, limited lifetime

Scenario

The app requires an IDisposable instance with a transient lifetime for either of the following scenarios:

  • The instance is resolved in the root scope (root container).
  • The instance should be disposed before the scope ends.

Solution

Use the factory pattern to create an instance outside of the parent scope. In this situation, the app would generally have a Create method that calls the final type's constructor directly. If the final type has other dependencies, the factory can:

Shared instance, limited lifetime

Scenario

The app requires a shared IDisposable instance across multiple services, but the IDisposable instance should have a limited lifetime.

Solution

Register the instance with a scoped lifetime. Use IServiceScopeFactory.CreateScope to create a new IServiceScope. Use the scope's IServiceProvider to get required services. Dispose the scope when it's no longer needed.

General IDisposable guidelines

  • Don't register IDisposable instances with a transient lifetime. Use the factory pattern instead.
  • Don't resolve IDisposable instances with a transient or scoped lifetime in the root scope. The only exception to this is if the app creates/recreates and disposes IServiceProvider, but this isn't an ideal pattern.
  • Receiving an IDisposable dependency via DI doesn't require that the receiver implement IDisposable itself. The receiver of the IDisposable dependency shouldn't call Dispose on that dependency.
  • Use scopes to control the lifetimes of services. Scopes aren't hierarchical, and there's no special connection among scopes.

For more information on resource cleanup, see Implement a Dispose method, or Implement a DisposeAsync method. Additionally, consider the Disposable transient services captured by container scenario as it relates to resource cleanup.

Default service container replacement

The built-in service container is designed to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support, such as:

  • Property injection
  • Injection based on name
  • Child containers
  • Custom lifetime management
  • Func<T> support for lazy initialization
  • Convention-based registration

The following third-party containers can be used with ASP.NET Core apps:

Thread safety

Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton.

The factory method of single service, such as the second argument to AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need to be thread-safe. Like a type (static) constructor, it's guaranteed to be called only once by a single thread.

Recommendations

  • async/await and Task based service resolution isn't supported. Because C# doesn't support asynchronous constructors, use asynchronous methods after synchronously resolving the service.
  • Avoid storing data and configuration directly in the service container. For example, a user's shopping cart shouldn't typically be added to the service container. Configuration should use the options pattern. Similarly, avoid "data holder" objects that only exist to allow access to another object. It's better to request the actual item via DI.
  • Avoid static access to services. For example, avoid capturing IApplicationBuilder.ApplicationServices as a static field or property for use elsewhere.
  • Keep DI factories fast and synchronous.
  • Avoid using the service locator pattern. For example, don't invoke GetService to obtain a service instance when you can use DI instead.
  • Another service locator variation to avoid is injecting a factory that resolves dependencies at runtime. Both of these practices mix Inversion of Control strategies.
  • Avoid calls to BuildServiceProvider in ConfigureServices. Calling BuildServiceProvider typically happens when the developer wants to resolve a service in ConfigureServices.
  • Disposable transient services are captured by the container for disposal. This can turn into a memory leak if resolved from the top-level container.
  • Enable scope validation to make sure the app doesn't have singletons that capture scoped services. For more information, see Scope validation.

Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. Exceptions are rare, mostly special cases within the framework itself.

DI is an alternative to static/global object access patterns. You may not be able to realize the benefits of DI if you mix it with static object access.

Example anti-patterns

In addition to the guidelines in this article, there are several anti-patterns you should avoid. Some of these anti-patterns are learnings from developing the runtimes themselves.

Warning

These are example anti-patterns, do not copy the code, do not use these patterns, and avoid these patterns at all costs.

Disposable transient services captured by container

When you register Transient services that implement IDisposable, by default the DI container will hold onto these references, and not Dispose() of them until the application stops. This can turn into a memory leak if resolved from the level container.

static void TransientDisposablesWithoutDispose()
{
    var services = new ServiceCollection();
    services.AddTransient<ExampleDisposable>();
    ServiceProvider serviceProvider = services.BuildServiceProvider();

    for (int i = 0; i < 1000; ++ i)
    {
        _ = serviceProvider.GetRequiredService<ExampleDisposable>();
    }

    // serviceProvider.Dispose();
}

In the preceding anti-pattern, 1,000 ExampleDisposable objects are instantiated and rooted. They will not be disposed of until the serviceProvider instance is disposed.

For more information on debugging memory leaks, see Debug a memory leak in .NET.

Async DI factories can cause deadlocks

The term "DI factories" refers to the overload methods that exist when calling Add{LIFETIME}. There are overloads accepting a Func<IServiceProvider, T> where T is the service being registered, and the parameter is named implementationFactory. The implementationFactory can be provided as a lambda expression, local function, or method. If the factory is asynchronous, and you use Task<TResult>.Result, this will cause a deadlock.

static void DeadLockWithAsyncFactory()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>(implementationFactory: provider =>
    {
        Bar bar = GetBarAsync(provider).Result;
        return new Foo(bar);
    });

    services.AddSingleton<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    _ = serviceProvider.GetRequiredService<Foo>();
}

In the preceding code, the implementationFactory is given a lambda expression where the body calls Task<TResult>.Result on a Task<Bar> returning method. This causes a deadlock. The GetBarAsync method simply emulates an asynchronous work operation with Task.Delay, and then calls GetRequiredService<T>(IServiceProvider).

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
    // Emulate asynchronous work operation
    await Task.Delay(1000);

    return serviceProvider.GetRequiredService<Bar>();
}

For more information on asynchronous guidance, see Asynchronous programming: Important info and advice. For more information debugging deadlocks, see Debug a deadlock in .NET.

When you're running this anti-pattern and the deadlock occurs, you can view the two threads waiting from Visual Studio's Parallel Stacks window. For more information, see View threads and tasks in the Parallel Stacks window.

Captive dependency

The term "captive dependency" was coined by Mark Seeman, and refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive.

static void CaptiveDependency()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    // Enable scope validation
    // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

    _ = serviceProvider.GetRequiredService<Foo>();

In the preceding code, Foo is registered as a singleton and Bar is scoped - which on the surface seems valid. However, consider the implementation of Foo.

namespace DependencyInjection.AntiPatterns
{
    public class Foo
    {
        public Foo(Bar bar)
        {
        }
    }
}

The Foo object requires a Bar object, and since Foo is a singleton, and Bar is scoped - this is a misconfiguration. As is, Foo would only be instantiated once, and it would hold onto Bar for its lifetime, which is longer than the intended scoped lifetime of Bar. You should consider validating scopes, by passing validateScopes: true to the BuildServiceProvider(IServiceCollection, Boolean). When you validate the scopes, you'd get an InvalidOperationException with a message similar to "Cannot consume scoped service 'Bar' from singleton 'Foo'.".

For more information, see Scope validation.

Scoped service as singleton

When using scoped services, if you're not creating a scope or within an existing scope - the service becomes a singleton.

static void ScopedServiceBecomesSingleton()
{
    var services = new ServiceCollection();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
    using (IServiceScope scope = serviceProvider.CreateScope())
    {
        // Correctly scoped resolution
        Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
    }

    // Not within a scope, becomes a singleton
    Bar avoid = serviceProvider.GetRequiredService<Bar>();
}

In the preceding code, Bar is retrieved within an IServiceScope, which is correct. The anti-pattern is the retrieval of Bar outside of the scope, and the variable is named avoid to show which example retrieval is incorrect.

See also