Внедрение зависимостей в ASP.NET CoreDependency injection in ASP.NET Core

Авторы: Стив Смит (Steve Smith), Скотт Эдди (Scott Addie) и Люк Латам (Luke Latham)By Steve Smith, Scott Addie, and Luke Latham

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 replace MyDependency with a different implementation, the class must be modified.
  • Если у MyDependency есть зависимости, их конфигурацию должен выполнять класс.If MyDependency has dependencies, they must be configured by the class. В больших проектах, когда от MyDependency зависят многие классы, код конфигурации растягивается по всему приложению.In a large project with multiple classes depending on MyDependency, the configuration code becomes scattered across the app.
  • Такая реализация плохо подходит для модульных тестов.This implementation is difficult to unit test. В приложении нужно использовать имитацию или заглушку в виде класса MyDependency, что при таком подходе невозможно.The app should use a mock or stub MyDependency class, which isn't possible with this approach.

Внедрение зависимостей устраняет эти проблемы следующим образом:Dependency injection addresses these problems through:

  • Используется интерфейс для абстрагирования реализации зависимостей.The use of an interface 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's Startup.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<T>), typeof(Logger<T>));

В примере приложения служба 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.AddMvc() добавляет службы, которые нужны для Razor Pages и MVC.For example, services.AddMvc() adds the services Razor Pages and MVC 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.

Если конструктору службы требуется встроенный тип, например 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.");
    }
}

Платформенные службыFramework-provided services

Метод Startup.ConfigureServices отвечает за определение служб, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC.The Startup.ConfigureServices method is responsible for defining the services 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 the following services defined (depending on how the host was configured):

Тип службыService Type Время существованияLifetime
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactoryMicrosoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory ВременныйTransient
Microsoft.AspNetCore.Hosting.IApplicationLifetimeMicrosoft.AspNetCore.Hosting.IApplicationLifetime ОдноэлементныйSingleton
Microsoft.AspNetCore.Hosting.IHostingEnvironmentMicrosoft.AspNetCore.Hosting.IHostingEnvironment ОдноэлементныйSingleton
Microsoft.AspNetCore.Hosting.IStartupMicrosoft.AspNetCore.Hosting.IStartup ОдноэлементныйSingleton
Microsoft.AspNetCore.Hosting.IStartupFilterMicrosoft.AspNetCore.Hosting.IStartupFilter ВременныйTransient
Microsoft.AspNetCore.Hosting.Server.IServerMicrosoft.AspNetCore.Hosting.Server.IServer ОдноэлементныйSingleton
Microsoft.AspNetCore.Http.IHttpContextFactoryMicrosoft.AspNetCore.Http.IHttpContextFactory ВременныйTransient
Microsoft.Extensions.Logging.ILogger<T>Microsoft.Extensions.Logging.ILogger<T> ОдноэлементныйSingleton
Microsoft.Extensions.Logging.ILoggerFactoryMicrosoft.Extensions.Logging.ILoggerFactory ОдноэлементныйSingleton
Microsoft.Extensions.ObjectPool.ObjectPoolProviderMicrosoft.Extensions.ObjectPool.ObjectPoolProvider ОдноэлементныйSingleton
Microsoft.Extensions.Options.IConfigureOptions<T>Microsoft.Extensions.Options.IConfigureOptions<T> ВременныйTransient
Microsoft.Extensions.Options.IOptions<T>Microsoft.Extensions.Options.IOptions<T> ОдноэлементныйSingleton
System.Diagnostics.DiagnosticSourceSystem.Diagnostics.DiagnosticSource ОдноэлементныйSingleton
System.Diagnostics.DiagnosticListenerSystem.Diagnostics.DiagnosticListener ОдноэлементныйSingleton

Если зарегистрировать службу (включая ее зависимые службы, если нужно) можно, используя метод расширения коллекции служб, для регистрации всех служб, необходимых этой службе, рекомендуется использовать один метод расширения 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, AddIdentity и AddMvc.The following code is an example of how to add additional services to the container using the extension methods AddDbContext, AddIdentity, and AddMvc:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();
}

Дополнительные сведения см. в документации по API в статье о классе ServiceCollection.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

Временные службы времени существования создаются при каждом их запросе из контейнера служб.Transient lifetime services are created each time they're requested from the service container. Это время существования лучше всего подходит для простых служб без отслеживания состояния.This lifetime works best for lightweight, stateless services.

Ограниченный областьюScoped

Службы времени существования с заданной областью создаются один раз для каждого клиентского запроса (подключения).Scoped lifetime services are created once per client request (connection).

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

При использовании такой службы в ПО промежуточного слоя внедрите ее в метод 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

Одинарные службы создаются при первом запросе (или при выполнении ConfigureServices, когда экземпляр указан во время регистрации службы) и существуют в единственном экземпляре.Singleton lifetime services are created the first time they're requested (or when 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.

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

