Рекомендации по внедрению зависимостейDependency injection guidelines

В этой статье приведены общие рекомендации по внедрению зависимостей в приложениях .NET.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

Контейнер отвечает за очистку создаваемых типов и вызывает Dispose для экземпляров IDisposable.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)
        {
            using 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:

  • Экземпляр ExampleService не создается контейнером службы.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.

Руководство по применению временных и общих экземпляров IDisposableIDisposable guidance for Transient and shared instances

Временный экземпляр, ограниченное время существованияTransient, limited lifetime

СценарийScenario

Приложению требуется экземпляр IDisposable с ограниченным временем существования для реализации любого из следующих сценариев: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. В этом случае приложение обычно имеет метод Create, который непосредственно вызывает конструктор окончательного типа.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

Приложению требуется общий экземпляр IDisposable в нескольких службах, но для экземпляра IDisposable требуется ограниченное время существования.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. Используйте IServiceScopeFactory.CreateScope для создания нового IServiceScope.Use IServiceScopeFactory.CreateScope to create a new IServiceScope. Используйте IServiceProvider области для получения необходимых служб.Use the scope's IServiceProvider to get required services. Удалите область, если она больше не нужна.Dispose the scope when it's no longer needed.

Общие рекомендации для IDisposableGeneral IDisposable guidelines

  • Не регистрируйте экземпляры IDisposable с временным временем существования.Don't register IDisposable instances with a transient lifetime. Вместо этого используйте шаблон фабрики.Use the factory pattern instead.
  • Не разрешайте экземпляры IDisposable с временным временем существования или временем существования с заданной областью в корневую область.Don't resolve IDisposable instances with a transient or scoped lifetime in the root scope. Единственное исключение — это когда приложение создает или повторно создает и удаляет IServiceProvider, но это не является идеальным вариантом.The only exception to this is if the app creates/recreates and disposes IServiceProvider, but this isn't an ideal pattern.
  • Для получения зависимости IDisposable через DI не требуется, чтобы получатель реализовывал сам интерфейс IDisposable.Receiving an IDisposable dependency via DI doesn't require that the receiver implement IDisposable itself. Получатель зависимости IDisposable не должен вызывать Dispose для этой зависимости.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.

Дополнительные сведения об очистке ресурсов см. в статьях Реализация метода Disposeили Реализация метода DisposeAsync.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> поддерживает отложенную инициализациюFunc<T> support for lazy initialization
  • Регистрация на основе соглашенияConvention-based registration

С приложениями ASP.NET Core можно использовать следующие сторонние контейнеры: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.

Фабричный метод отдельной службы, например второй аргумент в AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), не обязательно должен быть потокобезопасным.The factory method of a singleton service, such as the second argument to AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need to be thread-safe. Как и конструктор типов (static), он гарантированно будет вызываться только один раз одним потоком.Like a type (static) constructor, it's guaranteed to be called only once by a single thread.

РекомендацииRecommendations

  • Разрешение служб на основе async/await и Task не поддерживается.async/await and Task based service resolution isn't supported. Так как C# не поддерживает асинхронные конструкторы, следует использовать асинхронные методы после асинхронного разрешения службы.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. Например, не используйте везде IApplicationBuilder.ApplicationServices в качестве статического поля или свойства.For example, avoid capturing IApplicationBuilder.ApplicationServices as a static field or property for use elsewhere.
  • Обеспечьте высокую скорость и синхронизацию фабрик DI.Keep DI factories fast and synchronous.
  • Старайтесь не использовать шаблон обнаружения служб.Avoid using the service locator pattern. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей.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.
  • Избегайте вызовов BuildServiceProvider в ConfigureServices.Avoid calls to BuildServiceProvider in ConfigureServices. Вызов BuildServiceProvider обычно происходит, когда разработчику необходимо разрешить службу в 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.

Предупреждение

Это примеры антишаблонов. Не копируйте этот код и ни в коем случае не используйте такие действия.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

