Внедрение зависимостей в ASP.NET CoreDependency injection in ASP.NET Core
Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith), Скотт Эдди (Scott Addie) и Брэндон Далер (Brandon Dahler)By Kirk Larkin, Steve Smith, Scott Addie, and Brandon Dahler
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.For more information specific to dependency injection within MVC controllers, see Внедрение зависимостей в контроллеры в ASP.NET Core.
Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.For information on using dependency injection in applications other than web apps, see Dependency injection in .NET.
Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.For more information on dependency injection of options, see Шаблон параметров в ASP.NET Core.
В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core.This topic provides information on dependency injection in ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.The primary documentation on using dependency injection is contained in Dependency injection in .NET.
Просмотреть или скачать образец кода (как скачивать)View or download sample code (how to download)
Общие сведения о внедрении зависимостейOverview of dependency injection
Зависимость — это любой объект, от которого зависит другой объект.A dependency is an object that another object depends on. Рассмотрим следующий класс MyDependency
с методом WriteMessage
, от которого зависят другие классы:Examine the following MyDependency
class with a WriteMessage
method that other classes depend on:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
Класс может создать экземпляр класса MyDependency
, чтобы использовать его метод WriteMessage
.A class can create an instance of the MyDependency
class to make use of its WriteMessage
method. В следующем примере класс MyDependency
выступает зависимостью класса IndexModel
:In the following example, the MyDependency
class is a dependency of the IndexModel
class:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet created this message.");
}
}
Этот класс создает MyDependency
и напрямую зависит от этого класса.The class creates and directly depends on the MyDependency
class. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.Code dependencies, such as in the previous example, are problematic and should be avoided for the following reasons:
- Чтобы заменить
MyDependency
другой реализацией, классIndexModel
необходимо изменить.To replaceMyDependency
with a different implementation, theIndexModel
class must be modified. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять классIndexModel
.IfMyDependency
has dependencies, they must also be configured by theIndexModel
class. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению.In a large project with multiple classes depending onMyDependency
, the configuration code becomes scattered across the app. - Такая реализация плохо подходит для модульных тестов.This implementation is difficult to unit test. В приложении нужно использовать имитацию или заглушку в виде класса
MyDependency
, что при таком подходе невозможно.The app should use a mock or stubMyDependency
class, which isn't possible with this approach.
Внедрение зависимостей устраняет эти проблемы следующим образом:Dependency injection addresses these problems through:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.The use of an interface or base class to abstract the dependency implementation.
- Зависимость регистрируется в контейнере служб.Registration of the dependency in a service container. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider.ASP.NET Core provides a built-in service container, IServiceProvider. Как правило, службы регистрируются в приложении в методе
Startup.ConfigureServices
.Services are typically registered in the app'sStartup.ConfigureServices
method. - Служба внедряется в конструктор класса там, где он используется.Injection of the service into the constructor of the class where it's used. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed.
В примере приложения интерфейс IMyDependency
определяет метод WriteMessage
:In the sample app, the IMyDependency
interface defines the WriteMessage
method:
public interface IMyDependency
{
void WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.This interface is implemented by a concrete type, MyDependency
:
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Пример приложения регистрирует службу IMyDependency
с конкретным типом MyDependency
.The sample app registers the IMyDependency
service with the concrete type MyDependency
. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса.The AddScoped method registers the service with a scoped lifetime, the lifetime of a single request. Подробнее о времени существования служб мы поговорим далее в этой статье.Service lifetimes are described later in this topic.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddRazorPages();
}
В примере приложения запрашивается служба IMyDependency
, которая затем используется для вызова метода WriteMessage
:In the sample app, the IMyDependency
service is requested and used to call the WriteMessage
method:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
Используя шаблон внедрения зависимостей, контроллер:By using the DI pattern, the controller:
- не использует конкретный тип
MyDependency
, только интерфейсIMyDependency
, который он реализует.Doesn't use the concrete typeMyDependency
, only theIMyDependency
interface it implements. Это упрощает изменение реализации, используемой контроллером, без изменения контроллера.That makes it easy to change the implementation that the controller uses without modifying the controller. - не создает экземпляр
MyDependency
, он создается контейнером внедрения зависимостей.Doesn't create an instance ofMyDependency
, it's created by the DI container.
Реализацию интерфейса IMyDependency
можно улучшить с помощью встроенного API ведения журнала:The implementation of the IMyDependency
interface can be improved by using the built-in logging 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
:The updated ConfigureServices
method registers the new IMyDependency
implementation:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency2>();
services.AddRazorPages();
}
MyDependency2
зависит от ILogger<TCategoryName>, который запрашивается в конструкторе.MyDependency2
depends on ILogger<TCategoryName>, which it requests in the constructor. ILogger<TCategoryName>
— это предоставленная платформой служба.ILogger<TCategoryName>
is a framework-provided service.
Использование цепочки внедрений зависимостей не является чем-то необычным.It's not unusual to use dependency injection in a chained fashion. Каждая запрашиваемая зависимость запрашивает собственные зависимости.Each requested dependency in turn requests its own dependencies. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу.The container resolves the dependencies in the graph and returns the fully resolved service. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.The collective set of dependencies that must be resolved is typically referred to as a dependency tree, dependency graph, or object graph.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.The container resolves ILogger<TCategoryName>
by taking advantage of (generic) open types, eliminating the need to register every (generic) constructed type.
В терминологии внедрения зависимостей — служба:In dependency injection terminology, a service:
- Обычно является объектом, предоставляющим службу для других объектов, например службу
IMyDependency
.Is typically an object that provides a service to other objects, such as theIMyDependency
service. - Не относится к веб-службе, хотя служба может использовать веб-службу.Is not related to a web service, although the service may use a web service.
Платформа предоставляет эффективную систему ведения журнала.The framework provides a robust logging system. Реализации IMyDependency
, приведенные в предыдущем примере были написаны для демонстрации базового внедрения зависимостей, а не для реализации ведения журнала.The IMyDependency
implementations shown in the preceding examples were written to demonstrate basic DI, not to implement logging. Большинству приложений не нужно писать средства ведения журнала.Most apps shouldn't need to write loggers. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб в ConfigureServices
:The following code demonstrates using the default logging, which doesn't require any services to be registered in 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
, поскольку платформа предоставляет возможность ведения журнала.Using the preceding code, there is no need to update ConfigureServices
, because logging is provided by the framework.
Службы, внедренные в конструктор StartupServices injected into Startup
Службы можно внедрить в конструктор Startup
и метод Startup.Configure
.Services can be injected into the Startup
constructor and the Startup.Configure
method.
При использовании универсального узла (IHostBuilder) в конструктор Startup
могут внедряться только следующие службы:Only the following services can be injected into the Startup
constructor when using the Generic Host (IHostBuilder):
Любая служба, зарегистрированная в контейнере внедрения зависимостей, может быть внедрена в метод Startup.Configure
:Any service registered with the DI container can be injected into the Startup.Configure
method:
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
...
}
Дополнительные сведения см. в разделах Запуск приложения в ASP.NET Core и Доступ к конфигурации во время запуска.For more information, see Запуск приложения в ASP.NET Core and Access configuration in Startup.
Регистрация групп служб с помощью методов расширенияRegister groups of services with extension methods
Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение.The ASP.NET Core framework uses a convention for registering a group of related services. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME}
для регистрации всех служб, необходимых компоненту платформы.The convention is to use a single Add{GROUP_NAME}
extension method to register all of the services required by a framework feature. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.For example, the AddControllers extension method registers the services required for MVC controllers.
Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:The following code is generated by the Razor Pages template using individual user accounts and shows how to add additional services to the container using the extension methods AddDbContext and 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
, который регистрирует службы и настраивает параметры:Consider the following ConfigureServices
method, which registers services and configures options:
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();
}
Связанные группы регистраций можно переместить в метод расширения для регистрации служб.Related groups of registrations can be moved to an extension method to register services. Например, службы конфигурации добавляются в следующий класс:For example, the configuration services are added to the following class:
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;
}
}
}
Остальные службы регистрируются в аналогичном классе.The remaining services are registered in a similar class. Следующий метод ConfigureServices
использует новые методы расширения для регистрации служб:The following ConfigureServices
method uses the new extension methods to register the services:
public void ConfigureServices(IServiceCollection services)
{
services.AddConfig(Configuration)
.AddMyDependencyGroup();
services.AddRazorPages();
}
Примечание. Каждый метод расширения services.Add{GROUP_NAME}
добавляет и, возможно, настраивает службы.Note: Each services.Add{GROUP_NAME}
extension method adds and potentially configures services. Например, AddControllersWithViews добавляет контроллеры MVC служб с необходимыми представлениями, а AddRazorPages — службы, требуемые для работы Razor Pages.For example, AddControllersWithViews adds the services MVC controllers with views require, and AddRazorPages adds the services Razor Pages requires. Рекомендуем придерживаться этого соглашения об именовании в приложениях.We recommended that apps follow this naming convention. Поместите методы расширения в пространство имен Microsoft.Extensions.DependencyInjection, чтобы инкапсулировать группы зарегистрированных служб.Place extension methods in the Microsoft.Extensions.DependencyInjection namespace to encapsulate groups of service registrations.
Время существования службService lifetimes
См. раздел Время существования службы в статье Внедрение зависимостей в .NET.See Service lifetimes in Dependency injection in .NET
Используйте службы с заданной областью в ПО промежуточного слоя, применяя один из следующих подходов:To use scoped services in middleware, use one of the following approaches:
- Внедрите службу в метод
Invoke
илиInvokeAsync
ПО промежуточного слоя.Inject the service into the middleware'sInvoke
orInvokeAsync
method. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект.Using constructor injection throws a runtime exception because it forces the scoped service to behave like a singleton. В примере в разделе Параметры времени существования и регистрации демонстрируется подходInvokeAsync
.The sample in the Lifetime and registration options section demonstrates theInvokeAsync
approach. - Используйте фабричное ПО промежуточного слоя.Use Factory-based middleware. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в метод
InvokeAsync
ПО промежуточного слоя.Middleware registered using this approach is activated per client request (connection), which allows scoped services to be injected into the middleware'sInvokeAsync
method.
Для получения дополнительной информации см. Написание пользовательского ПО промежуточного слоя ASP.NET Core.For more information, see Написание пользовательского ПО промежуточного слоя ASP.NET Core.
Методы регистрации службыService registration methods
См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.See Service registration methods in Dependency injection in .NET
Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.It's common to use multiple implementations when mocking types for testing.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы.Registering a service with only an implementation type is equivalent to registering that service with the same implementation and service type. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы.This is why multiple implementations of a service cannot be registered using the methods that don't take an explicit service type. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.These methods can register multiple instances of a service, but they will all have the same implementation type.
Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы.Any of the above service registration methods can be used to register multiple service instances of the same service type. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
.In the following example, AddSingleton
is called twice with IMyDependency
as the service type. Второй вызов AddSingleton
переопределяет предыдущий, если он разрешается как IMyDependency
, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>
.The second call to AddSingleton
overrides the previous one when resolved as IMyDependency
and adds to the previous one when multiple services are resolved via IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.Services appear in the order they were registered when resolved via 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);
}
}
Поведение внедрения через конструкторConstructor injection behavior
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.See Constructor injection behavior in Dependency injection in .NET
Контексты Entity FrameworkEntity Framework contexts
По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса.By default, Entity Framework contexts are added to the service container using the scoped lifetime because web app database operations are normally scoped to the client request. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext.To use a different lifetime, specify the lifetime by using an AddDbContext overload. Службы данного времени существования не должны использовать контекст базы данных с временем существования короче, чем у службы.Services of a given lifetime shouldn't use a database context with a lifetime that's shorter than the service's lifetime.
Параметры времени существования и регистрацииLifetime and registration options
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId
.To demonstrate the difference between service lifetimes and their registration options, consider the following interfaces that represent a task as an operation with an identifier, OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.Depending on how the lifetime of an operation's service is configured for the following interfaces, the container provides either the same or different instances of the service when requested by a class:
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Следующий класс Operation
реализует все предыдущие интерфейсы.The following Operation
class implements all of the preceding interfaces. Конструктор Operation
создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId
:The Operation
constructor generates a GUID and stores the last 4 characters in the OperationId
property:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
Метод Startup.ConfigureServices
создает несколько регистраций класса Operation
в соответствии с именованным временем существования:The Startup.ConfigureServices
method creates multiple registrations of the Operation
class according to the named lifetimes:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddRazorPages();
}
В примере приложения показано время существования объектов в пределах запросов и между запросами.The sample app demonstrates object lifetimes both within and between requests. IndexModel
и ПО промежуточного слоя запрашивают каждый тип IOperation
и регистрируют OperationId
для каждого из них:The IndexModel
and the middleware request each kind of IOperation
type and log the OperationId
for each:
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
, ПО промежуточного слоя и разрешает те же службы:Similar to the IndexModel
, the middleware resolves the same services:
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
:Scoped services must be resolved in the InvokeAsync
method:
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);
}
Выходные данные средства ведения журнала содержат:The logger output shows:
- Временные объекты всегда разные.Transient objects are always different. Значение временного
OperationId
отличается вIndexModel
и ПО промежуточного слоя.The transientOperationId
value is different in theIndexModel
and in the middleware. - Объекты с заданной областью остаются неизменными в пределах одного запроса, но в разных запросах используются разные объекты.Scoped objects are the same for each request but different across each request.
- Одноэлементные объекты одинаковы для каждого запроса.Singleton objects are the same for every request.
Чтобы уменьшить объем выводимых данных журнала, задайте в файле appsettings.Development.json параметр "Logging:LogLevel:Microsoft:Error".To reduce the logging output, set "Logging:LogLevel:Microsoft:Error" in the appsettings.Development.json file:
{
"MyKey": "MyKey from appsettings.Developement.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
Вызов служб из функции mainCall services from main
Создайте IServiceScope с IServiceScopeFactory.CreateScope для разрешения службы с заданной областью в области приложения.Create an IServiceScope with IServiceScopeFactory.CreateScope to resolve a scoped service within the app's scope. Этот способ позволит получить доступ к службе с заданной областью при запуске для выполнения задач по инициализации.This approach is useful to access a scoped service at startup to run initialization tasks.
В следующем примере показано, как получить доступ к службе IMyDependency
с заданной областью и вызвать ее метод WriteMessage
в Program.Main
:The following example shows how to access the scoped IMyDependency
service and call its WriteMessage
method in 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>();
});
}
Проверка областиScope validation
См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.See Constructor injection behavior in Dependency injection in .NET
Дополнительные сведения см. в разделе Проверка области.For more information, see Scope validation.
Службы запросовRequest Services
Службы, доступные в пределах запроса ASP.NET Core, предоставляются через коллекцию HttpContext.RequestServices.The services available within an ASP.NET Core request are exposed through the HttpContext.RequestServices collection. При запросе в рамках запроса службы и их зависимости разрешаются из коллекции RequestServices
.When services are requested from inside of a request, the services and their dependencies are resolved from the RequestServices
collection.
Платформа создает область для каждого запроса, а RequestServices
предоставляет поставщик услуг с заданной областью.The framework creates a scope per request and RequestServices
exposes the scoped service provider. Все службы с заданной областью действительны до тех пор, пока запрос активен.All scoped services are valid for as long as the request is active.
Примечание
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не разрешать службы из коллекции RequestServices
.Prefer requesting dependencies as constructor parameters to resolving services from the RequestServices
collection. В результате создаются классы, которые легче тестировать.This results in classes that are easier to test.
Проектирование служб для внедрения зависимостейDesign services for dependency injection
При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:When designing services for dependency injection:
- Избегайте статических классов и членов с отслеживанием состояния.Avoid stateful, static classes and members. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.Avoid creating global state by designing apps to use singleton services instead.
- Избегайте прямого создания экземпляров зависимых классов внутри служб.Avoid direct instantiation of dependent classes within services. Прямое создание экземпляров обязывает использовать в коде определенную реализацию.Direct instantiation couples the code to a particular implementation.
- Сделайте службы приложения небольшими, хорошо организованными и удобными в тестировании.Make services small, well-factored, and easily tested.
Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности.If a class has a lot of injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP). Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы.Attempt to refactor the class by moving some of its responsibilities into new classes. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.Keep in mind that Razor Pages page model classes and MVC controller classes should focus on UI concerns.
Удаление службDisposal of services
Контейнер вызывает Dispose для создаваемых им типов IDisposable.The container calls Dispose for the IDisposable types it creates. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком.Services resolved from the container should never be disposed by the developer. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.If a type or factory is registered as a singleton, the container disposes the singleton automatically.
В следующем примере службы создаются контейнером службы и автоматически удаляются:In the following example, the services are created by the service container and disposed automatically:
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");
}
}
После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:The debug console shows the following output after each refresh of the Index page:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet
Service1.Dispose
Службы, не созданные контейнером службыServices not created by the service container
Рассмотрим следующий код.Consider the following code:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new Service1());
services.AddSingleton(new Service2());
services.AddRazorPages();
}
В приведенном выше коде:In the preceding code:
- Экземпляры службы не создаются контейнером службы.The service instances aren't created by the service container.
- Платформа не удаляет службы автоматически.The framework doesn't dispose of the services automatically.
- За удаление служб отвечает разработчик.The developer is responsible for disposing the services.
Руководство по применению временных и общих экземпляров IDisposableIDisposable guidance for Transient and shared instances
См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.See IDisposable guidance for Transient and shared instance in Dependency injection in .NET
Замена стандартного контейнера службDefault service container replacement
См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.See Default service container replacement in Dependency injection in .NET
РекомендацииRecommendations
См. раздел Рекомендации в статье Внедрение зависимостей в .NET.See Recommendations in Dependency injection in .NET
Старайтесь не использовать схему указателя служб.Avoid using the service locator pattern. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей:For example, don't invoke GetService to obtain a service instance when you can use DI instead:
Неправильно:Incorrect:
Правильно:Correct:
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Другой вариант указателя службы, позволяющий избежать этого, — внедрение фабрики, которая разрешает зависимости во время выполнения.Another service locator variation to avoid is injecting a factory that resolves dependencies at runtime. Оба метода смешивают стратегии инверсии управления.Both of these practices mix Inversion of Control strategies.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).Avoid static access toHttpContext
(for example, IHttpContextAccessor.HttpContext).
Избегайте вызовов BuildServiceProvider в
ConfigureServices
.Avoid calls to BuildServiceProvider inConfigureServices
. ВызовBuildServiceProvider
обычно происходит, когда разработчику необходимо разрешить службу вConfigureServices
.CallingBuildServiceProvider
typically happens when the developer wants to resolve a service inConfigureServices
. Например, рассмотрим случай, когдаLoginPath
загружается из конфигурации.For example, consider the case where theLoginPath
is loaded from configuration. Добавьте следующий код:Avoid the following approach:На предыдущем рисунке при выборе строки, отмеченной зеленой волнистой линией в разделе
services.BuildServiceProvider
, отображается следующее предупреждение ASP0000:In the preceding image, selecting the green wavy line underservices.BuildServiceProvider
shows the following ASP0000 warning:ASP0000. Вызов BuildServiceProvider из кода приложения приводит к созданию дополнительной копии создаваемых одноэлементных служб.ASP0000 Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. В качестве параметров для Configure можно использовать альтернативные варианты, такие как службы внедрения зависимостей.Consider alternatives such as dependency injecting services as parameters to 'Configure'.
При вызове
BuildServiceProvider
создается второй контейнер, который может создавать разорванные одноэлементные экземпляры и ссылаться на графы объектов в нескольких контейнерах.CallingBuildServiceProvider
creates a second container, which can create torn singletons and cause references to object graphs across multiple containers.Правильный способ получения
LoginPath
— использование встроенной поддержки шаблона параметров для внедрения зависимостей:A correct way to getLoginPath
is to use the options pattern's built-in support for DI: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(); }
Неудаляемые временные службы фиксируются контейнером для их удаления.Disposable transient services are captured by the container for disposal. Это может привести к утечке памяти, если она разрешена из контейнера верхнего уровня.This can turn into a memory leak if resolved from the top level container.
Включите проверку области, чтобы убедиться, что в приложении нет отдельных объектов, записывающих службы с заданной областью.Enable scope validation to make sure the app doesn't have singletons that capture scoped services. Дополнительные сведения см. в разделе Проверка области.For more information, see Scope validation.
Как и с любыми рекомендациями, у вас могут возникнуть ситуации, когда нужно отступить от одного из правил.Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. Исключения возникают редко, — как правило, это особые случаи, связанные с самой платформой.Exceptions are rare, mostly special cases within the framework itself.
Внедрение зависимостей является альтернативой для шаблонов доступа к статическим или глобальным объектам.DI is an alternative to static/global object access patterns. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если будете сочетать его с доступом к статическим объектам.You may not be able to realize the benefits of DI if you mix it with static object access.
Рекомендуемые подходы к мультитенантности при внедрении зависимостейRecommended patterns for multi-tenancy in DI
Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core.Orchard Core is an application framework for building modular, multi-tenant applications on ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.For more information, see the Orchard Core Documentation.
Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.See the Orchard Core samples for examples of how to build modular and multi-tenant apps using just the Orchard Core Framework without any of its CMS-specific features.
Платформенные службыFramework-provided services
Метод Startup.ConfigureServices
регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC.The Startup.ConfigureServices
method registers services that the app uses, including platform features, such as Entity Framework Core and ASP.NET Core MVC. Изначально коллекция IServiceCollection
, предоставленная для ConfigureServices
, содержит определенные платформой службы (в зависимости от настройки узла).Initially, the IServiceCollection
provided to ConfigureServices
has services defined by the framework depending on how the host was configured. Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.For apps based on the ASP.NET Core templates, the framework registers more than 250 services.
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.The following table lists a small sample of these framework-registered services:
Тип службыService Type | Время существованияLifetime |
---|---|
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory | ВременныйTransient |
IHostApplicationLifetime | ОдноэлементныйSingleton |
IWebHostEnvironment | ОдноэлементныйSingleton |
Microsoft.AspNetCore.Hosting.IStartup | ОдноэлементныйSingleton |
Microsoft.AspNetCore.Hosting.IStartupFilter | ВременныйTransient |
Microsoft.AspNetCore.Hosting.Server.IServer | ОдноэлементныйSingleton |
Microsoft.AspNetCore.Http.IHttpContextFactory | ВременныйTransient |
Microsoft.Extensions.Logging.ILogger<TCategoryName> | ОдноэлементныйSingleton |
Microsoft.Extensions.Logging.ILoggerFactory | ОдноэлементныйSingleton |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | ОдноэлементныйSingleton |
Microsoft.Extensions.Options.IConfigureOptions<TOptions> | ВременныйTransient |
Microsoft.Extensions.Options.IOptions<TOptions> | ОдноэлементныйSingleton |
System.Diagnostics.DiagnosticSource | ОдноэлементныйSingleton |
System.Diagnostics.DiagnosticListener | ОдноэлементныйSingleton |
Дополнительные ресурсыAdditional resources
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Внедрение зависимостей Blazor в ASP.NET Core
- Шаблоны конференций NDC для разработки приложений с внедрением зависимостейNDC Conference Patterns for DI app development
- Запуск приложения в ASP.NET Core
- Активация ПО промежуточного слоя на основе фабрики в ASP.NET Core
- Четыре способа удаления интерфейсов IDisposable в ASP.NET CoreFour ways to dispose IDisposables in ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)Writing Clean Code in ASP.NET Core with Dependency Injection (MSDN)
- Принцип явных зависимостейExplicit Dependencies Principle
- Контейнеры с инверсией управления и шаблон внедрения зависимостей (Мартин Фаулер)Inversion of Control Containers and the Dependency Injection Pattern (Martin Fowler)
- How to register a service with multiple interfaces in ASP.NET Core DI (Регистрация службы с несколькими интерфейсами с помощью внедрения зависимостей ASP.NET Core)How to register a service with multiple interfaces in ASP.NET Core DI
Авторы: Стив Смит (Steve Smith), Скотт Эдди (Scott Addie) и Брэндон Далер (Brandon Dahler)By Steve Smith, Scott Addie, and Brandon Dahler
ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.
Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.For more information specific to dependency injection within MVC controllers, see Внедрение зависимостей в контроллеры в ASP.NET Core.
Просмотреть или скачать образец кода (как скачивать)View or download sample code (how to download)
Общие сведения о внедрении зависимостейOverview of dependency injection
Зависимость — это любой объект, который требуется другому объекту.A dependency is any object that another object requires. Рассмотрим класс MyDependency
с методом WriteMessage
, от которого зависят другие классы в приложении.Examine the following MyDependency
class with a WriteMessage
method that other classes in an app depend upon:
public class MyDependency
{
public MyDependency()
{
}
public Task WriteMessage(string message)
{
Console.WriteLine(
$"MyDependency.WriteMessage called. Message: {message}");
return Task.FromResult(0);
}
}
Чтобы сделать метод WriteMessage
доступным какому-то классу, можно создать экземпляр класса MyDependency
.An instance of the MyDependency
class can be created to make the WriteMessage
method available to a class. Класс MyDependency
выступает зависимостью класса IndexModel
.The MyDependency
class is a dependency of the IndexModel
class:
public class IndexModel : PageModel
{
MyDependency _dependency = new MyDependency();
public async Task OnGetAsync()
{
await _dependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Этот класс создает экземпляр MyDependency
и напрямую зависит от него.The class creates and directly depends on the MyDependency
instance. Зависимости в коде (как в предыдущем примере) представляют собой определенную проблему. Их следует избегать по следующим причинам:Code dependencies (such as the previous example) are problematic and should be avoided for the following reasons:
- Чтобы заменить
MyDependency
другой реализацией, класс необходимо изменить.To replaceMyDependency
with a different implementation, the class must be modified. - Если у
MyDependency
есть зависимости, их конфигурацию должен выполнять класс.IfMyDependency
has dependencies, they must be configured by the class. В больших проектах, когда отMyDependency
зависят многие классы, код конфигурации растягивается по всему приложению.In a large project with multiple classes depending onMyDependency
, the configuration code becomes scattered across the app. - Такая реализация плохо подходит для модульных тестов.This implementation is difficult to unit test. В приложении нужно использовать имитацию или заглушку в виде класса
MyDependency
, что при таком подходе невозможно.The app should use a mock or stubMyDependency
class, which isn't possible with this approach.
Внедрение зависимостей устраняет эти проблемы следующим образом:Dependency injection addresses these problems through:
- Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.The use of an interface or base class to abstract the dependency implementation.
- Зависимость регистрируется в контейнере служб.Registration of the dependency in a service container. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider.ASP.NET Core provides a built-in service container, IServiceProvider. Службы регистрируются в приложении в методе
Startup.ConfigureServices
.Services are registered in the app'sStartup.ConfigureServices
method. - Служба внедряется в конструктор класса там, где он используется.Injection of the service into the constructor of the class where it's used. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed.
В примере приложения интерфейс IMyDependency
определяет метод, который служба предоставляет приложению.In the sample app, the IMyDependency
interface defines a method that the service provides to the app:
public interface IMyDependency
{
Task WriteMessage(string message);
}
Этот интерфейс реализуется конкретным типом, MyDependency
.This interface is implemented by a concrete type, MyDependency
:
public class MyDependency : IMyDependency
{
private readonly ILogger<MyDependency> _logger;
public MyDependency(ILogger<MyDependency> logger)
{
_logger = logger;
}
public Task WriteMessage(string message)
{
_logger.LogInformation(
"MyDependency.WriteMessage called. Message: {Message}",
message);
return Task.FromResult(0);
}
}
MyDependency
запрашивает ILogger<TCategoryName> в своем конструкторе.MyDependency
requests an ILogger<TCategoryName> in its constructor. Использование цепочки внедрений зависимостей не является чем-то необычным.It's not unusual to use dependency injection in a chained fashion. Каждая запрашиваемая зависимость запрашивает собственные зависимости.Each requested dependency in turn requests its own dependencies. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу.The container resolves the dependencies in the graph and returns the fully resolved service. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.The collective set of dependencies that must be resolved is typically referred to as a dependency tree, dependency graph, or object graph.
IMyDependency
и ILogger<TCategoryName>
должны быть зарегистрированы в контейнере служб.IMyDependency
and ILogger<TCategoryName>
must be registered in the service container. IMyDependency
регистрируется в Startup.ConfigureServices
.IMyDependency
is registered in Startup.ConfigureServices
. Службу ILogger<TCategoryName>
регистрирует инфраструктура абстракций ведения журналов, поэтому такая служба является платформенной, что означает, что ее по умолчанию регистрирует платформа.ILogger<TCategoryName>
is registered by the logging abstractions infrastructure, so it's a framework-provided service registered by default by the framework.
Контейнер разрешает ILogger<TCategoryName>
, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.The container resolves ILogger<TCategoryName>
by taking advantage of (generic) open types, eliminating the need to register every (generic) constructed type:
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
В примере приложения служба IMyDependency
зарегистрирована с конкретным типом MyDependency
.In the sample app, the IMyDependency
service is registered with the concrete type MyDependency
. Регистрация регулирует время существования службы согласно времени существования одного запроса.The registration scopes the service lifetime to the lifetime of a single request. Подробнее о времени существования служб мы поговорим далее в этой статье.Service lifetimes are described later in this topic.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
// OperationService depends on each of the other Operation types.
services.AddTransient<OperationService, OperationService>();
}
Примечание
Каждый метод расширения services.Add{SERVICE_NAME}
добавляет и, возможно, настраивает службы.Each services.Add{SERVICE_NAME}
extension method adds, and potentially configures, services. Например, services.AddControllersWithViews
, services.AddRazorPages
и services.AddControllers
добавляют службы, необходимые приложениям ASP.NET Core.For example, services.AddControllersWithViews
, services.AddRazorPages
, and services.AddControllers
adds the services ASP.NET Core apps require. Рекомендуем придерживаться такого подхода в приложениях.We recommended that apps follow this convention. Поместите методы расширения в пространство имен Microsoft.Extensions.DependencyInjection, чтобы инкапсулировать группы зарегистрированных служб.Place extension methods in the Microsoft.Extensions.DependencyInjection namespace to encapsulate groups of service registrations. Включение части Microsoft.Extensions.DependencyInjection
пространства имен для методов расширения внедрения зависимостей также:Including the namespace portion Microsoft.Extensions.DependencyInjection
for DI extension methods also:
- позволяет отображать их в IntelliSense без добавления дополнительных блоков
using
;Allows them to be displayed in IntelliSense without adding additionalusing
blocks. - позволяет избежать чрезмерного количества инструкций
using
в классеStartup
, из которого обычно вызываются эти методы расширения.Prevents excessiveusing
statements in theStartup
class where these extension methods are typically called from.
Если конструктору службы требуется встроенный тип, например string
, его можно внедрить с помощью конфигурации или шаблона параметров.If the service's constructor requires a built in type, such as a string
, the type can be injected by using configuration or the options pattern:
public class MyDependency : IMyDependency
{
public MyDependency(IConfiguration config)
{
var myStringValue = config["MyStringKey"];
// Use myStringValue
}
...
}
Экземпляр службы запрашивается через конструктор класса, в котором эта служба используется и назначается скрытому полю.An instance of the service is requested via the constructor of a class where the service is used and assigned to a private field. Поле используется во всем классе для доступа к службе (по мере необходимости).The field is used to access the service as necessary throughout the class.
В примере приложения запрашивается экземпляр IMyDependency
, который затем используется для вызова метода службы WriteMessage
.In the sample app, the IMyDependency
instance is requested and used to call the service's WriteMessage
method:
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
public OperationService OperationService { get; }
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
public async Task OnGetAsync()
{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Службы, внедренные в конструктор StartupServices injected into Startup
При использовании универсального узла (IHostBuilder) в конструктор Startup
могут внедряться только следующие типы служб:Only the following service types can be injected into the Startup
constructor when using the Generic Host (IHostBuilder):
IWebHostEnvironment
- IHostEnvironment
- IConfiguration
Службы можно внедрить в Startup.Configure
:Services can be injected into Startup.Configure
:
public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
{
...
}
Дополнительные сведения см. в разделе Запуск приложения в ASP.NET Core.For more information, see Запуск приложения в ASP.NET Core.
Платформенные службыFramework-provided services
Метод Startup.ConfigureServices
отвечает за определение служб, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC.The Startup.ConfigureServices
method is responsible for defining the services that the app uses, including platform features, such as Entity Framework Core and ASP.NET Core MVC. Изначально коллекция IServiceCollection
, предоставленная для ConfigureServices
, содержит определенные платформой службы (в зависимости от настройки узла).Initially, the IServiceCollection
provided to ConfigureServices
has services defined by the framework depending on how the host was configured. Приложение, основанное на шаблоне ASP.NET Core, часто содержит сотни служб, зарегистрированных платформой.It's not uncommon for an app based on an ASP.NET Core template to have hundreds of services registered by the framework. В следующей таблице перечислены некоторые примеры зарегистрированных платформой служб.A small sample of framework-registered services is listed in the following table.
Регистрация дополнительных служб с помощью методов расширенияRegister additional services with extension methods
Если зарегистрировать службу (включая ее зависимые службы, если нужно) можно, используя метод расширения коллекции служб, для регистрации всех служб, необходимых этой службе, рекомендуется использовать один метод расширения Add{SERVICE_NAME}
.When a service collection extension method is available to register a service (and its dependent services, if required), the convention is to use a single Add{SERVICE_NAME}
extension method to register all of the services required by that service. Ниже приведен пример того, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext<TContext> и AddIdentityCore:The following code is an example of how to add additional services to the container using the extension methods AddDbContext<TContext> and AddIdentityCore:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
...
}
Дополнительные сведения см. в разделе о классе ServiceCollection в документации по API-интерфейсам.For more information, see the ServiceCollection class in the API documentation.
Время существования службService lifetimes
Для каждой зарегистрированной службы выбирайте подходящее время существования.Choose an appropriate lifetime for each registered service. Для служб ASP.NET можно настроить следующие параметры времени существования:ASP.NET Core services can be configured with the following lifetimes:
ВременныйTransient
Временные службы времени существования (AddTransient) создаются при каждом их запросе из контейнера служб.Transient lifetime services (AddTransient) are created each time they're requested from the service container. Это время существования лучше всего подходит для простых служб без отслеживания состояния.This lifetime works best for lightweight, stateless services.
В приложениях, обрабатывающих запросы, временные службы удаляются в конце запроса.In apps that process requests, transient services are disposed at the end of the request.
Область действияScoped
Службы времени существования с заданной областью (AddScoped) создаются один раз для каждого клиентского запроса (подключения).Scoped lifetime services (AddScoped) are created once per client request (connection).
В приложениях, обрабатывающих запросы, службы с заданной областью удаляются в конце запроса.In apps that process requests, scoped services are disposed at the end of the request.
Предупреждение
При использовании такой службы в ПО промежуточного слоя внедрите ее в метод Invoke
или InvokeAsync
.When using a scoped service in a middleware, inject the service into the Invoke
or InvokeAsync
method. Не внедряйте службу с помощью внедрения через конструктор, поскольку в таком случае служба будет вести себя как одноэлементный объект.Don't inject via constructor injection because it forces the service to behave like a singleton. Дополнительные сведения см. в разделе Написание пользовательского ПО промежуточного слоя ASP.NET Core.For more information, see Написание пользовательского ПО промежуточного слоя ASP.NET Core.
ОдноэлементныйSingleton
Отдельные службы времени существования (AddSingleton) создаются при первом запросе (или при выполнении Startup.ConfigureServices
, когда экземпляр указан во время регистрации службы).Singleton lifetime services (AddSingleton) are created the first time they're requested (or when Startup.ConfigureServices
is run and an instance is specified with the service registration). Созданный экземпляр используют все последующие запросы.Every subsequent request uses the same instance. Если в приложении нужно использовать одинарные службы, рекомендуется разрешить контейнеру служб управлять временем их существования.If the app requires singleton behavior, allowing the service container to manage the service's lifetime is recommended. Реализовывать конструктивный шаблон с одинарными службами и предоставлять пользовательский код для управления временем существования объекта в классе категорически не рекомендуется.Don't implement the singleton design pattern and provide user code to manage the object's lifetime in the class.
В приложениях, обрабатывающих запросы, отдельные службы удаляются, когда ServiceProvider удаляется по завершении работы приложения.In apps that process requests, singleton services are disposed when the ServiceProvider is disposed at app shutdown.
Предупреждение
Разрешать регулируемую службу из одиночной нельзя.It's dangerous to resolve a scoped service from a singleton. При обработке последующих запросов это может вызвать неправильное состояние службы.It may cause the service to have incorrect state when processing subsequent requests.
Методы регистрации службыService registration methods
Методы расширения регистрации службы предлагают перегрузки, которые полезны в определенных сценариях.Service registration extension methods offer overloads that are useful in specific scenarios.
МетодMethod | АвтоматическиAutomatic objectobject удалениеdisposal |
НесколькоMultiple реализацииimplementations |
Передача аргументовPass args |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() ПримерExample: services.AddSingleton<IMyDep, MyDep>(); |
ДаYes | ДаYes | НетNo |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) Примеры:Examples: services.AddSingleton<IMyDep>(sp => new MyDep()); services.AddSingleton<IMyDep>(sp => new MyDep("A string!")); |
ДаYes | ДаYes | ДаYes |
Add{LIFETIME}<{IMPLEMENTATION}>() ПримерExample: services.AddSingleton<MyDep>(); |
ДаYes | НетNo | НетNo |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) Примеры:Examples: services.AddSingleton<IMyDep>(new MyDep()); services.AddSingleton<IMyDep>(new MyDep("A string!")); |
НетNo | ДаYes | ДаYes |
AddSingleton(new {IMPLEMENTATION}) Примеры:Examples: services.AddSingleton(new MyDep()); services.AddSingleton(new MyDep("A string!")); |
НетNo | НетNo | ДаYes |
Дополнительные сведения об удалении типа см. в разделе Удаление служб.For more information on type disposal, see the Disposal of services section. Распространенный сценарий для нескольких реализаций — макеты типов для тестирования.A common scenario for multiple implementations is mocking types for testing.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы.Registering a service with only an implementation type is equivalent to registering that service with the same implementation and service type. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы.This is why multiple implementations of a service cannot be registered using the methods that don't take an explicit service type. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.These methods can register multiple instances of a service, but they will all have the same implementation type.
Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы.Any of the above service registration methods can be used to register multiple service instances of the same service type. В следующем примере метод AddSingleton
вызывается дважды с типом службы IMyDependency
.In the following example, AddSingleton
is called twice with IMyDependency
as the service type. Второй вызов AddSingleton
переопределяет предыдущий, если он разрешается как IMyDependency
, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>
.The second call to AddSingleton
overrides the previous one when resolved as IMyDependency
and adds to the previous one when multiple services are resolved via IEnumerable<IMyDependency>
. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>
.Services appear in the order they were registered when resolved via 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);
}
}
Платформа также предоставляет методы расширения TryAdd{LIFETIME}
, которые регистрируют службу только в том случае, если реализация еще не зарегистрирована.The framework also provides TryAdd{LIFETIME}
extension methods, which register the service only if there isn't already an implementation registered.
В следующем примере вызов AddSingleton
регистрирует MyDependency
как реализацию для IMyDependency
.In the following example, the call to AddSingleton
registers MyDependency
as an implementation for IMyDependency
. Вызов TryAddSingleton
ничего не делает, поскольку у IMyDependency
уже есть зарегистрированная реализация.The call to TryAddSingleton
has no effect because IMyDependency
already has a registered implementation.
services.AddSingleton<IMyDependency, MyDependency>();
// The following line has no effect:
services.TryAddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is MyDependency);
Trace.Assert(myDependencies.Single() is MyDependency);
}
}
Дополнительные сведения см. в разделе:For more information, see:
Методы TryAddEnumerable(ServiceDescriptor) регистрируют службу только в том случае, если еще не существует реализации того же типа.TryAddEnumerable(ServiceDescriptor) methods only register the service if there isn't already an implementation of the same type. Несколько служб разрешается через IEnumerable<{SERVICE}>
.Multiple services are resolved via IEnumerable<{SERVICE}>
. При регистрации служб разработчик хочет добавить экземпляр только в том случае, если экземпляр такого типа еще не был добавлен.When registering services, the developer only wants to add an instance if one of the same type hasn't already been added. Как правило, этот метод используется авторами библиотек, чтобы избежать регистрации двух копий экземпляра в контейнере.Generally, this method is used by library authors to avoid registering two copies of an instance in the container.
В следующем примере первая строка регистрирует MyDep
для IMyDep1
.In the following example, the first line registers MyDep
for IMyDep1
. Вторая строка регистрирует MyDep
для IMyDep2
.The second line registers MyDep
for IMyDep2
. Третья строка ничего не делает, поскольку у IMyDep1
уже есть зарегистрированная реализация MyDep
:The third line has no effect because IMyDep1
already has a registered implementation of MyDep
:
public interface IMyDep1 {}
public interface IMyDep2 {}
public class MyDep : IMyDep1, IMyDep2 {}
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep2, MyDep>());
// Two registrations of MyDep for IMyDep1 is avoided by the following line:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
Поведение внедрения через конструкторConstructor injection behavior
Службы можно разрешать двумя методами:Services can be resolved by two mechanisms:
- IServiceProvider
- ActivatorUtilities. Позволяет создавать объекты без регистрации службы в контейнере внедрения зависимостей.ActivatorUtilities: Permits object creation without service registration in the dependency injection container.
ActivatorUtilities
используется с абстракциями пользовательского уровня, например со вспомогательными функциями для тегов, контроллерами MVC, и связывателями моделей.ActivatorUtilities
is used with user-facing abstractions, such as Tag Helpers, MVC controllers, and model binders.
Конструкторы могут принимать аргументы, которые не предоставляются внедрением зависимостей, но эти аргументы должны назначать значения по умолчанию.Constructors can accept arguments that aren't provided by dependency injection, but the arguments must assign default values.
Когда разрешение служб выполняется через IServiceProvider
или ActivatorUtilities
, для внедрения через конструктор требуется открытый конструктор.When services are resolved by IServiceProvider
or ActivatorUtilities
, constructor injection requires a public constructor.
Когда разрешение служб выполняется через ActivatorUtilities
, для внедрения через конструктор требуется наличие только одного соответствующего конструктора.When services are resolved by ActivatorUtilities
, constructor injection requires that only one applicable constructor exists. Перегрузки конструктора поддерживаются, но может существовать всего одна перегрузка, все аргументы которой могут быть обработаны с помощью внедрения зависимостей.Constructor overloads are supported, but only one overload can exist whose arguments can all be fulfilled by dependency injection.
Контексты Entity FrameworkEntity Framework contexts
Контексты Entity Framework обычно добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса.Entity Framework contexts are usually added to the service container using the scoped lifetime because web app database operations are normally scoped to the client request. Время существования по умолчанию имеет заданную область, если время существования не указано в перегрузке AddDbContext<TContext> при регистрации контекста базы данных.The default lifetime is scoped if a lifetime isn't specified by an AddDbContext<TContext> overload when registering the database context. Службы данного времени существования не должны использовать контекст базы данных с временем существования короче, чем у службы.Services of a given lifetime shouldn't use a database context with a shorter lifetime than the service.
Параметры времени существования и регистрацииLifetime and registration options
Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации, рассмотрим интерфейсы, представляющие задачи в виде операции с уникальным идентификатором OperationId
.To demonstrate the difference between the lifetime and registration options, consider the following interfaces that represent tasks as an operation with a unique identifier, OperationId
. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.Depending on how the lifetime of an operations service is configured for the following interfaces, the container provides either the same or a different instance of the service when requested by a class:
public interface IOperation
{
Guid OperationId { get; }
}
public interface IOperationTransient : IOperation
{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}
Интерфейсы реализованы в классе Operation
.The interfaces are implemented in the Operation
class. Если идентификатор GUID не предоставлен, конструктор Operation
создает его.The Operation
constructor generates a GUID if one isn't supplied:
public class Operation : IOperationTransient,
IOperationScoped,
IOperationSingleton,
IOperationSingletonInstance
{
public Operation() : this(Guid.NewGuid())
{
}
public Operation(Guid id)
{
OperationId = id;
}
public Guid OperationId { get; private set; }
}
Регистрируется служба OperationService
, которая зависит от каждого из других типов Operation
.An OperationService
is registered that depends on each of the other Operation
types. Когда служба OperationService
запрашивается через внедрение зависимостей, она получает либо новый экземпляр каждой службы, либо экземпляр уже существующей службы в зависимости от времени существования зависимой службы.When OperationService
is requested via dependency injection, it receives either a new instance of each service or an existing instance based on the lifetime of the dependent service.
- Если при запросе из контейнера создаются временные службы,
OperationId
службыIOperationTransient
отличается отOperationId
службыOperationService
.When transient services are created when requested from the container, theOperationId
of theIOperationTransient
service is different than theOperationId
of theOperationService
.OperationService
получает новый экземпляр классаIOperationTransient
.OperationService
receives a new instance of theIOperationTransient
class. Новый экземпляр возвращает другой идентификаторOperationId
.The new instance yields a differentOperationId
. - Если при каждом клиентском запросе создаются регулируемые службы, идентификатор
OperationId
службыIOperationScoped
будет таким же, как для службыOperationService
в клиентском запросе.When scoped services are created per client request, theOperationId
of theIOperationScoped
service is the same as that ofOperationService
within a client request. В разных клиентских запросах обе службы используют разные значенияOperationId
.Across client requests, both services share a differentOperationId
value. - Если одинарные службы и службы с одинарным экземпляром создаются один раз и используются во всех клиентских запросах и службах, идентификатор
OperationId
будет одинаковым во всех запросах служб.When singleton and singleton-instance services are created once and used across all client requests and all services, theOperationId
is constant across all service requests.
public class OperationService
{
public OperationService(
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance instanceOperation)
{
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = instanceOperation;
}
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
}
В Startup.ConfigureServices
каждый тип добавляется в контейнер согласно его времени существования.In Startup.ConfigureServices
, each type is added to the container according to its named lifetime:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
// OperationService depends on each of the other Operation types.
services.AddTransient<OperationService, OperationService>();
}
Служба IOperationSingletonInstance
использует определенный экземпляр с известным идентификатором Guid.Empty
.The IOperationSingletonInstance
service is using a specific instance with a known ID of Guid.Empty
. Понятно, когда этот тип используется (его GUID — все нули).It's clear when this type is in use (its GUID is all zeroes).
В примере приложения показано время существования объектов в пределах одного и нескольких запросов.The sample app demonstrates object lifetimes within and between individual requests. В примере приложения IndexModel
запрашивает каждый вид типа IOperation
, а также OperationService
.The sample app's IndexModel
requests each kind of IOperation
type and the OperationService
. После этого на странице отображаются все значения OperationId
службы и класса модели страниц в виде назначенных свойств.The page then displays all of the page model class's and service's OperationId
values through property assignments:
public class IndexModel : PageModel
{
private readonly IMyDependency _myDependency;
public IndexModel(
IMyDependency myDependency,
OperationService operationService,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation,
IOperationSingletonInstance singletonInstanceOperation)
{
_myDependency = myDependency;
OperationService = operationService;
TransientOperation = transientOperation;
ScopedOperation = scopedOperation;
SingletonOperation = singletonOperation;
SingletonInstanceOperation = singletonInstanceOperation;
}
public OperationService OperationService { get; }
public IOperationTransient TransientOperation { get; }
public IOperationScoped ScopedOperation { get; }
public IOperationSingleton SingletonOperation { get; }
public IOperationSingletonInstance SingletonInstanceOperation { get; }
public async Task OnGetAsync()
{
await _myDependency.WriteMessage(
"IndexModel.OnGetAsync created this message.");
}
}
Результаты двух запросов будут выглядеть так:Two following output shows the results of two requests:
Первый запросFirst request:
Операции контроллера:Controller operations:
Transient: d233e165-f417-469b-a866-1cf1935d2518Transient: d233e165-f417-469b-a866-1cf1935d2518
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000Instance: 00000000-0000-0000-0000-000000000000
Операции OperationService
:OperationService
operations:
Transient: c6b049eb-1318-4e31-90f1-eb2dd849ff64Transient: c6b049eb-1318-4e31-90f1-eb2dd849ff64
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000Instance: 00000000-0000-0000-0000-000000000000
Второй запросSecond request:
Операции контроллера:Controller operations:
Transient: b63bd538-0a37-4ff1-90ba-081c5138dda0Transient: b63bd538-0a37-4ff1-90ba-081c5138dda0
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000Instance: 00000000-0000-0000-0000-000000000000
Операции OperationService
:OperationService
operations:
Transient: c4cbacb8-36a2-436d-81c8-8c1b78808aafTransient: c4cbacb8-36a2-436d-81c8-8c1b78808aaf
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000Instance: 00000000-0000-0000-0000-000000000000
Проанализируйте, какие значения OperationId
изменяются в пределах одного и нескольких запросов.Observe which of the OperationId
values vary within a request and between requests:
- Временные объекты всегда разные.Transient objects are always different. Временное значение
OperationId
отличается в первом и втором клиентских запросах, а также в обеих операцияхOperationService
.The transientOperationId
value for both the first and second client requests are different for bothOperationService
operations and across client requests. Каждому запросу службы и каждому клиентскому запросу предоставляется новый экземпляр.A new instance is provided to each service request and client request. - Ограниченные по области объекты одинаковы в рамках клиентского запроса, но различаются для разных запросов.Scoped objects are the same within a client request but different across client requests.
- Одинарные объекты остаются неизменными для всех объектов и запросов независимо от того, предоставляется ли в
Startup.ConfigureServices
экземплярOperation
.Singleton objects are the same for every object and every request regardless of whether anOperation
instance is provided inStartup.ConfigureServices
.
Вызов служб из функции mainCall services from main
Создайте IServiceScope с IServiceScopeFactory.CreateScope для разрешения службы с заданной областью в области приложения.Create an IServiceScope with IServiceScopeFactory.CreateScope to resolve a scoped service within the app's scope. Этот способ позволит получить доступ к службе с заданной областью при запуске для выполнения задач по инициализации.This approach is useful to access a scoped service at startup to run initialization tasks. В следующем примере показано, как получить контекст для MyScopedService
в Program.Main
:The following example shows how to obtain a context for the MyScopedService
in Program.Main
:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public class Program
{
public static async Task Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var serviceContext = services.GetRequiredService<MyScopedService>();
// Use the context here
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}
await host.RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Проверка областиScope validation
Когда приложение выполняется в среде разработки, поставщик службы по умолчанию проверяет следующее:When the app is running in the Development environment, the default service provider performs checks to verify that:
- Службы с заданной областью не разрешаются из корневого поставщика службы, прямо или косвенно.Scoped services aren't directly or indirectly resolved from the root service provider.
- Службы с заданной областью не вводятся в одноэлементные объекты, прямо или косвенно.Scoped services aren't directly or indirectly injected into singletons.
Корневой поставщик службы создается при вызове BuildServiceProvider.The root service provider is created when BuildServiceProvider is called. Время существования корневого поставщика службы соответствует времени существования приложения или сервера — поставщик запускается с приложением и удаляется, когда приложение завершает работу.The root service provider's lifetime corresponds to the app/server's lifetime when the provider starts with the app and is disposed when the app shuts down.
Службы с заданной областью удаляются создавшим их контейнером.Scoped services are disposed by the container that created them. Если служба с заданной областью создается в корневом контейнере, время существования службы повышается до уровня одноэлементного объекта, поскольку она удаляется только корневым контейнером при завершении работы приложения или сервера.If a scoped service is created in the root container, the service's lifetime is effectively promoted to singleton because it's only disposed by the root container when app/server is shut down. Проверка областей службы перехватывает эти ситуации при вызове BuildServiceProvider
.Validating service scopes catches these situations when BuildServiceProvider
is called.
Дополнительные сведения см. в разделе Веб-узел ASP.NET Core.For more information, see Веб-узел ASP.NET Core.
Службы запросовRequest Services
Службы, доступные в пределах запроса ASP.NET Core из HttpContext
, предоставляются через коллекцию HttpContext.RequestServices.The services available within an ASP.NET Core request from HttpContext
are exposed through the HttpContext.RequestServices collection.
Службы запросов — это все настроенные и запрашиваемые в приложении службы.Request Services represent the services configured and requested as part of the app. Когда объекты указывают зависимости, они удовлетворяются за счет типов из RequestServices
, а не из ApplicationServices
.When the objects specify dependencies, these are satisfied by the types found in RequestServices
, not ApplicationServices
.
Как правило, использовать эти свойства в приложении напрямую не следует.Generally, the app shouldn't use these properties directly. Вместо этого запрашивайте требуемые для классов типы через конструкторы классов и разрешите платформе внедрять зависимости.Instead, request the types that classes require via class constructors and allow the framework inject the dependencies. Этот процесс создает классы, которые легче тестировать.This yields classes that are easier to test.
Примечание
Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не обращаться к коллекции RequestServices
.Prefer requesting dependencies as constructor parameters to accessing the RequestServices
collection.
Проектирование служб для внедрения зависимостейDesign services for dependency injection
Придерживайтесь следующих рекомендаций:Best practices are to:
- Проектируйте службы так, чтобы для получения зависимостей они использовали внедрение зависимостей.Design services to use dependency injection to obtain their dependencies.
- Избегайте статических классов и членов с отслеживанием состояния.Avoid stateful, static classes and members. Вместо этого следует проектировать приложения для использования отдельных служб, что позволяет избежать создания глобального состояния.Design apps to use singleton services instead, which avoid creating global state.
- Избегайте прямого создания экземпляров зависимых классов внутри служб.Avoid direct instantiation of dependent classes within services. Прямое создание экземпляров обязывает использовать в коде определенную реализацию.Direct instantiation couples the code to a particular implementation.
- Сделайте классы приложения небольшими, хорошо организованными и удобными в тестировании.Make app classes small, well-factored, and easily tested.
Если класс имеет слишком много внедренных зависимостей, обычно это указывает на то, что у класса слишком много задач и он не соответствует принципу единственной обязанности.If a class seems to have too many injected dependencies, this is generally a sign that the class has too many responsibilities and is violating the Single Responsibility Principle (SRP). Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новый класс.Attempt to refactor the class by moving some of its responsibilities into a new class. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.Keep in mind that Razor Pages page model classes and MVC controller classes should focus on UI concerns. Бизнес-правила и реализация доступа к данным должны обрабатываться в классах, которые предназначены для этих целей.Business rules and data access implementation details should be kept in classes appropriate to these separate concerns.
Удаление службDisposal of services
Контейнер вызывает Dispose для создаваемых им типов IDisposable.The container calls Dispose for the IDisposable types it creates. Если экземпляр добавлен в контейнер с помощью пользовательского кода, он не будет удален автоматически.If an instance is added to the container by user code, it isn't disposed automatically.
В следующем примере службы создаются контейнером службы и автоматически удаляются:In the following example, the services are created by the service container and disposed automatically:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public interface IService3 {}
public class Service3 : IService3, IDisposable {}
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<Service1>();
services.AddSingleton<Service2>();
services.AddSingleton<IService3>(sp => new Service3());
}
В следующем примере:In the following example:
- Экземпляры службы не создаются контейнером службы.The service instances aren't created by the service container.
- Платформе неизвестно предполагаемое время жизни службы.The intended service lifetimes aren't known by the framework.
- Платформа не удаляет службы автоматически.The framework doesn't dispose of the services automatically.
- Если службы не удаляются явным образом в коде разработчика, они сохраняются до завершения работы приложения.If the services aren't explicitly disposed in developer code, they persist until the app shuts down.
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<Service1>(new Service1());
services.AddSingleton(new Service2());
}
Руководство по применению временных и общих экземпляров IDisposableIDisposable guidance for Transient and shared instances
Временный экземпляр, ограниченное время существованияTransient, limited lifetime
СценарийScenario
Приложению требуется экземпляр IDisposable с ограниченным временем существования для реализации любого из следующих сценариев:The app requires an IDisposable instance with a transient lifetime for either of the following scenarios:
- Экземпляр разрешается в корневой области.The instance is resolved in the root scope.
- Экземпляр должен быть удален до завершения области.The instance should be disposed before the scope ends.
РешениеSolution
Используйте шаблон фабрики для создания экземпляра за пределами родительской области.Use the factory pattern to create an instance outside of the parent scope. В этом случае приложение обычно имеет метод Create
, который непосредственно вызывает конструктор окончательного типа.In this situation, the app would generally have a Create
method that calls the final type's constructor directly. Если окончательный тип имеет другие зависимости, фабрика позволяет:If the final type has other dependencies, the factory can:
- Получить IServiceProvider в своем конструкторе.Receive an IServiceProvider in its constructor.
- Использовать ActivatorUtilities.CreateInstance, чтобы создать экземпляр за пределами контейнера, используя контейнер для его зависимостей.Use ActivatorUtilities.CreateInstance to instantiate the instance outside the container, while using the container for its dependencies.
Общий экземпляр, ограниченное время существованияShared Instance, limited lifetime
СценарийScenario
Приложению требуется общий экземпляр IDisposable в нескольких службах, но для IDisposable требуется ограниченное время существования.The app requires a shared IDisposable instance across multiple services, but the IDisposable should have a limited lifetime.
РешениеSolution
Зарегистрируйте экземпляр с временем существования с заданной областью.Register the instance with a Scoped lifetime. Используйте IServiceScopeFactory.CreateScope для запуска и создания интерфейса IServiceScope.Use IServiceScopeFactory.CreateScope to start and create a new IServiceScope. Используйте IServiceProvider области для получения необходимых служб.Use the scope's IServiceProvider to get required services. Удалить область по истечении времени существования.Dispose the scope when the lifetime should end.
Общие рекомендацииGeneral Guidelines
- Не регистрируйте экземпляры IDisposable с временной областью.Don't register IDisposable instances with a Transient scope. Вместо этого используйте шаблон фабрики.Use the factory pattern instead.
- Не разрешайте временные экземпляры IDisposable или экземпляры с заданной областью в корневую область.Don't resolve Transient or Scoped IDisposable instances in the root scope. Единственное исключение — это когда приложение создает или повторно создает и удаляет IServiceProvider, что не является идеальным вариантом.The only general exception is when the app creates/recreates and disposes the IServiceProvider, which isn't an ideal pattern.
- Для получения зависимости IDisposable через DI не требуется, чтобы получатель реализовывал сам интерфейс IDisposable.Receiving an IDisposable dependency via DI doesn't require that the receiver implement IDisposable itself. Получатель зависимости IDisposable не должен вызывать Dispose для этой зависимости.The receiver of the IDisposable dependency shouldn't call Dispose on that dependency.
- Области должны использоваться для управления жизненным циклом служб.Scopes should be used to control lifetimes of services. Области не являются иерархическими, и между ними нет специальной связи.Scopes aren't hierarchical, and there's no special connection among scopes.
Замена стандартного контейнера службDefault service container replacement
Встроенный контейнер служб предназначен для удовлетворения потребностей платформы и большинства клиентских приложений.The built-in service container is designed to serve the needs of the framework and most consumer apps. Мы рекомендуем использовать встроенный контейнер, если только не требуется конкретная функциональная возможность, которую он не поддерживает, например:We recommend using the built-in container unless you need a specific feature that the built-in container doesn't support, such as:
- Внедрение свойствProperty injection
- Внедрение по имениInjection based on name
- Дочерние контейнерыChild containers
- Настраиваемое управление временем существованияCustom lifetime management
Func<T>
поддерживает отложенную инициализациюFunc<T>
support for lazy initialization- Регистрация на основе соглашенияConvention-based registration
С приложениями ASP.NET Core можно использовать следующие сторонние контейнеры:The following third-party containers can be used with ASP.NET Core apps:
- Autofac;Autofac
- DryIoc;DryIoc
- Grace;Grace
- LightInject;LightInject
- Lamar;Lamar
- Stashbox;Stashbox
- UnityUnity
ПотокобезопасностьThread safety
Создавайте потокобезопасные одноэлементные службы.Create thread-safe singleton services. Если одноэлементная служба имеет зависимость от временной службы, с учетом характера использования одноэлементной службой к этой временной службе также может предъявляться требование потокобезопасности.If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending how it's used by the singleton.
Фабричный метод одной службы, например второй аргумент для AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), не обязательно должен быть потокобезопасным.The factory method of single service, such as the second argument to AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need to be thread-safe. Как и конструктор типов (static
), он гарантированно будет вызываться один раз одним потоком.Like a type (static
) constructor, it's guaranteed to be called once by a single thread.
РекомендацииRecommendations
Разрешение служб на основе
async/await
иTask
не поддерживается.async/await
andTask
based service resolution is not supported. C# не поддерживает асинхронные конструкторы, поэтому рекомендуем использовать асинхронные методы после асинхронного разрешения службы.C# does not support asynchronous constructors; therefore, the recommended pattern is to use asynchronous methods after synchronously resolving the service.Не храните данные и конфигурацию непосредственно в контейнере служб.Avoid storing data and configuration directly in the service container. Например, обычно не следует добавлять корзину пользователя в контейнер служб.For example, a user's shopping cart shouldn't typically be added to the service container. Конфигурация должна использовать шаблон параметров.Configuration should use the options pattern. Аналогичным образом, избегайте объектов "хранения данных", которые служат лишь для доступа к некоторому другому объекту.Similarly, avoid "data holder" objects that only exist to allow access to some other object. Лучше запросить фактический элемент через внедрение зависимостей.It's better to request the actual item via DI.
Избегайте статического доступа к службам.Avoid static access to services. Например, не используйте везде IApplicationBuilder.ApplicationServices.For example, avoid statically-typing IApplicationBuilder.ApplicationServices for use elsewhere.
Старайтесь не использовать шаблон обнаружения служб, который использует разные стратегии инверсии управления.Avoid using the service locator pattern, which mixes Inversion of Control strategies.
Не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей:Don't invoke GetService to obtain a service instance when you can use DI instead:
Неправильно:Incorrect:
public class MyClass() public void MyMethod() { var optionsMonitor = _services.GetService<IOptionsMonitor<MyOptions>>(); var option = optionsMonitor.CurrentValue.Option; ... }
Правильно:Correct:
public class MyClass { private readonly IOptionsMonitor<MyOptions> _optionsMonitor; public MyClass(IOptionsMonitor<MyOptions> optionsMonitor) { _optionsMonitor = optionsMonitor; } public void MyMethod() { var option = _optionsMonitor.CurrentValue.Option; ... } }
Избегайте внедрения фабрики, которая разрешает зависимости во время выполнения с помощью GetService.Avoid injecting a factory that resolves dependencies at runtime using GetService.
Не используйте статический доступ к
HttpContext
(например, IHttpContextAccessor.HttpContext).Avoid static access toHttpContext
(for example, IHttpContextAccessor.HttpContext).
Как и с любыми рекомендациями, у вас могут возникнуть ситуации, когда нужно отступить от одного из правил.Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. Исключения возникают редко, — как правило, это особые случаи, связанные с самой платформой.Exceptions are rare, mostly special cases within the framework itself.
Внедрение зависимостей является альтернативой для шаблонов доступа к статическим или глобальным объектам.DI is an alternative to static/global object access patterns. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если будете сочетать его с доступом к статическим объектам.You may not be able to realize the benefits of DI if you mix it with static object access.
Дополнительные ресурсыAdditional resources
- Внедрение зависимостей в представления в ASP.NET Core
- Внедрение зависимостей в контроллеры в ASP.NET Core
- Внедрение зависимостей в обработчики требований в ASP.NET Core
- Внедрение зависимостей Blazor в ASP.NET Core
- Запуск приложения в ASP.NET Core
- Активация ПО промежуточного слоя на основе фабрики в ASP.NET Core
- Четыре способа удаления интерфейсов IDisposable в ASP.NET CoreFour ways to dispose IDisposables in ASP.NET Core
- Написание чистого кода в ASP.NET Core с внедрением зависимостей (MSDN)Writing Clean Code in ASP.NET Core with Dependency Injection (MSDN)
- Принцип явных зависимостейExplicit Dependencies Principle
- Контейнеры с инверсией управления и шаблон внедрения зависимостей (Мартин Фаулер)Inversion of Control Containers and the Dependency Injection Pattern (Martin Fowler)
- How to register a service with multiple interfaces in ASP.NET Core DI (Регистрация службы с несколькими интерфейсами с помощью внедрения зависимостей ASP.NET Core)How to register a service with multiple interfaces in ASP.NET Core DI