Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье приведены общие рекомендации и лучшие практики по инъекции зависимостей (DI) в приложениях .NET.
Проектирование сервисов для внедрения зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:
- Избегайте классов и членов, которые сохраняют состояние, статических. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
- Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров связывает код с конкретной реализацией.
- Сделайте сервисы небольшими, хорошо структурированными и удобными для тестирования.
Если в классе много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы.
Ликвидация услуг
Контейнер отвечает за очистку создаваемых типов и вызывает Dispose для экземпляров IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удаляет одноэлементный объект.
В следующем примере службы создаются контейнером сервисов и автоматически освобождаются.
namespace ConsoleDisposable.Example;
public sealed class TransientDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}
Предыдущий удаляемый объект должен иметь временное существование.
namespace ConsoleDisposable.Example;
public sealed class ScopedDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}
Предшествующий одноразовый элемент предназначен для ограниченного времени жизни.
namespace ConsoleDisposable.Example;
public sealed class SingletonDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}
Этот одноразовый объект предназначен для существования в режиме одиночного экземпляра.
using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();
using IHost host = builder.Build();
ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();
ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();
await host.RunAsync();
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>();
}
Консоль отладки после выполнения отображает следующие выходные данные:
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()
Службы, не созданные контейнером службы
Рассмотрим следующий код:
// Register example service in IServiceCollection.
builder.Services.AddSingleton(new ExampleService());
В предыдущем коде:
- Экземпляр
ExampleServiceне создается контейнером службы. - Платформа не удаляет службы автоматически.
- За распоряжение службами отвечает разработчик.
Руководство по применению IDisposable для временных и общих экземпляров
Временный, ограниченный срок службы
Сценарий
Приложению требуется экземпляр IDisposable с ограниченным временем существования для реализации любого из следующих сценариев:
- Экземпляр создаётся в корневой области (корневом контейнере).
- Экземпляр должен быть удален до окончания области действия.
Решение
Используйте фабричный метод для создания экземпляра за пределами родительской области. В этой ситуации приложение обычно имеет Create метод, который вызывает конструктор конечного типа напрямую. Если окончательный тип имеет другие зависимости, фабрика может:
- Получить IServiceProvider в своем конструкторе.
- Используйте ActivatorUtilities.CreateInstance, чтобы создать экземпляр за пределами контейнера, используя контейнер для его зависимостей.
Общий экземпляр, ограниченное время существования
Сценарий
Приложению требуется общий экземпляр IDisposable в нескольких службах, но для экземпляра IDisposable требуется ограниченное время существования.
Решение
Зарегистрируйте экземпляр с областью действия с заданным временем существования. Используйте IServiceScopeFactory.CreateScope для создания нового IServiceScope. Используйте IServiceProvider области, чтобы получить необходимые сервисы. Удалите контекст, когда он больше не нужен.
Общие рекомендации IDisposable
- Не регистрируйте экземпляры IDisposable с кратковременным временем существования. Используйте шаблон фабрики, чтобы решаемая служба могла быть удалена вручную, когда она больше не используется.
- Не разрешайте экземпляры IDisposable с временным или ограниченным временем существования в корневой области. Единственным исключением из этого является случай, когда приложение создает, повторно создает и удаляет IServiceProvider, но это не идеальный паттерн.
- Для получения зависимости IDisposable через DI нет необходимости, чтобы получатель сам выполнял IDisposable. Получатель зависимости IDisposable не должен вызывать Dispose для этой зависимости.
- Используйте области для управления сроками существования служб. Области не являются иерархическими, и между ними нет специальной связи.
Дополнительные сведения об очистке ресурсов см. в разделе реализация метода Dispose или реализация метода DisposeAsync. Кроме того, рассмотрите сценарий Одноразовые временные службы, удерживаемые контейнером, так как он имеет отношение к очистке ресурсов.
Замена стандартного контейнера служб
Встроенный контейнер служб предназначен для удовлетворения потребностей платформы и большинства клиентских приложений. Мы рекомендуем использовать встроенный контейнер, если только не требуется конкретная функциональная возможность, которую он не поддерживает, например:
- Внедрение свойств
- Дочерние контейнеры
- Настраиваемое управление жизненным циклом
-
Func<T>поддерживает отложенную инициализацию - Регистрация на основе соглашения
С приложениями ASP.NET Core можно использовать следующие сторонние контейнеры:
Потокобезопасность
Создавайте потокобезопасные службы синглтон. Если одноэлементная служба имеет зависимость от временной службы, временная служба также может требовать безопасности потоков в зависимости от того, как она используется одноэлементным. Метод фабрики одиночного сервиса, например второй аргумент AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), не должен быть потокобезопасным. Как и конструктор типов (static), он гарантированно будет вызываться только один раз одним потоком.
Кроме того, процесс разрешения служб из встроенного контейнера внедрения зависимостей .NET является потокобезопасным.
IServiceProvider или IServiceScope создание позволяет безопасно разрешать службы параллельно из нескольких потоков.
Замечание
Безопасность потока самого контейнера DI гарантирует, что создание и разрешение служб безопасно. Это не делает разрешенные экземпляры службы потокобезопасными сами по себе.
Любая служба (особенно одноэлементные) с общим мутируемым состоянием должна реализовать собственную логику синхронизации при одновременном доступе.
Рекомендации
- Разрешение служб на основе
async/awaitиTaskне поддерживается. Так как C# не поддерживает асинхронные конструкторы, следует использовать асинхронные методы после синхронного разрешения зависимостей службы. - Не храните данные и конфигурацию непосредственно в контейнере служб. Например, обычно не следует добавлять корзину пользователя в контейнер служб. Конфигурация должна использовать шаблон параметров. Аналогичным образом, избегайте объектов "хранения данных", которые служат лишь для доступа к другому объекту. Лучше запросить конкретный элемент через внедрение зависимостей.
- Избегайте статического доступа к службам. Например, не используйте везде IApplicationBuilder.ApplicationServices в качестве статического поля или свойства.
- Поддерживайте быструю и синхронную работу фабрик DI.
- Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать DI.
- Другой вариацией паттерна локатора сервиса, которой следует избегать, является внедрение фабрики, решающей зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
- Избегайте вызовов BuildServiceProvider при настройке служб. Вызов
BuildServiceProviderобычно происходит, когда разработчик хочет разрешить службу при регистрации другой службы. Вместо этого используйте перегрузку, которая включает в себяIServiceProviderпо этой причине. - Одноразовые временные службы захвачены контейнером для утилизации. Это может привести к утечке памяти, если разрешение выполняется в контейнере верхнего уровня.
- Включите проверку области, чтобы убедиться, что в приложении нет отдельных объектов, записывающих службы с заданной областью. Дополнительные сведения см. в разделе Проверка области.
- Используйте Singleton только для служб, имеющих собственное состояние, которое дорого создавать, или для служб, которое необходимо глобально разделять. Избегайте использования одноэлементного времени существования для служб, которые сами по себе не имеют состояния. Большинство контейнеров Интернета вещей .NET используют "Transient" в качестве области по умолчанию. Особенности и недостатки синглтонов:
- Безопасность потоков: синглтон должен быть реализован в потокобезопасной манере.
- Связь: она может объединять запросы, которые в противном случае не связаны друг с другом.
- Проблемы тестирования: общие данные и связанность могут усложнять модульное тестирование.
- Влияние на память: синглтон может удерживать в памяти большой граф объектов в течение всего жизненного цикла приложения.
- Отказоустойчивость: В случае, если одиночный экземпляр или любая часть его дерева зависимостей вышли из строя, восстановить его работоспособность сложно.
- Перезагрузка конфигурации. Как правило, однотонные не могут поддерживать "горячую перезагрузку" значений конфигурации.
- Утечка области: синглтон может непреднамеренно захватывать скоупные или транзиентные зависимости, фактически превращая их в синглтоны и вызывая непреднамеренные побочные эффекты.
- Затраты на инициализацию. При разрешении службы контейнер IoC должен искать одноэлементный экземпляр. Если он еще не существует, он должен создать его в потокобезопасном режиме. В отличие от этого, бессостояточная временная служба очень дешева для создания и уничтожения.
Как и все наборы рекомендаций, могут возникнуть ситуации, когда игнорирование рекомендации необходимо. Исключения являются редкими и в основном представляют собой особые случаи в самом фреймворке.
DI является альтернативой для шаблонов доступа к статическим или глобальным объектам. Возможно, вы не понимаете преимущества DI, если вы смешиваете его со статическим доступом к объекту.
Примеры антишаблонов
Помимо рекомендаций в этой статье, следует избегать нескольких антишаблонов. Некоторые из этих антишаблонов представляют собой уроки, вынесенные из разработки самих сред выполнения.
Предупреждение
Это примеры антипаттернов. Ни в коем случае не копируйте код, не используйте эти шаблоны и избегайте этих шаблонов любой ценой.
Контейнер захватывает одноразовые транзитные службы
При регистрации временных служб, реализующих IDisposable, по умолчанию контейнер DI сохраняет эти ссылки. Он не удаляет их, пока контейнер не будет удален при остановке приложения, если они были разрешены из контейнера, или пока область не будет удалена, если они были разрешены из области. Утечка памяти может произойти, если она разрешена на уровне контейнера.
В предыдущем антишаблоне создаются и закрепляются в памяти экземпляры 1000 объектов ExampleDisposable. Они не будут удалены, пока serviceProvider экземпляр не будет удален.
Дополнительные сведения об отладке утечек памяти см. в статье Отладка утечки памяти в .NET Core.
Фабрики асинхронного внедрения могут вызвать взаимоблокировки
Термин "фабрики асинхронного внедрения" обозначает методы перегрузки, которые существуют при вызове Add{LIFETIME}. Существуют перегрузки, которые принимают Func<IServiceProvider, T>, где T — это регистрируемая служба, и параметр называется implementationFactory.
implementationFactory можно предоставить как лямбда-выражение, локальную функцию или метод. Если фабрика асинхронна и используется Task<TResult>.Result, это приведет к взаимоблокировке.
В приведенном выше коде объект implementationFactory получает лямбда-выражение, в котором тело вызывает Task<TResult>.Result для метода возврата Task<Bar>. Это вызывает взаимоблокировку. Метод GetBarAsync эмулирует асинхронную работу с использованием Task.Delay, а затем вызывает GetRequiredService<T>(IServiceProvider).
Дополнительные сведения об асинхронной работе см. в статье Асинхронное программирование: важная информация и советы. Дополнительные сведения об отладке взаимоблокировок см. в статье Отладка взаимоблокировки в .NET Core.
Когда при использовании этого антипаттерна возникает взаимоблокировка, вы можете просмотреть два ожидающих потока в окне параллельных стеков Visual Studio. Дополнительные сведения см. в статье о просмотре потоков и задач в окне "Параллельные стеки".
Зависимость от внешнего управления
Термин "неволительная зависимость", придуманный Марком Seemann, относится к неправильной настройке времени существования службы, когда более длительно существующая служба удерживает зависимую службу с более коротким сроком существования.
В приведенном выше коде Foo регистрируется как singleton, а Bar регистрация ограничена областью действия, что на первый взгляд выглядит корректным. Но давайте рассмотрим реализацию Foo.
namespace DependencyInjection.AntiPatterns;
public class Foo(Bar bar)
{
}
Для Foo объекта требуется Bar объект, и так как Foo - одиночный объект, а Bar имеет область применения, это является неправильной настройкой. В текущем состоянии экземпляр Foo создается только один раз и удерживает Bar в течение его срока жизни, который длиннее предполагаемого срока жизни Bar. Рассмотрите возможность проверки областей путем передачи validateScopes: true в BuildServiceProvider(IServiceCollection, Boolean). При проверке областей вы получите InvalidOperationException с сообщением, аналогичным "Невозможно использовать область службы 'Бар' из одноэлементной 'Foo'.".
Дополнительные сведения см. в разделе Проверка области.
Выполнение в одном экземпляре службы с заданной областью
При использовании служб с областью действия, если вы не создаете область или работаете в рамках существующей области, служба становится синглетоном.
В приведенном коде Bar извлекается внутри IServiceScope, что является правильным. Антишаблоном здесь будет извлечение Bar вне пределов области, и имя переменной avoid подсказывает нам, какой пример извлечения неправилен.