Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Примечание.
Это не последняя версия этой статьи. В текущем выпуске смотрите версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске смотрите версию этой статьи для .NET 9.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске смотрите статью о версии .NET 9.
Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith) и Брэндон Далер (Brandon Dahler)
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.
Руководство по Blazor DI, которое дополняет или заменяет руководство в этой статье, см. в ASP.NET Core Blazor внедрении зависимостей.
Сведения, относящиеся к внедрению зависимостей в контроллерах MVC, см. в разделе внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.
Сведения о внедрении зависимостей см. в шаблоне параметров в ASP.NET Core.
В этой статье содержатся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.
Просмотреть или скачать образец кода (описание загрузки)
Общие сведения о внедрении зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency
с методом WriteMessage
, от которого зависят другие классы:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
Класс может создать экземпляр класса MyDependency
, чтобы использовать его метод WriteMessage
. В следующем примере класс MyDependency
выступает зависимостью класса IndexModel
:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
Этот класс создает MyDependency
и напрямую зависит от этого класса. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.
- Чтобы заменить
MyDependency
другой реализацией, классIndexModel
необходимо изменить. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять классIndexModel
. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению. - Такая реализация плохо подходит для модульных тестов.
Внедрение зависимостей решает эти проблемы следующим образом:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
- Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Службы обычно регистрируются в файле приложения
Program.cs
. - Внедрение службы в конструктор класса, где она используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.
В примере приложения интерфейс IMyDependency
определяет метод WriteMessage
:
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency
с конкретным типом MyDependency
. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса.
Сроки службы службы описаны ниже в этой статье.
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
В примере приложения запрашивается служба IMyDependency
, которая затем используется для вызова метода WriteMessage
:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя паттерн DI, контроллер или страница Razor:
- не использует конкретный тип
MyDependency
, только интерфейсIMyDependency
, который он реализует. Это упрощает изменение реализации без изменения контроллера или страницы Razor. - не создает экземпляр
MyDependency
, он создается контейнером внедрения зависимостей.
Реализацию интерфейса IMyDependency
можно улучшить с помощью встроенного API ведения журнала:
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
}
}
Обновленный метод Program.cs
регистрирует новую реализацию IMyDependency
:
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
var app = builder.Build();
MyDependency2
зависит от ILogger<TCategoryName>, который запрашивается в конструкторе.
ILogger<TCategoryName>
— это предоставленная платформой служба.
Внедрение зависимостей в цепочке — это вполне обычная практика. Каждая из запрашиваемых зависимостей поочерёдно запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.
В терминологии внедрения зависимостей под службой понимается:
- Обычно является объектом, предоставляющим службу для других объектов, например службу
IMyDependency
. - Не связан с веб-службой, хотя служба может использовать веб-службу.
Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency
, приведенные в предыдущем примере, были написаны для демонстрации основ внедрения зависимостей, а не для реализации ведения журнала. Большинству приложений не нужно создавать лог-файлы. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб:
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; } = string.Empty;
public void OnGet()
{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
Предыдущий код работает правильно, не изменяя ничего в Program.cs
, так как ведение журнала предоставляется платформой.
Регистрация групп сервисов с помощью методов расширения
Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME}
для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.
Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:
using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
Рассмотрим следующий код, который регистрирует службы и настраивает параметры:
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();
Связанные группы регистраций можно переместить в метод расширения для регистрации служб. Например, службы конфигурации добавляются в следующий класс:
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
Остальные службы регистрируются в аналогичном классе. Следующий код использует новые методы расширения для регистрации служб:
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
Примечание. Каждый services.Add{GROUP_NAME}
метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет контроллеры MVC служб с необходимыми представлениями, а AddRazorPages — службы, требуемые для работы Razor Pages.
Время существования служб
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.
Используйте службы ограниченной области в промежуточном ПО, применяя один из следующих подходов:
- Внедрите сервис в метод
Invoke
илиInvokeAsync
ПО промежуточного слоя. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подходInvokeAsync
. - Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в конструктор ПО промежуточного слоя.
Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.
Методы регистрации службы
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.
Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они имеют одинаковый тип реализации.
Любой из этих методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
. Второй вызов AddSingleton
переопределяет предыдущий, если он разрешается как IMyDependency
, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
Ключи служб
Термин «службы с ключами» относится к механизму регистрации и поиска служб внедрения зависимостей с использованием ключей. Служба связана с ключом путем вызова AddKeyedSingleton (или AddKeyedScoped
AddKeyedTransient
) для регистрации. Доступ к зарегистрированной службе путем указания ключа с атрибутом [FromKeyedServices]
. В следующем коде показано, как использовать ключи служб:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
smallCache.Get("date"));
app.MapControllers();
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
{
return cache.Get("data-mvc");
}
}
public class MyHub : Hub
{
public void Method([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}
Ключи служб в ПО промежуточного слоя
Промежуточное ПО поддерживает службы с ключами как в конструкторе, так и в методе Invoke
/InvokeAsync
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");
var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();
internal class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next,
[FromKeyedServices("test")] MySingletonClass service)
{
_next = next;
}
public Task Invoke(HttpContext context,
[FromKeyedServices("test2")]
MyScopedClass scopedService) => _next(context);
}
Дополнительные сведения о создании ПО промежуточного слоя см. в статье "Создание пользовательского ПО промежуточного слоя для ASP.NET Core"
Поведение внедрения через конструктор
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Контексты Entity Framework
По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью ограниченного по области видимости времени существования, поскольку операции базы данных в веб-приложении обычно ограничены клиентским запросом. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы с заданным временем жизни не должны использовать контекст базы данных с более коротким временем жизни, чем у самой службы.
Варианты срока действия и регистрации
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation
реализует все предыдущие интерфейсы. Конструктор Operation
создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId
:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Следующий код создает несколько регистраций класса Operation
в соответствии с именованным временем существования:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMyMiddleware();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
В примере приложения показано время существования объектов в пределах запросов и между запросами.
IndexModel
и ПО промежуточного слоя запрашивают каждый тип IOperation
и регистрируют OperationId
для каждого из них:
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + _scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
}
}
Подобно IndexModel
, промежуточное программное обеспечение обрабатывает те же службы.
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
Службы с заданной областью и временные службы должны быть разрешены в методе InvokeAsync
:
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
Выходные данные средства ведения журнала содержат:
-
Временные объекты всегда разные. Значение временного
OperationId
отличается вIndexModel
и в ПО для промежуточного слоя. - Объекты с заданной областью остаются неизменными в пределах указанного запроса, но в новых запросах используются разные объекты.
- Одноэлементные объекты одинаковы для каждого запроса.
Чтобы уменьшить выходные данные ведения журнала, в файле appsettings.Development.json
установите "Logging:LogLevel:Microsoft:Error".
{
"MyKey": "MyKey from appsettings.Development.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Разрешение службы при запуске приложения
В следующем коде показано, как разрешить службу с областью действия в течение ограниченного времени при запуске приложения:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
app.MapGet("/", () => "Hello World!");
app.Run();
Подтверждение области охвата
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Дополнительные сведения см. в разделе Проверка области.
Службы запросов
Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.
Фреймворк создает область для каждого запроса, а RequestServices
предоставляет поставщика услуг в рамках области. Все службы с заданной областью действительны до тех пор, пока запрос активен.
Примечание.
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не получать службы из RequestServices
. Запрос зависимостей в виде параметров конструктора приводит к созданию классов, которые проще тестировать.
Проектирование сервисов для внедрения зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:
- Избегайте статических классов и членов с отслеживанием состояния. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
- Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров связывает код с определенной реализацией.
- Сделайте службы небольшими, хорошо структурированными и легко тестируемыми.
Если класс имеет много внедренных зависимостей, это может быть признаком того, что класс имеет слишком много обязанностей и нарушает принцип единой ответственности (SRP). Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.
Прекращение оказания услуг
Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как синглтон, контейнер автоматически удаляет синглтон.
В следующем примере службы создаются контейнером службы и автоматически удаляются:
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
using DIsample2.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));
var app = builder.Build();
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3 service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Development.json
Service1.Dispose
Службы, не созданные контейнером службы
Рассмотрим следующий код:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
В предыдущем коде:
- Экземпляры служб не создаются контейнером.
- Платформа не удаляет службы автоматически.
- За управление службами отвечает разработчик.
Руководство по использованию временных и разделяемых экземпляров IDisposable
См. руководство по IDisposable для переходных и разделяемых экземпляров в разделе Внедрение зависимостей в .NET.
Замена стандартного контейнера служб
См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.
Рекомендации
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.
Старайтесь не использовать шаблон локатора службы. Например, не вызывайте GetService для получения экземпляра службы, если можно использовать внедрение зависимостей (DI):
Неправильно:
Правильное.
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Другой вариацией паттерна локатора сервиса, которой следует избегать, является внедрение фабрики, решающей зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).
DI является альтернативой для доступа к статическим или глобальным объектам. Возможно, вы не сможете реализовать преимущества DI, если вы смешиваете его со статическим доступом к объектам.
Рекомендуемые подходы к многопользовательской архитектуре при использовании Dependency Injection (DI)
Orchard Core — это платформа приложений для создания модульных мультитенантных приложений на ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.
Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.
Службы, предоставляемые фреймворком
Program.cs
регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection
, предоставленная для Program.cs
, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.
Тип службы | Срок службы |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Временный |
IHostApplicationLifetime | Синглтон |
IWebHostEnvironment | Синглтон |
Microsoft.AspNetCore.Hosting.IStartup | Отдельная |
Microsoft.AspNetCore.Hosting.IStartupFilter | Временный |
Microsoft.AspNetCore.Hosting.Server.IServer | Синглтон |
Microsoft.AspNetCore.Http.IHttpContextFactory | Временный |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | Синглтон |
Microsoft.Extensions.Logging.ILoggerFactory | Отдельная |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Синглтон |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | Временный |
Microsoft.Extensions.Options.IOptions<TOptions> | Синглтон |
System.Diagnostics.DiagnosticSource | Отдельная |
System.Diagnostics.DiagnosticListener | Отдельная |
Дополнительные ресурсы
- Внедрение зависимостей Blazor в ASP.NET Core
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Шаблоны конференций NDC для разработки приложений с внедрением зависимостей
- Запуск приложения в ASP.NET Core
- Активация ПО промежуточного слоя на основе фабрики в ASP.NET Core
- Основные сведения о внедрении зависимостей в .NET
- Рекомендации по внедрению зависимостей
- Руководство. Использование внедрения зависимостей в .NET
- Внедрение зависимостей .NET
- ВНЕДРЕНИЕ ЗАВИСИМОСТЕЙ В ASP.NET CORE: ЧТО ТАКОЕ ISERVICECOLLECTION?
- Четыре способа управления объектами IDisposable в ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)
- Принцип явных зависимостей
- Контейнеры с инверсией управления и паттерн внедрения зависимостей (Мартин Фаулер)
- How to register a service with multiple interfaces in ASP.NET Core DI (Регистрация службы с несколькими интерфейсами с помощью внедрения зависимостей ASP.NET Core)
Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith) и Брэндон Далер (Brandon Dahler)
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.
Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.
В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.
Просмотреть или скачать образец кода (описание загрузки)
Общие сведения о внедрении зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency
с методом WriteMessage
, от которого зависят другие классы:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
Класс может создать экземпляр класса MyDependency
, чтобы использовать его метод WriteMessage
. В следующем примере класс MyDependency
выступает зависимостью класса IndexModel
:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
Этот класс создает MyDependency
и напрямую зависит от этого класса. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.
- Чтобы заменить
MyDependency
другой реализацией, классIndexModel
необходимо изменить. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять классIndexModel
. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению. - Такая реализация плохо подходит для модульных тестов.
Внедрение зависимостей устраняет эти проблемы следующим образом:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
- Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Службы обычно регистрируются в файле приложения
Program.cs
. - Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.
В примере приложения интерфейс IMyDependency
определяет метод WriteMessage
:
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency
с конкретным типом MyDependency
. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
В примере приложения запрашивается служба IMyDependency
, которая затем используется для вызова метода WriteMessage
:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя шаблон внедрения зависимостей, контроллер или страницу Razor:
- не использует конкретный тип
MyDependency
, только интерфейсIMyDependency
, который он реализует. Это упрощает изменение реализации без изменения контроллера или страницы Razor. - Не создает экземпляр
MyDependency
, он создается контейнером DI.
Реализацию интерфейса IMyDependency
можно улучшить с помощью встроенного API ведения журнала:
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
}
}
Обновленный метод Program.cs
регистрирует новую реализацию IMyDependency
:
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
var app = builder.Build();
MyDependency2
зависит от ILogger<TCategoryName>, который запрашивается через конструктор.
ILogger<TCategoryName>
— это предоставленная платформой служба.
Использование внедрений зависимостей в цепочке не является чем-то необычным. Каждая запрашиваемая зависимость, в свою очередь, запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает успешно разрешённый сервис. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.
В терминологии внедрения зависимостей, служба — это:
- Обычно является объектом, предоставляющим службу для других объектов, например службу
IMyDependency
. - Не относится к веб-службе, хотя служба может использовать веб-службу.
Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency
, приведенные в предыдущем примере, были написаны для демонстрации базового внедрения зависимостей (DI), а не для реализации ведения журнала. Большинству приложений не нужно писать логгеры. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб:
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; } = string.Empty;
public void OnGet()
{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
Используя приведенный выше код, не нужно обновлять Program.cs
, поскольку платформа предоставляет возможность ведения журнала.
Регистрация групп сервисов с помощью методов расширения
Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME}
для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.
Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:
using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
Рассмотрим следующий код, который регистрирует службы и настраивает параметры:
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();
Связанные группы регистраций можно переместить в метод расширения для регистрации сервисов. Например, службы конфигурации добавляются в следующий класс:
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
Остальные сервисы регистрируются в аналогичном классе. Следующий код использует новые методы расширения для регистрации служб:
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
Примечание. Каждый services.Add{GROUP_NAME}
метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет необходимые службы для контроллеров MVC с представлениями, а AddRazorPages добавляет службы, требуемые для Razor Pages.
Срок службы
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.
Используйте службы с заданной областью действия в промежуточном программном обеспечении, применяя один из следующих подходов:
- Внедрите сервис в метод
Invoke
илиInvokeAsync
посредника. Использование внедрения через конструктор вызывает исключение времени выполнения, поскольку оно заставляет службу с областью действия вести себя как синглтон. В примере в разделе Параметры времени существования и регистрации демонстрируется подходInvokeAsync
. - Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в конструктор ПО промежуточного слоя.
Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.
Методы регистрации службы
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.
Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.
Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
. Второй вызов AddSingleton
переопределяет предыдущий, если он разрешается как IMyDependency
, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
Ключевые службы
Службы, использующие ключи относятся к механизму регистрации и получения служб внедрения зависимостей (DI) с использованием ключей. Служба связана с ключом путем вызова AddKeyedSingleton (или AddKeyedScoped
AddKeyedTransient
) для регистрации. Доступ к зарегистрированной службе путем указания ключа с атрибутом [FromKeyedServices]
. В следующем коде показано, как использовать ключи служб:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
smallCache.Get("date"));
app.MapControllers();
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
{
return cache.Get("data-mvc");
}
}
public class MyHub : Hub
{
public void Method([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}
Поведение при внедрении через конструктор
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Контексты Entity Framework
По умолчанию контексты Entity Framework добавляются в контейнер служб с использованием ограниченного области действия времени существования, поскольку операции с базой данных в веб-приложениях обычно ограничиваются клиентским запросом. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы с заданным временем существования не должны использовать контекст базы данных, время жизни которого короче времени жизни службы.
Параметры срока действия и регистрации
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation
реализует все предыдущие интерфейсы. Конструктор Operation
создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId
:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Следующий код создает несколько регистраций класса Operation
в соответствии с именованными сроками жизни:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMyMiddleware();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
В примере приложения показано время существования объектов в пределах запросов и между запросами.
IndexModel
и ПО промежуточного слоя запрашивают каждый тип IOperation
и регистрируют OperationId
для каждого из них:
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + _scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
}
}
Аналогично IndexModel
, ПО промежуточного слоя разрешает те же службы.
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
Службы с заданной областью и временные службы должны быть разрешены в методе InvokeAsync
:
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
Выходные данные средства ведения журнала показывают:
-
Временные объекты всегда разные. Значение
OperationId
временного отличается вIndexModel
и в промежуточном программном обеспечении. - Объекты Scope одинаковы для данного запроса, но отличаются в каждом новом запросе.
- Одноэлементные объекты одинаковы для каждого запроса.
Чтобы уменьшить объем ведения журнала, настройте в файле appsettings.Development.json
параметр "Logging:LogLevel:Microsoft:Error".
{
"MyKey": "MyKey from appsettings.Development.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Разрешение службы при запуске приложения
В следующем коде показано, как разрешить службу с областью действия в течение ограниченного времени при запуске приложения:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
app.MapGet("/", () => "Hello World!");
app.Run();
Проверка объема задач
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Дополнительные сведения см. в разделе Проверка области.
Службы запросов
Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.
Фреймворк создает область для каждого запроса, а RequestServices
предоставляет сервисный провайдер в заданной области. Все службы с заданной областью действительны до тех пор, пока запрос активен.
Примечание.
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, чем получать службы из RequestServices
. Запрос зависимостей в качестве параметров конструктора приводит к созданию классов, которые проще тестировать.
Проектирование сервисов для внедрения зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:
- Избегайте классов и членов, которые являются статическими и поддерживают состояние. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
- Избегайте прямого создания экземпляров зависимых классов внутри сервисов. Прямое создание экземпляров привязывает код к определенной реализации.
- Сделайте сервисы небольшими, хорошо организованными и удобными в тестировании.
Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.
Ликвидация услуг
Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как синглтон, контейнер автоматически освобождает этот синглтон.
В следующем примере сервисы создаются контейнером сервисов и удаляются автоматически: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
using DIsample2.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));
var app = builder.Build();
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3 service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Development.json
Service1.Dispose
Службы, не созданные контейнером службы
Рассмотрим следующий код:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
В предыдущем коде:
- Экземпляры службы не создаются контейнером службы.
- Платформа не удаляет службы автоматически.
- Разработчик отвечает за утилизацию служб.
Руководство по использованию временных и разделяемых экземпляров IDisposable
См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.
Замена стандартного контейнера служб
См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.
Рекомендации
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.
Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра сервиса, когда можно использовать DI (внедрение зависимостей):
Неправильно:
Правильное.
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Другой вариант указателя службы, позволяющий избежать этого, — внедрение фабрики, которая разрешает зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).
Внедрение зависимостей (DI) является альтернативой доступу к статическим или глобальным объектам. Вы не сможете понять преимущества внедрения зависимостей (DI), если будете сочетать его с доступом к статическим объектам.
Рекомендуемые подходы к мультитенантности при внедрении зависимостей
Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.
Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.
Платформенные службы
Program.cs
регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection
, предоставленная для Program.cs
, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.
Тип службы | Срок службы |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Временный |
IHostApplicationLifetime | Отдельная |
IWebHostEnvironment | Синглтон |
Microsoft.AspNetCore.Hosting.IStartup | Отдельная |
Microsoft.AspNetCore.Hosting.IStartupFilter | Временный |
Microsoft.AspNetCore.Hosting.Server.IServer | Синглтон |
Microsoft.AspNetCore.Http.IHttpContextFactory | Временный |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | Отдельная |
Microsoft.Extensions.Logging.ILoggerFactory | Отдельная |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Отдельная |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | Временный |
Microsoft.Extensions.Options.IOptions<TOptions> | Одноэлементный экземпляр |
System.Diagnostics.DiagnosticSource | Отдельная |
System.Diagnostics.DiagnosticListener | Синглтон |
Дополнительные ресурсы
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Внедрение зависимостей Blazor в ASP.NET Core
- Конференция NDC: шаблоны для разработки приложений с использованием внедрения зависимостей
- Запуск приложения в ASP.NET Core
- Активация промежуточного программного обеспечения на базе фабрики в ASP.NET Core
- Основные сведения о внедрении зависимостей в .NET
- Рекомендации по внедрению зависимостей
- Руководство. Использование внедрения зависимостей в .NET
- Внедрение зависимостей .NET
- ASP.NET ВНЕДРЕНИЕ ОСНОВНЫХ ЗАВИСИМОСТЕЙ: ЧТО ТАКОЕ ISERVICECOLLECTION?
- Четыре способа освобождения объектов IDisposable в ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)
- Принцип явных зависимостей
- Контейнеры с инверсией управления и паттерн внедрения зависимостей (Мартин Фаулер)
- Как зарегистрировать службу с несколькими интерфейсами в ASP.NET Core DI
Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith) и Брэндон Далер (Brandon Dahler)
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.
Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.
В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.
Просмотреть или скачать образец кода (описание загрузки)
Общие сведения о внедрении зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency
с методом WriteMessage
, от которого зависят другие классы:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
Класс может создать экземпляр класса MyDependency
, чтобы использовать его метод WriteMessage
. В следующем примере класс MyDependency
выступает зависимостью класса IndexModel
:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
Класс создает и напрямую зависит от класса MyDependency
. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.
- Чтобы заменить
MyDependency
другой реализацией, классIndexModel
необходимо изменить. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять классIndexModel
. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению. - Такая реализация плохо подходит для модульных тестов.
Внедрение зависимостей устраняет эти проблемы следующим образом:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
- Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Службы обычно регистрируются в файле приложения
Program.cs
. - Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.
В примере приложения интерфейс IMyDependency
определяет метод WriteMessage
:
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency
с конкретным типом MyDependency
. Метод AddScoped регистрирует службу с областью действия, ограниченной временем обработки одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
В примере приложения запрашивается служба IMyDependency
, которая затем используется для вызова метода WriteMessage
:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя шаблон DI, контроллер или страница Razor:
- не использует конкретный тип
MyDependency
, только интерфейсIMyDependency
, который он реализует. Это упрощает изменение реализации без изменения контроллера или страницы Razor. - не создает экземпляр
MyDependency
, он создается контейнером внедрения зависимостей.
Реализацию интерфейса IMyDependency
можно улучшить с помощью встроенного API ведения журнала:
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
}
}
Обновленный метод Program.cs
регистрирует новую реализацию IMyDependency
:
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
var app = builder.Build();
MyDependency2
зависит от ILogger<TCategoryName>, запрос которой происходит в конструкторе.
ILogger<TCategoryName>
— это предоставленная платформой служба.
Использование внедрения зависимостей в цепочечной форме не является чем-то необычным. Каждая искомая зависимость по очереди запрашивает свои собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.
В терминологии внедрения зависимостей — сервис.
- Обычно является объектом, предоставляющим службу для других объектов, например службу
IMyDependency
. - Не относится к веб-службе, хотя служба может использовать веб-службу.
Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency
, приведенные в предыдущем примере, были написаны для демонстрации базового DI, а не для реализации ведения журнала. Большинству приложений не нужно писать средства журналирования. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб:
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; } = string.Empty;
public void OnGet()
{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
Используя приведенный выше код, не нужно обновлять Program.cs
, поскольку платформа предоставляет возможность ведения журнала.
Регистрация групп сервисов с помощью методов расширения
Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME}
для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.
Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:
using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
Рассмотрим следующий код, который регистрирует службы и настраивает параметры:
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();
Связанные группы регистраций можно переместить в метод расширения для регистрации сервисов. Например, службы конфигурации добавляются в следующий класс:
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
Остальные услуги регистрируются в аналогичном классе. Следующий код использует новые методы расширения для регистрации служб:
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
Примечание. Каждый services.Add{GROUP_NAME}
метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет службы, необходимые для контроллеров MVC с представлениями, а AddRazorPages — службы, которые требуются для Razor Pages.
Время существования служб
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.
Используйте службы с заданной областью в посредническом ПО, применяя один из следующих подходов:
- Внедрите сервис в методы
Invoke
илиInvokeAsync
промежуточного программного обеспечения. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подходInvokeAsync
. - Используйте фабричное промежуточное программное обеспечение. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в конструктор ПО промежуточного слоя.
Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.
Методы регистрации службы
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.
Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.
Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
. Второй вызов AddSingleton
переопределяет предыдущий, если он разрешается как IMyDependency
, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
Поведение внедрения через конструктор
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Контексты Entity Framework
По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы с заданным временем жизни не должны использовать контекст базы данных с временем жизни короче, чем у службы.
Параметры срока действия и регистрации
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation
реализует все предыдущие интерфейсы. Конструктор Operation
создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId
:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Следующий код создает несколько регистраций класса Operation
в соответствии с именованным временем существования:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMyMiddleware();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
В примере приложения показано время существования объектов в пределах запросов и между запросами.
IndexModel
и ПО промежуточного слоя запрашивают каждый тип IOperation
и регистрируют OperationId
для каждого из них:
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + _scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
}
}
Подобно IndexModel
, посредник решает те же службы.
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
Службы с заданной областью и временные службы должны быть разрешены в методе InvokeAsync
:
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
Выходные данные средства ведения журнала содержат:
-
Временные объекты всегда разные. Значение временного
OperationId
отличается вIndexModel
и в промежуточном ПО. - Объекты с областью видимости одинаковы для одного запроса, но различаются для каждого нового запроса.
- Одноэлементные объекты одинаковы для каждого запроса.
Чтобы уменьшить выходные данные ведения журнала, в файле appsettings.Development.json
задайте "Logging:LogLevel:Microsoft:Error".
{
"MyKey": "MyKey from appsettings.Development.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Разрешение службы при запуске приложения
В следующем коде показано, как определить службу с ограниченной областью действия на ограниченное время при запуске приложения.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
app.MapGet("/", () => "Hello World!");
app.Run();
Подтверждение объема проекта
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Дополнительные сведения см. в разделе Проверка области.
Службы запросов
Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.
Фреймворк создает область для каждого запроса, а RequestServices
предоставляет доступ к поставщику услуг с заданной областью. Все службы с заданной областью действительны до тех пор, пока запрос активен.
Примечание.
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не разрешать службы из RequestServices
. Запрос зависимостей в качестве параметров конструктора приводит к созданию классов, которые проще тестировать.
Проектирование сервисов для внедрения зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:
- Избегайте классов и членов, зависящих от состояния, и статических. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
- Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров связывает код с конкретной реализацией.
- Сделайте службы небольшими, хорошо структурированными и удобными в тестировании.
Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.
Ликвидация услуг
Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удаляет одноэлементный объект.
В следующем примере сервисы создаются контейнером сервисов и автоматически освобождаются: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
using DIsample2.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));
var app = builder.Build();
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3 service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Development.json
Service1.Dispose
Службы, не созданные контейнером службы
Рассмотрим следующий код:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
В предыдущем коде:
- Экземпляры службы не создаются контейнером службы.
- Платформа не удаляет службы автоматически.
- За утилизацию служб отвечает разработчик.
Руководство по применению экземпляров IDisposable для временного и общего использования
См. раздел Рекомендации по IDisposable для временных и разделяемых экземпляров в статье Внедрение зависимостей в .NET.
Замена стандартного контейнера служб
См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.
Рекомендации
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.
Старайтесь не использовать шаблон сервисного локатора. Например, не вызывайте GetService для получения экземпляра службы, когда вместо этого можно использовать инверсию зависимостей (DI):
Неправильно:
Правильное.
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Другой вариант локатора служб, которого следует избегать, — это внедрение фабрики, разрешающей зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).
Внедрение зависимостей (Dependency Injection) является альтернативой к шаблонам доступа к статическим или глобальным объектам. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если сочетаете его с доступом к статическим объектам.
Рекомендуемые шаблоны для многопользовательской поддержки в DI (внедрении зависимостей)
Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.
Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.
Сервисы, предоставляемые фреймворком
Program.cs
регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально IServiceCollection
, предоставленный для Program.cs
, имеет службы, определенные фреймворком в зависимости от того, как был настроен хост. Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.
Тип службы | Срок службы |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Временный |
IHostApplicationLifetime | Отдельная |
IWebHostEnvironment | Отдельная |
Microsoft.AspNetCore.Hosting.IStartup | Синглтон |
Microsoft.AspNetCore.Hosting.IStartupFilter | Временный |
Microsoft.AspNetCore.Hosting.Server.IServer | Синглтон |
Microsoft.AspNetCore.Http.IHttpContextFactory | Временный |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | Синглтон |
Microsoft.Extensions.Logging.ILoggerFactory | Отдельная |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Синглтон |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | Временный |
Microsoft.Extensions.Options.IOptions<TOptions> | Синглтон |
System.Diagnostics.DiagnosticSource | Синглтон |
System.Diagnostics.DiagnosticListener | Синглтон |
Дополнительные ресурсы
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Внедрение зависимостей Blazor в ASP.NET Core
- Шаблоны проектирования NDC конференции для разработки приложений с внедрением DI
- Запуск приложения в ASP.NET Core
- Активация промежуточного программного обеспечения с использованием фабрики в ASP.NET Core
- Четыре способа удаления объектов IDisposable в ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)
- Принцип явных зависимостей
- Контейнеры с инверсией управления и паттерн внедрения зависимостей (Мартин Фаулер)
- How to register a service with multiple interfaces in ASP.NET Core DI (Регистрация службы с несколькими интерфейсами с помощью внедрения зависимостей ASP.NET Core)
Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith), Скотт Эдди (Scott Addie) и Брэндон Далер (Brandon Dahler)
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.
Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.
В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.
Просмотреть или скачать образец кода (описание загрузки)
Общие сведения о внедрении зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency
с методом WriteMessage
, от которого зависят другие классы:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
Класс может создать экземпляр класса MyDependency
, чтобы использовать его метод WriteMessage
. В следующем примере класс MyDependency
выступает зависимостью класса IndexModel
:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet created this message.");
}
}
Класс создает и напрямую зависит от указанного класса MyDependency
. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.
- Чтобы заменить
MyDependency
другой реализацией, классIndexModel
необходимо изменить. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять классIndexModel
. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению. - Такая реализация плохо подходит для модульных тестов. В приложении нужно использовать имитацию или заглушку в виде класса
MyDependency
, что при таком подходе невозможно.
Внедрение зависимостей устраняет эти проблемы следующим образом:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
- Регистрация зависимости в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Как правило, службы регистрируются в приложении в методе
Startup.ConfigureServices
. - Сервис внедряется в конструктор класса, в котором он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.
В примере приложения интерфейс IMyDependency
определяет метод WriteMessage
:
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency
, указывая конкретный тип MyDependency
. Метод AddScoped регистрирует службу с ограниченным временем существования, равным времени существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddRazorPages();
}
В примере приложения запрашивается служба IMyDependency
, которая затем используется для вызова метода WriteMessage
:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя шаблон внедрения зависимостей, контроллер:
- не использует конкретный тип
MyDependency
, только интерфейсIMyDependency
, который он реализует. Это упрощает изменение реализации, используемой контроллером, без изменения контроллера. - Не создает экземпляр
MyDependency
, он создается DI контейнером.
Реализацию интерфейса IMyDependency
можно улучшить с помощью встроенного API ведения журнала:
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
}
}
Обновленный метод ConfigureServices
регистрирует новую реализацию IMyDependency
:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency2>();
services.AddRazorPages();
}
MyDependency2
зависит от ILogger<TCategoryName>, который запрашивается в конструкторе.
ILogger<TCategoryName>
— это предоставленная платформой служба.
Использование внедрения зависимостей в цепочке не является чем-то необычным. Каждая запрашиваемая зависимость запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью завершенный сервис. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.
В терминологии внедрения зависимостей — сервис:
- Обычно является объектом, предоставляющим службу для других объектов, например службу
IMyDependency
. - Не относится к веб-службе, хотя служба может использовать веб-службу.
Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency
, приведенные в предыдущих примерах, были написаны для демонстрации базового внедрения зависимостей, а не для ведения журнала. Большинству приложений не нужно создавать средства ведения журнала. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб в ConfigureServices
:
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; }
public void OnGet()
{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
Используя приведенный выше код, не нужно обновлять ConfigureServices
, поскольку платформа предоставляет возможность ведения журнала.
Службы, внедренные в Startup
Службы можно внедрить в конструктор Startup
и метод Startup.Configure
.
Только следующие службы могут быть внедрены в конструктор Startup
, когда используется универсальный хост (IHostBuilder):
Любая служба, зарегистрированная в контейнере внедрения зависимостей (DI-контейнере), может быть внедрена в метод Startup.Configure
.
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
...
}
Дополнительные сведения см. в статьях Запуск приложения в ASP.NET Core и Доступ к конфигурации во время запуска.
Регистрация групп сервисов с помощью методов расширения
Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME}
для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.
Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
}
Рассмотрим следующий метод ConfigureServices
, который регистрирует службы и настраивает параметры:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<PositionOptions>(
Configuration.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
Configuration.GetSection(ColorOptions.Color));
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
services.AddRazorPages();
}
Связанные группы регистраций можно переместить в метод расширения для регистрации сервисов. Например, службы конфигурации добавляются в следующий класс:
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
Остальные службы регистрируются в аналогичном классе. Следующий метод ConfigureServices
использует новые методы расширения для регистрации служб:
public void ConfigureServices(IServiceCollection services)
{
services.AddConfig(Configuration)
.AddMyDependencyGroup();
services.AddRazorPages();
}
Примечание. Каждый services.Add{GROUP_NAME}
метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет необходимые службы для контроллеров MVC с представлениями, а AddRazorPages добавляет службы, которые требуются для Razor Pages. Рекомендуется соблюдать в приложениях соглашение об именовании создаваемых методов расширения в пространстве имен Microsoft.Extensions.DependencyInjection. Создание методов расширения в пространстве имен Microsoft.Extensions.DependencyInjection
:
- Инкапсулирует группы регистраций сервисов.
- Предоставляет удобный доступ к службе с помощью IntelliSense.
Сроки службы
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.
Используйте службы с заданной областью в ПО промежуточного слоя, применяя один из следующих подходов:
- Внедрите сервис в метод
Invoke
илиInvokeAsync
промежуточного ПО. С помощью внедрения конструктора создается исключение времени выполнения, поскольку это заставляет службу с областью действия вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подходInvokeAsync
. - Используйте фабричное ПО промежуточного слоя. Промежуточное ПО, зарегистрированное данным способом, активируется при каждом клиентском запросе (подключении), что позволяет интегрировать службы с заданной областью в его метод
InvokeAsync
.
Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.
Методы регистрации службы
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.
Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.
Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
. Второй вызов AddSingleton
переопределяет предыдущий, когда он разрешается как IMyDependency
, и добавляется к предыдущему, когда несколько служб разрешаются через IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
Поведение внедрения через конструктор
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Контексты Entity Framework
По умолчанию контексты Entity Framework добавляются в контейнер служб с помощью времени существования в рамках, поскольку операции базы данных в веб-приложении обычно ограничиваются областью клиентского запроса. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы с заданным временем жизни не должны использовать контекст базы данных с временем жизни, которое короче времени жизни самой службы.
Опции срока действия и регистрации
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation
реализует все предыдущие интерфейсы. Конструктор Operation
создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId
:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Метод Startup.ConfigureServices
создает несколько регистраций класса Operation
в соответствии с именованным временем существования:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddRazorPages();
}
В примере приложения показано время существования объектов в пределах запросов и между запросами.
IndexModel
и средства промежуточного слоя запрашивают каждый тип IOperation
и ведут запись в журнал OperationId
для каждого из них.
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + _scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
}
}
Аналогично IndexModel
, промежуточное ПО обрабатывает те же службы.
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationTransient transientOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
Службы с заданной областью должны быть разрешены в методе InvokeAsync
:
public async Task InvokeAsync(HttpContext context,
IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
Выход логгера показывает:
-
Временные объекты всегда разные. Значение временного
OperationId
отличается вIndexModel
и в промежуточном ПО. - Объекты с областью видимости одинаковы для данного запроса, но различаются для каждого нового запроса.
- Одноэлементные объекты одинаковы для каждого запроса.
Чтобы уменьшить выходные данные ведения журнала, задайте "Logging:LogLevel:Microsoft:Error" в файле appsettings.Development.json
.
{
"MyKey": "MyKey from appsettings.Development.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Вызов служб из функции main
Создайте IServiceScope с помощью IServiceScopeFactory.CreateScope для разрешения службы в рамках области приложения. Этот способ позволит получить доступ к службе с заданной областью при запуске для выполнения задач по инициализации.
В следующем примере показано, как получить доступ к службе IMyDependency
с заданной областью и вызвать ее метод WriteMessage
в Program.Main
:
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Проверка объема
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.
Дополнительные сведения см. в разделе Проверка области.
Службы запросов
Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.
Фреймворк создает область для каждого запроса, а RequestServices
предоставляет провайдера услуг в этой области. Все службы с заданной областью действительны до тех пор, пока запрос активен.
Примечание.
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не получать службы из RequestServices
. При передаче зависимостей в качестве параметров конструктора получается класс, который легче тестировать.
Проектирование сервисов для внедрения зависимостей
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:
- Избегайте классов и членов, имеющих состояние, и статических. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
- Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров связывает код с конкретной реализацией.
- Сделайте сервисы маленькими, хорошо организованными и легко тестируемыми.
Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.
Прекращение предоставления услуг
Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.
В следующем примере сервисы создаются контейнером сервисов и автоматически освобождаются.
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<Service1>();
services.AddSingleton<Service2>();
var myKey = Configuration["MyKey"];
services.AddSingleton<IService3>(sp => new Service3(myKey));
services.AddRazorPages();
}
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3 service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = My Key from config
Service1.Dispose
Службы, не созданные контейнером службы
Рассмотрим следующий код:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new Service1());
services.AddSingleton(new Service2());
services.AddRazorPages();
}
В предыдущем коде:
- Экземпляры сервисов не создаются контейнером службы.
- Платформа не удаляет службы автоматически.
- За утилизацию служб отвечает разработчик.
Руководство по использованию временных и общих объектов IDisposable
См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.
Замена стандартного контейнера служб
См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.
Рекомендации
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.
Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать DI (внедрение зависимостей):
Неправильно:
Правильное.
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Другой вариант локатора сервисов, которого следует избегать, - это внедрение фабрики, которая разрешает зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).
Избегайте вызовов BuildServiceProvider в
ConfigureServices
. ВызовBuildServiceProvider
обычно происходит, когда разработчику необходимо разрешить службу вConfigureServices
. Например, рассмотрим случай, когдаLoginPath
загружается из конфигурации. Избегайте следующего подхода:На предыдущем рисунке при выборе строки, отмеченной зеленой волнистой линией в разделе
services.BuildServiceProvider
, отображается следующее предупреждение ASP0000:ASP0000. Вызов BuildServiceProvider из кода приложения приводит к созданию дополнительной копии создаваемых одноэлементных служб. В качестве параметров для Configure можно использовать альтернативные варианты, такие как службы внедрения зависимостей.
При вызове
BuildServiceProvider
создается второй контейнер, который может создавать разорванные одноэлементные экземпляры и ссылаться на графы объектов в нескольких контейнерах.Правильный способ получить
LoginPath
— использовать встроенную поддержку паттерна параметров для внедрения зависимостей:public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); services.AddOptions<CookieAuthenticationOptions>( CookieAuthenticationDefaults.AuthenticationScheme) .Configure<IMyService>((options, myService) => { options.LoginPath = myService.GetLoginPath(); }); services.AddRazorPages(); }
Контейнер собирает удаляемые временные службы для удаления. Это может привести к утечке памяти, если решается из контейнера верхнего уровня.
Включите проверку области, чтобы убедиться, что в приложении нет синглтонов, захватывающих службы с ограниченной областью. Дополнительные сведения см. в разделе Проверка области.
Как и в случае с любыми рекомендациями, могут возникнуть ситуации, когда требуется игнорировать рекомендацию. Исключения возникают редко, — как правило, это особые случаи, связанные с самой платформой.
Внедрение зависимостей (Dependency Injection) является альтернативой для шаблонов доступа к статическим или глобальным объектам. Вы можете не ощутить преимуществ ДИ, если будете сочетать его с доступом к статическим объектам.
Рекомендуемые подходы к мультитенантности при внедрении зависимостей (DI)
Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.
Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.
Службы, предоставляемые фреймворком
Метод Startup.ConfigureServices
регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection
, предоставленная для ConfigureServices
, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.
Тип службы | Срок службы |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | Временный |
IHostApplicationLifetime | Отдельная |
IWebHostEnvironment | Отдельная |
Microsoft.AspNetCore.Hosting.IStartup | Синглтон |
Microsoft.AspNetCore.Hosting.IStartupFilter | Временный |
Microsoft.AspNetCore.Hosting.Server.IServer | Отдельная |
Microsoft.AspNetCore.Http.IHttpContextFactory | Временный |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | Отдельная |
Microsoft.Extensions.Logging.ILoggerFactory | Отдельная |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Отдельная |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | Временный |
Microsoft.Extensions.Options.IOptions<TOptions> | Отдельная |
System.Diagnostics.DiagnosticSource | Отдельная |
System.Diagnostics.DiagnosticListener | Отдельная |
Дополнительные ресурсы
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Внедрение зависимостей Blazor в ASP.NET Core
- Шаблоны конференций NDC для разработки приложений с внедрением зависимостей
- Запуск приложения в ASP.NET Core
- Активация промежуточного программного обеспечения с использованием фабрики в ASP.NET Core
- Четыре способа удаления интерфейсов IDisposable в ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)
- Принцип явных зависимостей
- Контейнеры с инверсией управления и шаблон внедрения зависимостей (Мартин Фаулер)
- How to register a service with multiple interfaces in ASP.NET Core DI (Регистрация службы с несколькими интерфейсами с помощью внедрения зависимостей ASP.NET Core)
ASP.NET Core