При регистрации временных служб, которые реализуют IDisposable, по умолчанию контейнер внедрения будет удерживать эти ссылки, не избавляясь от них методом Dispose(), пока не будет удален сам контейнер — то есть когда остановится приложение, если ссылки были разрешены из контейнера, или будет удалена область действия, если они были разрешены из этой области.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 container is disposed when application stops if they were resolved from the container, or until the scope is disposed if they were resolved from a scope. Это может привести к утечке памяти, если разрешение выполняется на уровне контейнера.This can turn into a memory leak if resolved from container level.

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();
}

В предыдущем антишаблоне создаются и размещаются на корневом уровне экземпляры 1000 объектов ExampleDisposable.In the preceding anti-pattern, 1,000 ExampleDisposable objects are instantiated and rooted. Они не будут удалены, пока существует экземпляр serviceProvider.They will not be disposed of until the serviceProvider instance is disposed.

Дополнительные сведения об отладке утечек памяти см. в статье Отладка утечки памяти в .NET Core.For more information on debugging memory leaks, see Debug a memory leak in .NET.

Фабрики асинхронного внедрения могут вызвать взаимоблокировкиAsync DI factories can cause deadlocks

Термин "фабрики асинхронного внедрения" обозначает методы перегрузки, которые существуют при вызове Add{LIFETIME}.The term "DI factories" refers to the overload methods that exist when calling Add{LIFETIME}. Некоторые перегрузки принимают Func<IServiceProvider, T>, где T обозначает регистрируемую службу, а параметр имеет имя implementationFactory.There are overloads accepting a Func<IServiceProvider, T> where T is the service being registered, and the parameter is named implementationFactory. implementationFactory можно предоставить как лямбда-выражение, локальную функцию или метод.The implementationFactory can be provided as a lambda expression, local function, or method. Если фабрика является асинхронной и используется Task<TResult>.Result, происходит взаимоблокировка.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>();
}

В приведенном выше коде объект implementationFactory получает лямбда-выражение, в котором тело вызывает Task<TResult>.Result для метода возврата Task<Bar>.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. Метод GetBarAsync эмулирует асинхронную работу с использованием Task.Delay, а затем вызывает GetRequiredService<T>(IServiceProvider).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. Дополнительные сведения об отладке взаимоблокировок см. в статье Отладка взаимоблокировки в .NET Core.For more information debugging deadlocks, see Debug a deadlock in .NET.

Когда при использовании этого антишаблона возникает взаимоблокировка, вы можете изучить два ожидающих потока в окне параллельных стеков Visual Studio.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

Термин "зависимость с захватом" был предложен Марком Симаном (Mark Seeman) для обозначения ситуаций с неверной настройкой времени существования службы, когда более длительно выполняемая служба "захватывает" службы с более коротким временем существования.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>();

В приведенном выше коде Foo регистрируется как служба с одним экземпляром, а для Bar ограничена область действия. На первый взгляд, все нормально.In the preceding code, Foo is registered as a singleton and Bar is scoped - which on the surface seems valid. Но давайте рассмотрим реализацию Foo.However, consider the implementation of Foo.

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

Объекту Foo требуется объект Bar, и возникает ошибка конфигурации, так как Foo работает с одним экземпляром, а Bar имеет ограниченную область действия.The Foo object requires a Bar object, and since Foo is a singleton, and Bar is scoped - this is a misconfiguration. В таком варианте экземпляр Foo будет создан только один раз, и он будет удерживать Bar в течение всего времени существования, что превышает предполагаемое время существования для службы Bar с ограниченной областью действия.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. Мы рекомендуем проверить области, передав validateScopes: true в BuildServiceProvider(IServiceCollection, Boolean).You should consider validating scopes, by passing validateScopes: true to the BuildServiceProvider(IServiceCollection, Boolean). При проверке областей вы получите сообщение InvalidOperationException, похожее на строку "Не удается использовать службу "Bar" с заданной областью из службы "Foo" с одним экземпляром".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>();
}

В приведенном выше коде Bar извлекается в IServiceScope, что является верным.In the preceding code, Bar is retrieved within an IServiceScope, which is correct. Антишаблоном здесь будет извлечение Bar вне пределов области, и имя переменной avoid подсказывает нам, какой пример извлечения неправилен.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