Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье приведены общие рекомендации по внедрению зависимостей в приложениях .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
. Кроме того, рассмотрите сценарий Одноразовые временные службы, удерживаемые контейнером, так как он имеет отношение к очистке ресурсов.
Замена стандартного контейнера служб
Встроенный контейнер служб предназначен для удовлетворения потребностей платформы и большинства клиентских приложений. Мы рекомендуем использовать встроенный контейнер, если только не требуется конкретная функциональная возможность, которую он не поддерживает, например:
- Внедрение свойств
- Внедрение на основе имени (только для .NET 7 и более ранних версий). Дополнительные сведения см. в разделе ключевые службы.
- Дочерние контейнеры
- Настраиваемое управление жизненным циклом
-
Func<T>
поддерживает отложенную инициализацию - Регистрация на основе соглашения
С приложениями ASP.NET Core можно использовать следующие сторонние контейнеры:
Потокобезопасность
Создавайте потокобезопасные службы синглтон. Если одноэлементный сервис имеет зависимость от временной службы, временная служба может также требовать потокобезопасности в зависимости от того, как она используется одноэлементным сервисом.
Метод фабрики одиночного сервиса, например второй аргумент AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), не должен быть потокобезопасным. Как и конструктор типов (static
), он гарантированно будет вызываться только один раз одним потоком.
Рекомендации
- Разрешение служб на основе
async/await
иTask
не поддерживается. Так как C# не поддерживает асинхронные конструкторы, следует использовать асинхронные методы после синхронного разрешения зависимостей службы. - Не храните данные и конфигурацию непосредственно в контейнере служб. Например, обычно не следует добавлять корзину пользователя в контейнер служб. Конфигурация должна использовать шаблон параметров. Аналогичным образом, избегайте объектов "хранения данных", которые служат лишь для доступа к другому объекту. Лучше запросить конкретный элемент через внедрение зависимостей.
- Избегайте статического доступа к службам. Например, не используйте везде IApplicationBuilder.ApplicationServices в качестве статического поля или свойства.
- Поддерживайте быструю и синхронную работу фабрик DI.
- Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать DI.
- Другой вариант локатора служб, которого нужно избегать, — это внедрение фабрики, разрешающей зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
- Избегайте вызовов BuildServiceProvider при настройке служб. Вызов
BuildServiceProvider
обычно происходит, когда разработчик хочет разрешить службу при регистрации другой службы. Вместо этого используйте перегрузку, которая включает в себяIServiceProvider
по этой причине. - Одноразовые временные службы захвачены контейнером для утилизации. Это может привести к утечке памяти, если разрешение выполняется в контейнере верхнего уровня.
- Включите проверку области, чтобы убедиться, что в приложении нет отдельных объектов, записывающих службы с заданной областью. Дополнительные сведения см. в разделе Проверка области.
Как и с любыми рекомендациями, могут возникнуть ситуации, когда необходимо игнорировать одну из них. Исключения возникают редко, — как правило, это особые случаи, связанные с самой платформой.
DI является альтернативой для шаблонов доступа к статическим или глобальным объектам. Вы можете не ощутить преимущества DI, если будете сочетать его с доступом к статическим объектам.
Примеры антишаблонов
В дополнение к основным рекомендациям этой статьи мы рекомендуем изучить несколько антишаблонов, которых следует избегать. Некоторые из этих антишаблонов представляют собой уроки, вынесенные из разработки самих сред выполнения.
Предупреждение
Это примеры антишаблонов. Не копируйте этот код и ни в коем случае не используйте такие действия.
Контейнер захватывает одноразовые транзитные службы
При регистрации транзитивных служб, которые реализуют IDisposable, по умолчанию контейнер зависимостей будет удерживать эти ссылки и не избавляться от них, пока не будет удалён сам контейнер — это произойдёт при остановке приложения, если службы были разрешены из контейнера, или до удаления области действий, если они были разрешены из этой области. Это может привести к утечке памяти, если это решается на уровне контейнера.
В предыдущем антишаблоне создаются и закрепляются в памяти экземпляры 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. Дополнительные сведения см. в статье о просмотре потоков и задач в окне "Параллельные стеки".
Зависимость от внешнего управления
Термин «захваченная зависимость» был введён Маркoм Симeном и описывает неправильную настройку времени существования службы, при которой служба с более длительным сроком существования удерживает службу с более коротким сроком существования.
В приведенном выше коде 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, похожее на строку "Не удается использовать службу "Bar" с заданной областью из службы "Foo" с одним экземпляром".
Дополнительные сведения см. в разделе Проверка области.
Выполнение в одном экземпляре службы с заданной областью
При использовании служб с ограниченной областью видимости, если вы не создаете новую область или не находитесь в существующей области, служба становится синглтоном.
В приведенном коде Bar
извлекается внутри IServiceScope, что является правильным. Антишаблоном здесь будет извлечение Bar
вне пределов области, и имя переменной avoid
подсказывает нам, какой пример извлечения неправилен.