Разрешать регулируемую службу из одиночной нельзя.It's dangerous to resolve a scoped service from a singleton. При обработке последующих запросов это может вызвать неправильное состояние службы.It may cause the service to have incorrect state when processing subsequent requests.

Поведение внедрения через конструктор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, the OperationId of the IOperationTransient service is different than the OperationId of the OperationService. OperationService получает новый экземпляр класса IOperationTransient.OperationService receives a new instance of the IOperationTransient class. Новый экземпляр возвращает другой идентификатор OperationId.The new instance yields a different OperationId.
  • Если при каждом клиентском запросе создаются регулируемые службы, идентификатор OperationId службы IOperationScoped будет таким же, как для службы OperationService в клиентском запросе.When scoped services are created per client request, the OperationId of the IOperationScoped service is the same as that of OperationService within a client request. В разных клиентских запросах обе службы используют разные значения OperationId.Across client requests, both services share a different OperationId value.
  • Если одинарные службы и службы с одинарным экземпляром создаются один раз и используются во всех клиентских запросах и службах, идентификатор OperationId будет одинаковым во всех запросах служб.When singleton and singleton-instance services are created once and used across all client requests and all services, the OperationId 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 transient OperationId value for both the first and second client requests are different for both OperationService 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.
  • Одинарные объекты остаются неизменными для всех объектов и запросов независимо от того, предоставляется ли в ConfigureServices экземпляр Operation.Singleton objects are the same for every object and every request regardless of whether an Operation instance is provided in 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:

public static void 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.");
        }
    }

    host.Run();
}

Проверка области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 method calls.
  • Избегайте прямого создания экземпляров зависимых классов внутри служб.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.

// Services that implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public interface ISomeService {}
public class SomeServiceImplementation : ISomeService, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    // The container creates the following instances and disposes them automatically:
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation());

    // The container doesn't create the following instances, so it doesn't dispose of
    // the instances automatically:
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

Замена стандартного контейнера службDefault service container replacement

Встроенный контейнер служб предназначен для удовлетворения базовых потребностей платформы и большинства клиентских приложений.The built-in service container is meant to serve the needs of the framework and most consumer apps. Мы рекомендуем использовать встроенный контейнер, если только не требуется конкретная функциональная возможность, которую он не поддерживает.We recommend using the built-in container unless you need a specific feature that it doesn't support. Некоторые функции, поддерживаемые сторонними контейнерами, отсутствуют во встроенном контейнере:Some of the features supported in 3rd party containers not found in the built-in container:

  • Внедрение свойствProperty injection
  • Внедрение по имениInjection based on name
  • Дочерние контейнерыChild containers
  • Настраиваемое управление временем существованияCustom lifetime management
  • Func<T> поддерживает отложенную инициализациюFunc<T> support for lazy initialization

Список некоторых контейнеров, которые поддерживают адаптеры, см. в разделе Файл readme.md внедрения зависимостей.See the Dependency Injection readme.md file for a list of some of the containers that support adapters.

В следующем примере встроенный контейнер заменяется на Autofac:The following sample replaces the built-in container with Autofac:

  • Установите соответствующие пакеты контейнера:Install the appropriate container package(s):

  • Настройте контейнер в Startup.ConfigureServices и возвратите IServiceProvider.Configure the container in Startup.ConfigureServices and return an IServiceProvider:

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        // Add other framework services
    
        // Add Autofac
        var containerBuilder = new ContainerBuilder();
        containerBuilder.RegisterModule<DefaultModule>();
        containerBuilder.Populate(services);
        var container = containerBuilder.Build();
        return new AutofacServiceProvider(container);
    }
    

    Чтобы использовать сторонний контейнер, метод Startup.ConfigureServices должен возвращать IServiceProvider.To use a 3rd party container, Startup.ConfigureServices must return IServiceProvider.

  • Настройте Autofac в DefaultModule.Configure Autofac in DefaultModule:

    public class DefaultModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
        }
    }
    

В среде выполнения Autofac используется для разрешения типов и внедрения зависимостей.At runtime, Autofac is used to resolve types and inject dependencies. Дополнительные сведения об использовании Autofac с ASP.NET Core см. в документации по Autofac.To learn more about using Autofac with ASP.NET Core, see the Autofac documentation.

Потокобезопасность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 and Task 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.

  • Не используйте статический доступ к службам (например, не используйте везде IApplicationBuilder.ApplicationServices).Avoid static access to services (for example, statically-typing IApplicationBuilder.ApplicationServices for use elsewhere).

  • Старайтесь не использовать схему указателя служб.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:

    public void MyMethod()
    {
        var options = 
            _services.GetService<IOptionsMonitor<MyOptions>>();
        var option = options.CurrentValue.Option;
    
        ...
    }
    

    Правильно:Correct:

    private readonly MyOptions _options;
    
    public MyClass(IOptionsMonitor<MyOptions> options)
    {
        _options = options.CurrentValue;
    }
    
    public void MyMethod()
    {
        var option = _options.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 to HttpContext (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