Использование внедрения зависимостей в ASP.NET Core

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith) и Брэндон Далер (Brandon Dahler)

ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.

Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.

Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.

Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.

В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.

Просмотреть или скачать образец кода (описание загрузки)

Общие сведения о внедрении зависимостей

Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency с методом WriteMessage, от которого зависят другие классы:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Класс может создать экземпляр класса MyDependency, чтобы использовать его метод WriteMessage. В следующем примере класс MyDependency выступает зависимостью класса IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

Этот класс создает MyDependency и напрямую зависит от этого класса. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.

  • Чтобы заменить MyDependency другой реализацией, класс IndexModel необходимо изменить.
  • Если у MyDependency есть зависимости, их конфигурацию должен выполнять класс IndexModel. В больших проектах, когда от MyDependency зависят многие классы, код конфигурации растягивается по всему приложению.
  • Такая реализация плохо подходит для модульных тестов.

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

  • Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
  • Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Службы обычно регистрируются в файле приложения Program.cs .
  • Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.

В примере приложения интерфейс IMyDependency определяет метод WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Этот интерфейс реализуется конкретным типом, MyDependency.

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

Пример приложения регистрирует службу IMyDependency с конкретным типом MyDependency. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

В примере приложения запрашивается служба IMyDependency, которая затем используется для вызова метода WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Используя шаблон внедрения зависимостей, контроллер или страницу Razor:

  • не использует конкретный тип MyDependency, только интерфейс IMyDependency, который он реализует. Это упрощает изменение реализации без изменения контроллера или страницы Razor.
  • не создает экземпляр MyDependency, он создается контейнером внедрения зависимостей.

Реализацию интерфейса IMyDependency можно улучшить с помощью встроенного API ведения журнала:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

Обновленный метод Program.cs регистрирует новую реализацию IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 зависит от ILogger<TCategoryName>, который запрашивается в конструкторе. ILogger<TCategoryName> — это предоставленная платформой служба.

Использование цепочки внедрений зависимостей не является чем-то необычным. Каждая запрашиваемая зависимость запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.

Контейнер разрешает ILogger<TCategoryName>, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.

В терминологии внедрения зависимостей — служба:

  • Обычно является объектом, предоставляющим службу для других объектов, например службу IMyDependency.
  • Не относится к веб-службе, хотя служба может использовать веб-службу.

Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency, приведенные в предыдущем примере были написаны для демонстрации базового внедрения зависимостей, а не для реализации ведения журнала. Большинству приложений не нужно писать средства ведения журнала. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Используя приведенный выше код, не нужно обновлять Program.cs, поскольку платформа предоставляет возможность ведения журнала.

Регистрация групп служб с помощью методов расширения

Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME} для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.

Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Рассмотрим следующий код, который регистрирует службы и настраивает параметры:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Связанные группы регистраций можно переместить в метод расширения для регистрации служб. Например, службы конфигурации добавляются в следующий класс:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Остальные службы регистрируются в аналогичном классе. Следующий код использует новые методы расширения для регистрации служб:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Примечание. Каждый services.Add{GROUP_NAME} метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет контроллеры MVC служб с необходимыми представлениями, а AddRazorPages — службы, требуемые для работы Razor Pages.

Время существования служб

См. раздел Время существования службы в статье Внедрение зависимостей в .NET.

Используйте службы с заданной областью в ПО промежуточного слоя, применяя один из следующих подходов:

  • Внедрите службу в метод Invoke или InvokeAsync ПО промежуточного слоя. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подход InvokeAsync.
  • Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в конструктор ПО промежуточного слоя.

Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.

Методы регистрации службы

См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.

Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.

Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.

Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton вызывается дважды с типом службы IMyDependency. Второй вызов AddSingleton переопределяет предыдущий, если он разрешается как IMyDependency, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Ключи служб

Ключи служб относятся к механизму регистрации и получения служб внедрения зависимостей (DI) с помощью ключей. Служба связана с ключом путем вызова AddKeyedSingleton (или AddKeyedScopedAddKeyedTransient) для регистрации. Доступ к зарегистрированной службе путем указания ключа с атрибутом [FromKeyedServices] . В следующем коде показано, как использовать ключи служб:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
                                                               smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
    [HttpGet("big-cache")]
    public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
    {
        return cache.Get("data-mvc");
    }
}

public class MyHub : Hub
{
    public void Method([FromKeyedServices("small")] ICache cache)
    {
        Console.WriteLine(cache.Get("signalr"));
    }
}

Поведение внедрения через конструктор

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Контексты Entity Framework

По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы данного времени существования не должны использовать контекст базы данных с временем существования короче, чем у службы.

Параметры времени существования и регистрации

Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

Следующий класс Operation реализует все предыдущие интерфейсы. Конструктор Operation создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

Следующий код создает несколько регистраций класса Operation в соответствии с именованным временем существования:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

В примере приложения показано время существования объектов в пределах запросов и между запросами. IndexModel и ПО промежуточного слоя запрашивают каждый тип IOperation и регистрируют OperationId для каждого из них:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Аналогично IndexModel, ПО промежуточного слоя и разрешает те же службы:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Службы с заданной областью и временные службы должны быть разрешены в методе InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

Выходные данные средства ведения журнала содержат:

  • Временные объекты всегда разные. Значение временного OperationId отличается в IndexModel и ПО промежуточного слоя.
  • Объекты с заданной областью остаются неизменными в пределах указанного запроса, но в новых запросах используются разные объекты.
  • Одноэлементные объекты одинаковы для каждого запроса.

Чтобы уменьшить выходные данные ведения журнала, задайте в appsettings.Development.json файле параметр LogLevel:Microsoft:Error:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Разрешение службы при запуске приложения

В следующем коде показано, как разрешить службу с областью действия в течение ограниченного времени при запуске приложения:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Проверка области

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Дополнительные сведения см. в разделе Проверка области.

Службы запросов

Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.

Платформа создает область для каждого запроса, а RequestServices предоставляет поставщик услуг с заданной областью. Все службы с заданной областью действительны до тех пор, пока запрос активен.

Примечание.

Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не разрешать службы из RequestServices. Таким образом вы получите классы, которые проще тестировать.

Проектирование служб для внедрения зависимостей

При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:

  • Избегайте статических классов и членов с отслеживанием состояния. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
  • Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров обязывает использовать в коде определенную реализацию.
  • Сделайте службы приложения небольшими, хорошо организованными и удобными в тестировании.

Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.

Удаление служб

Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.

В следующем примере службы создаются контейнером службы и удаляются автоматически: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Службы, не созданные контейнером службы

Рассмотрим следующий код:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

В предыдущем коде:

  • Экземпляры службы не создаются контейнером службы.
  • Платформа не удаляет службы автоматически.
  • За удаление служб отвечает разработчик.

Руководство по применению временных и общих экземпляров IDisposable

См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.

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

См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.

Рекомендации

См. раздел Рекомендации в статье Внедрение зависимостей в .NET.

  • Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей:

    Неправильно:

    Incorrect code

    Правильное.

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Другой вариант указателя службы, позволяющий избежать этого, — внедрение фабрики, которая разрешает зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.

  • Не используйте статический доступ к HttpContext (например, IHttpContextAccessor.HttpContext).

Внедрение зависимостей является альтернативой для шаблонов доступа к статическим или глобальным объектам. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если будете сочетать его с доступом к статическим объектам.

Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.

Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.

Платформенные службы

Program.cs регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection, предоставленная для Program.cs, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.

В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.

Тип службы Время существования
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Временный
IHostApplicationLifetime Отдельная
IWebHostEnvironment Отдельная
Microsoft.AspNetCore.Hosting.IStartup Отдельная
Microsoft.AspNetCore.Hosting.IStartupFilter Временный
Microsoft.AspNetCore.Hosting.Server.IServer Отдельная
Microsoft.AspNetCore.Http.IHttpContextFactory Временный
Microsoft.Extensions.Logging.ILogger<TCategoryName> Отдельная
Microsoft.Extensions.Logging.ILoggerFactory Отдельная
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Отдельная
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Временный
Microsoft.Extensions.Options.IOptions<TOptions> Отдельная
System.Diagnostics.DiagnosticSource Отдельная
System.Diagnostics.DiagnosticListener Отдельная

Дополнительные ресурсы

Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith) и Брэндон Далер (Brandon Dahler)

ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.

Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.

Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.

Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.

В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.

Просмотреть или скачать образец кода (описание загрузки)

Общие сведения о внедрении зависимостей

Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency с методом WriteMessage, от которого зависят другие классы:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Класс может создать экземпляр класса MyDependency, чтобы использовать его метод WriteMessage. В следующем примере класс MyDependency выступает зависимостью класса IndexModel:


public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet");
    }
}

Этот класс создает MyDependency и напрямую зависит от этого класса. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.

  • Чтобы заменить MyDependency другой реализацией, класс IndexModel необходимо изменить.
  • Если у MyDependency есть зависимости, их конфигурацию должен выполнять класс IndexModel. В больших проектах, когда от MyDependency зависят многие классы, код конфигурации растягивается по всему приложению.
  • Такая реализация плохо подходит для модульных тестов.

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

  • Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
  • Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Службы обычно регистрируются в файле приложения Program.cs .
  • Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.

В примере приложения интерфейс IMyDependency определяет метод WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Этот интерфейс реализуется конкретным типом, MyDependency.

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

Пример приложения регистрирует службу IMyDependency с конкретным типом MyDependency. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

В примере приложения запрашивается служба IMyDependency, которая затем используется для вызова метода WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Используя шаблон внедрения зависимостей, контроллер или страницу Razor:

  • не использует конкретный тип MyDependency, только интерфейс IMyDependency, который он реализует. Это упрощает изменение реализации без изменения контроллера или страницы Razor.
  • не создает экземпляр MyDependency, он создается контейнером внедрения зависимостей.

Реализацию интерфейса IMyDependency можно улучшить с помощью встроенного API ведения журнала:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

Обновленный метод Program.cs регистрирует новую реализацию IMyDependency:

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency2>();

var app = builder.Build();

MyDependency2 зависит от ILogger<TCategoryName>, который запрашивается в конструкторе. ILogger<TCategoryName> — это предоставленная платформой служба.

Использование цепочки внедрений зависимостей не является чем-то необычным. Каждая запрашиваемая зависимость запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.

Контейнер разрешает ILogger<TCategoryName>, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.

В терминологии внедрения зависимостей — служба:

  • Обычно является объектом, предоставляющим службу для других объектов, например службу IMyDependency.
  • Не относится к веб-службе, хотя служба может использовать веб-службу.

Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency, приведенные в предыдущем примере были написаны для демонстрации базового внедрения зависимостей, а не для реализации ведения журнала. Большинству приложений не нужно писать средства ведения журнала. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; } = string.Empty;

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Используя приведенный выше код, не нужно обновлять Program.cs, поскольку платформа предоставляет возможность ведения журнала.

Регистрация групп служб с помощью методов расширения

Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME} для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.

Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:

using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

Рассмотрим следующий код, который регистрирует службы и настраивает параметры:

using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
    builder.Configuration.GetSection(ColorOptions.Color));

builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();

var app = builder.Build();

Связанные группы регистраций можно переместить в метод расширения для регистрации служб. Например, службы конфигурации добавляются в следующий класс:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Остальные службы регистрируются в аналогичном классе. Следующий код использует новые методы расширения для регистрации служб:

using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddConfig(builder.Configuration)
    .AddMyDependencyGroup();

builder.Services.AddRazorPages();

var app = builder.Build();

Примечание. Каждый services.Add{GROUP_NAME} метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет контроллеры MVC служб с необходимыми представлениями, а AddRazorPages — службы, требуемые для работы Razor Pages.

Время существования служб

См. раздел Время существования службы в статье Внедрение зависимостей в .NET.

Используйте службы с заданной областью в ПО промежуточного слоя, применяя один из следующих подходов:

  • Внедрите службу в метод Invoke или InvokeAsync ПО промежуточного слоя. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подход InvokeAsync.
  • Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в конструктор ПО промежуточного слоя.

Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.

Методы регистрации службы

См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.

Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.

Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.

Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton вызывается дважды с типом службы IMyDependency. Второй вызов AddSingleton переопределяет предыдущий, если он разрешается как IMyDependency, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Поведение внедрения через конструктор

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Контексты Entity Framework

По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы данного времени существования не должны использовать контекст базы данных с временем существования короче, чем у службы.

Параметры времени существования и регистрации

Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

Следующий класс Operation реализует все предыдущие интерфейсы. Конструктор Operation создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

Следующий код создает несколько регистраций класса Operation в соответствии с именованным временем существования:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseMyMiddleware();
app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

В примере приложения показано время существования объектов в пределах запросов и между запросами. IndexModel и ПО промежуточного слоя запрашивают каждый тип IOperation и регистрируют OperationId для каждого из них:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Аналогично IndexModel, ПО промежуточного слоя и разрешает те же службы:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationTransient transientOperation, IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + transientOperation.OperationId);
        _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Службы с заданной областью и временные службы должны быть разрешены в методе InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + transientOperation.OperationId);
    _logger.LogInformation("Scoped: " + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

Выходные данные средства ведения журнала содержат:

  • Временные объекты всегда разные. Значение временного OperationId отличается в IndexModel и ПО промежуточного слоя.
  • Объекты с заданной областью остаются неизменными в пределах указанного запроса, но в новых запросах используются разные объекты.
  • Одноэлементные объекты одинаковы для каждого запроса.

Чтобы уменьшить выходные данные ведения журнала, задайте в appsettings.Development.json файле параметр LogLevel:Microsoft:Error:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Разрешение службы при запуске приложения

В следующем коде показано, как разрешить службу с областью действия в течение ограниченного времени при запуске приложения:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

Проверка области

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Дополнительные сведения см. в разделе Проверка области.

Службы запросов

Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.

Платформа создает область для каждого запроса, а RequestServices предоставляет поставщик услуг с заданной областью. Все службы с заданной областью действительны до тех пор, пока запрос активен.

Примечание.

Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не разрешать службы из RequestServices. Таким образом вы получите классы, которые проще тестировать.

Проектирование служб для внедрения зависимостей

При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:

  • Избегайте статических классов и членов с отслеживанием состояния. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
  • Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров обязывает использовать в коде определенную реализацию.
  • Сделайте службы приложения небольшими, хорошо организованными и удобными в тестировании.

Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.

Удаление служб

Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.

В следующем примере службы создаются контейнером службы и удаляются автоматически: dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
using DIsample2.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();

var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));

var app = builder.Build();
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose

Службы, не созданные контейнером службы

Рассмотрим следующий код:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());

В предыдущем коде:

  • Экземпляры службы не создаются контейнером службы.
  • Платформа не удаляет службы автоматически.
  • За удаление служб отвечает разработчик.

Руководство по применению временных и общих экземпляров IDisposable

См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.

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

См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.

Рекомендации

См. раздел Рекомендации в статье Внедрение зависимостей в .NET.

  • Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей:

    Неправильно:

    Incorrect code

    Правильное.

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Другой вариант указателя службы, позволяющий избежать этого, — внедрение фабрики, которая разрешает зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.

  • Не используйте статический доступ к HttpContext (например, IHttpContextAccessor.HttpContext).

Внедрение зависимостей является альтернативой для шаблонов доступа к статическим или глобальным объектам. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если будете сочетать его с доступом к статическим объектам.

Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.

Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.

Платформенные службы

Program.cs регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection, предоставленная для Program.cs, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.

В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.

Тип службы Время существования
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Временный
IHostApplicationLifetime Отдельная
IWebHostEnvironment Отдельная
Microsoft.AspNetCore.Hosting.IStartup Отдельная
Microsoft.AspNetCore.Hosting.IStartupFilter Временный
Microsoft.AspNetCore.Hosting.Server.IServer Отдельная
Microsoft.AspNetCore.Http.IHttpContextFactory Временный
Microsoft.Extensions.Logging.ILogger<TCategoryName> Отдельная
Microsoft.Extensions.Logging.ILoggerFactory Отдельная
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Отдельная
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Временный
Microsoft.Extensions.Options.IOptions<TOptions> Отдельная
System.Diagnostics.DiagnosticSource Отдельная
System.Diagnostics.DiagnosticListener Отдельная

Дополнительные ресурсы

Авторы: Кирк Ларкин (Kirk Larkin), Стив Смит (Steve Smith), Скотт Эдди (Scott Addie) и Брэндон Далер (Brandon Dahler)

ASP.NET Core поддерживает проектирование программного обеспечения с возможностью внедрения зависимостей. При таком подходе достигается инверсия управления между классами и их зависимостями.

Дополнительные сведения о внедрении зависимостей в контроллерах MVC см. в статье Внедрение зависимостей в контроллеры в ASP.NET Core.

Дополнительные сведения об использовании внедрения зависимостей в приложениях (кроме веб-приложений) см. в статье Внедрение зависимостей в .NET.

Дополнительные сведения о внедрении параметров зависимостей см. в разделе Шаблон параметров в ASP.NET Core.

В этой статье приводятся сведения о внедрении зависимостей в ASP.NET Core. Основная документация по использованию внедрения зависимостей указана в статье Внедрение зависимостей в .NET.

Просмотреть или скачать образец кода (описание загрузки)

Общие сведения о внедрении зависимостей

Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MyDependency с методом WriteMessage, от которого зависят другие классы:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

Класс может создать экземпляр класса MyDependency, чтобы использовать его метод WriteMessage. В следующем примере класс MyDependency выступает зависимостью класса IndexModel:

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

Этот класс создает MyDependency и напрямую зависит от этого класса. Зависимости в коде, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам.

  • Чтобы заменить MyDependency другой реализацией, класс IndexModel необходимо изменить.
  • Если у MyDependency есть зависимости, их конфигурацию должен выполнять класс IndexModel. В больших проектах, когда от MyDependency зависят многие классы, код конфигурации растягивается по всему приложению.
  • Такая реализация плохо подходит для модульных тестов. В приложении нужно использовать имитацию или заглушку в виде класса MyDependency, что при таком подходе невозможно.

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

  • Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
  • Зависимость регистрируется в контейнере служб. ASP.NET Core предоставляет встроенный контейнер служб, IServiceProvider. Как правило, службы регистрируются в приложении в методе Startup.ConfigureServices.
  • Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.

В примере приложения интерфейс IMyDependency определяет метод WriteMessage:

public interface IMyDependency
{
    void WriteMessage(string message);
}

Этот интерфейс реализуется конкретным типом, MyDependency.

public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}

Пример приложения регистрирует службу IMyDependency с конкретным типом MyDependency. Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

В примере приложения запрашивается служба IMyDependency, которая затем используется для вызова метода WriteMessage:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

Используя шаблон внедрения зависимостей, контроллер:

  • не использует конкретный тип MyDependency, только интерфейс IMyDependency, который он реализует. Это упрощает изменение реализации, используемой контроллером, без изменения контроллера.
  • не создает экземпляр MyDependency, он создается контейнером внедрения зависимостей.

Реализацию интерфейса IMyDependency можно улучшить с помощью встроенного API ведения журнала:

public class MyDependency2 : IMyDependency
{
    private readonly ILogger<MyDependency2> _logger;

    public MyDependency2(ILogger<MyDependency2> logger)
    {
        _logger = logger;
    }

    public void WriteMessage(string message)
    {
        _logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
    }
}

Обновленный метод ConfigureServices регистрирует новую реализацию IMyDependency:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 зависит от ILogger<TCategoryName>, который запрашивается в конструкторе. ILogger<TCategoryName> — это предоставленная платформой служба.

Использование цепочки внедрений зависимостей не является чем-то необычным. Каждая запрашиваемая зависимость запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.

Контейнер разрешает ILogger<TCategoryName>, используя преимущества (универсальных) открытых типов, что устраняет необходимость регистрации каждого (универсального) сконструированного типа.

В терминологии внедрения зависимостей — служба:

  • Обычно является объектом, предоставляющим службу для других объектов, например службу IMyDependency.
  • Не относится к веб-службе, хотя служба может использовать веб-службу.

Платформа предоставляет эффективную систему ведения журнала. Реализации IMyDependency, приведенные в предыдущем примере были написаны для демонстрации базового внедрения зависимостей, а не для реализации ведения журнала. Большинству приложений не нужно писать средства ведения журнала. В следующем коде показано использование журнала по умолчанию, для которого не требуется регистрация служб в ConfigureServices:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

Используя приведенный выше код, не нужно обновлять ConfigureServices, поскольку платформа предоставляет возможность ведения журнала.

Службы, внедренные в конструктор Startup

Службы можно внедрить в конструктор Startup и метод Startup.Configure.

При использовании универсального узла (IHostBuilder) в конструктор Startup могут внедряться только следующие службы:

Любая служба, зарегистрированная в контейнере внедрения зависимостей, может быть внедрена в метод Startup.Configure:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

Дополнительные сведения см. в статьях Запуск приложения в ASP.NET Core и Доступ к конфигурации во время запуска.

Регистрация групп служб с помощью методов расширения

Для регистрации группы связанных служб на платформе ASP.NET Core используется соглашение. Соглашение заключается в использовании одного метода расширения Add{GROUP_NAME} для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddControllers регистрирует службы, необходимые контроллерам MVC.

Следующий код создается шаблоном Razor Pages с использованием отдельных учетных записей пользователей. Он демонстрирует, как добавить дополнительные службы в контейнер с помощью методов расширения AddDbContext и AddDefaultIdentity:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Рассмотрим следующий метод ConfigureServices, который регистрирует службы и настраивает параметры:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

Связанные группы регистраций можно переместить в метод расширения для регистрации служб. Например, службы конфигурации добавляются в следующий класс:

using ConfigSample.Options;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Extensions.DependencyInjection
{
    public static class MyConfigServiceCollectionExtensions
    {
        public static IServiceCollection AddConfig(
             this IServiceCollection services, IConfiguration config)
        {
            services.Configure<PositionOptions>(
                config.GetSection(PositionOptions.Position));
            services.Configure<ColorOptions>(
                config.GetSection(ColorOptions.Color));

            return services;
        }

        public static IServiceCollection AddMyDependencyGroup(
             this IServiceCollection services)
        {
            services.AddScoped<IMyDependency, MyDependency>();
            services.AddScoped<IMyDependency2, MyDependency2>();

            return services;
        }
    }
}

Остальные службы регистрируются в аналогичном классе. Следующий метод ConfigureServices использует новые методы расширения для регистрации служб:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

Примечание. Каждый services.Add{GROUP_NAME} метод расширения добавляет и потенциально настраивает службы. Например, AddControllersWithViews добавляет контроллеры MVC служб с необходимыми представлениями, а AddRazorPages — службы, требуемые для работы Razor Pages. Рекомендуется соблюдать в приложениях соглашение об именовании создаваемых методов расширения в пространстве имен Microsoft.Extensions.DependencyInjection. Создание методов расширения в пространстве имен Microsoft.Extensions.DependencyInjection:

  • Инкапсулирует группы регистраций служб.
  • Предоставляет удобный доступ к службе с помощью IntelliSense.

Время существования служб

См. раздел Время существования службы в статье Внедрение зависимостей в .NET.

Используйте службы с заданной областью в ПО промежуточного слоя, применяя один из следующих подходов:

  • Внедрите службу в метод Invoke или InvokeAsync ПО промежуточного слоя. С помощью внедрите конструктор создается исключение времени выполнения, поскольку оно заставляет службу с заданной областью вести себя как одноэлементный объект. В примере в разделе Параметры времени существования и регистрации демонстрируется подход InvokeAsync.
  • Используйте фабричное ПО промежуточного слоя. ПО промежуточного слоя, зарегистрированное с использованием этого подхода, активируется при каждом клиентском запросе (подключении), что позволяет внедрять службы с заданной областью в метод InvokeAsync ПО промежуточного слоя.

Дополнительные сведения см. в разделе Создание пользовательского ПО промежуточного слоя ASP.NET Core.

Методы регистрации службы

См. раздел Методы регистрации службы в статье Внедрение зависимостей в .NET.

Распространенный сценарий для использования нескольких реализаций — макетирование типов для тестирования.

Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.

Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton вызывается дважды с типом службы IMyDependency. Второй вызов AddSingleton переопределяет предыдущий, если он разрешается как IMyDependency, и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable<IMyDependency>. Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable<{SERVICE}>.

services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
       IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is DifferentDependency);

        var dependencyArray = myDependencies.ToArray();
        Trace.Assert(dependencyArray[0] is MyDependency);
        Trace.Assert(dependencyArray[1] is DifferentDependency);
    }
}

Поведение внедрения через конструктор

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Контексты Entity Framework

По умолчанию контексты Entity Framework добавляются в контейнер службы с помощью времени существования с заданной областью, поскольку операции базы данных в веб-приложении обычно относятся к области клиентского запроса. Чтобы использовать другое время существования, укажите его с помощью перегрузки AddDbContext. Службы данного времени существования не должны использовать контекст базы данных с временем существования короче, чем у службы.

Параметры времени существования и регистрации

Чтобы продемонстрировать различия между указанными вариантами времени существования и регистрации службы, рассмотрим интерфейсы, представляющие задачу в виде операции с идентификатором OperationId. В зависимости от того, как время существования службы операции настроено для этих интерфейсов, при запросе из класса контейнер предоставляет тот же или другой экземпляр службы.

public interface IOperation
{
    string OperationId { get; }
}

public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }

Следующий класс Operation реализует все предыдущие интерфейсы. Конструктор Operation создает идентификатор GUID и сохраняет последние 4 символа в свойстве OperationId:

public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
    public Operation()
    {
        OperationId = Guid.NewGuid().ToString()[^4..];
    }

    public string OperationId { get; }
}

Метод Startup.ConfigureServices создает несколько регистраций класса Operation в соответствии с именованным временем существования:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

В примере приложения показано время существования объектов в пределах запросов и между запросами. IndexModel и ПО промежуточного слоя запрашивают каждый тип IOperation и регистрируют OperationId для каждого из них:

public class IndexModel : PageModel
{
    private readonly ILogger _logger;
    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;
    private readonly IOperationScoped _scopedOperation;

    public IndexModel(ILogger<IndexModel> logger,
                      IOperationTransient transientOperation,
                      IOperationScoped scopedOperation,
                      IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _scopedOperation    = scopedOperation;
        _singletonOperation = singletonOperation;
    }

    public void  OnGet()
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + _scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
    }
}

Аналогично IndexModel, ПО промежуточного слоя и разрешает те же службы:

public class MyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    private readonly IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        IOperationScoped scopedOperation)
    {
        _logger.LogInformation("Transient: " + _transientOperation.OperationId);
        _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
        _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

        await _next(context);
    }
}

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
}

Службы с заданной областью должны быть разрешены в методе InvokeAsync:

public async Task InvokeAsync(HttpContext context,
    IOperationScoped scopedOperation)
{
    _logger.LogInformation("Transient: " + _transientOperation.OperationId);
    _logger.LogInformation("Scoped: "    + scopedOperation.OperationId);
    _logger.LogInformation("Singleton: " + _singletonOperation.OperationId);

    await _next(context);
}

Выходные данные средства ведения журнала содержат:

  • Временные объекты всегда разные. Значение временного OperationId отличается в IndexModel и ПО промежуточного слоя.
  • Объекты с заданной областью остаются неизменными в пределах указанного запроса, но в новых запросах используются разные объекты.
  • Одноэлементные объекты одинаковы для каждого запроса.

Чтобы уменьшить выходные данные ведения журнала, задайте в appsettings.Development.json файле параметр LogLevel:Microsoft:Error:

{
  "MyKey": "MyKey from appsettings.Developement.json",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Debug",
      "Microsoft": "Error"
    }
  }
}

Вызов служб из функции main

Создайте IServiceScope с IServiceScopeFactory.CreateScope для разрешения службы с заданной областью в области приложения. Этот способ позволит получить доступ к службе с заданной областью при запуске для выполнения задач по инициализации.

В следующем примере показано, как получить доступ к службе IMyDependency с заданной областью и вызвать ее метод WriteMessage в Program.Main:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Проверка области

См. раздел Поведение при внедрении конструктора в статье Внедрение зависимостей в .NET.

Дополнительные сведения см. в разделе Проверка области.

Службы запросов

Службы и их зависимости в запросе ASP.NET Core предоставляются с помощью свойства HttpContext.RequestServices.

Платформа создает область для каждого запроса, а RequestServices предоставляет поставщик услуг с заданной областью. Все службы с заданной областью действительны до тех пор, пока запрос активен.

Примечание.

Предпочтительнее запрашивать зависимости в качестве параметров конструктора, а не разрешать службы из RequestServices. Таким образом вы получите классы, которые проще тестировать.

Проектирование служб для внедрения зависимостей

При разработке служб для внедрения зависимостей придерживайтесь следующих рекомендаций:

  • Избегайте статических классов и членов с отслеживанием состояния. Избегайте создания глобального состояния. Для этого проектируйте приложения для использования отдельных служб.
  • Избегайте прямого создания экземпляров зависимых классов внутри служб. Прямое создание экземпляров обязывает использовать в коде определенную реализацию.
  • Сделайте службы приложения небольшими, хорошо организованными и удобными в тестировании.

Если класс имеет слишком много внедренных зависимостей, это может указывать на то, что у класса слишком много задач и он нарушает принцип единственной обязанности. Попробуйте выполнить рефакторинг класса и перенести часть его обязанностей в новые классы. Помните, что в классах модели страниц Razor Pages и классах контроллера MVC должны преимущественно выполняться задачи, связанные с пользовательским интерфейсом.

Удаление служб

Контейнер вызывает Dispose для создаваемых им типов IDisposable. Службы, разрешенные из контейнера, никогда не должны удаляться разработчиком. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.

В следующем примере службы создаются контейнером службы и автоматически удаляются:

public class Service1 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service1: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service1.Dispose");
        _disposed = true;
    }
}

public class Service2 : IDisposable
{
    private bool _disposed;

    public void Write(string message)
    {
        Console.WriteLine($"Service2: {message}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service2.Dispose");
        _disposed = true;
    }
}

public interface IService3
{
    public void Write(string message);
}

public class Service3 : IService3, IDisposable
{
    private bool _disposed;

    public Service3(string myKey)
    {
        MyKey = myKey;
    }

    public string MyKey { get; }

    public void Write(string message)
    {
        Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
    }

    public void Dispose()
    {
        if (_disposed)
            return;

        Console.WriteLine("Service3.Dispose");
        _disposed = true;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
public class IndexModel : PageModel
{
    private readonly Service1 _service1;
    private readonly Service2 _service2;
    private readonly IService3 _service3;

    public IndexModel(Service1 service1, Service2 service2, IService3 service3)
    {
        _service1 = service1;
        _service2 = service2;
        _service3 = service3;
    }

    public void OnGet()
    {
        _service1.Write("IndexModel.OnGet");
        _service2.Write("IndexModel.OnGet");
        _service3.Write("IndexModel.OnGet");
    }
}

После каждого обновления страницы индекса в консоли отладки отображаются следующие выходные данные:

Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = My Key from config
Service1.Dispose

Службы, не созданные контейнером службы

Рассмотрим следующий код:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

В предыдущем коде:

  • Экземпляры службы не создаются контейнером службы.
  • Платформа не удаляет службы автоматически.
  • За удаление служб отвечает разработчик.

Руководство по применению временных и общих экземпляров IDisposable

См. раздел Рекомендации по IDisposable при использовании промежуточного и общего экземпляра в статье Внедрение зависимостей в .NET.

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

См. раздел Замена контейнера службы по умолчанию в статье Внедрение зависимостей в .NET.

Рекомендации

См. раздел Рекомендации в статье Внедрение зависимостей в .NET.

  • Старайтесь не использовать шаблон обнаружения служб. Например, не вызывайте GetService для получения экземпляра службы, когда можно использовать внедрение зависимостей:

    Неправильно:

    Incorrect code

    Правильное.

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • Другой вариант указателя службы, позволяющий избежать этого, — внедрение фабрики, которая разрешает зависимости во время выполнения. Оба метода смешивают стратегии инверсии управления.

  • Не используйте статический доступ к HttpContext (например, IHttpContextAccessor.HttpContext).

  • Избегайте вызовов BuildServiceProvider в ConfigureServices. Вызов BuildServiceProvider обычно происходит, когда разработчику необходимо разрешить службу в ConfigureServices. Например, рассмотрим случай, когда LoginPath загружается из конфигурации. Добавьте следующий код:

    bad code calling BuildServiceProvider

    На предыдущем рисунке при выборе строки, отмеченной зеленой волнистой линией в разделе services.BuildServiceProvider, отображается следующее предупреждение ASP0000:

    ASP0000. Вызов BuildServiceProvider из кода приложения приводит к созданию дополнительной копии создаваемых одноэлементных служб. В качестве параметров для Configure можно использовать альтернативные варианты, такие как службы внедрения зависимостей.

    При вызове BuildServiceProvider создается второй контейнер, который может создавать разорванные одноэлементные экземпляры и ссылаться на графы объектов в нескольких контейнерах.

    Правильный способ получения LoginPath — использование встроенной поддержки шаблона параметров для внедрения зависимостей:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • Контейнер собирает удаляемые временные службы для удаления. Это может привести к утечке памяти, если она разрешена из контейнера верхнего уровня.

  • Включите проверку области, чтобы убедиться, что в приложении нет отдельных объектов, записывающих службы с заданной областью. Дополнительные сведения см. в разделе Проверка области.

Как и с любыми рекомендациями, у вас могут возникнуть ситуации, когда нужно отступить от одного из правил. Исключения возникают редко, — как правило, это особые случаи, связанные с самой платформой.

Внедрение зависимостей является альтернативой для шаблонов доступа к статическим или глобальным объектам. Вы не сможете воспользоваться преимуществами внедрения зависимостей, если будете сочетать его с доступом к статическим объектам.

Orchard Core — это платформа приложений для создания модульных мультитенантных приложений в ASP.NET Core. Дополнительные сведения см. в документации по Orchard Core.

Примеры создания модульных и мультитенантных приложений с использованием только Orchard Core Framework без каких-либо особых функций CMS см. здесь.

Платформенные службы

Метод Startup.ConfigureServices регистрирует службы, которые использует приложение, включая такие компоненты, как Entity Framework Core и ASP.NET Core MVC. Изначально коллекция IServiceCollection, предоставленная для ConfigureServices, содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах ASP.NET Core, платформа регистрирует более 250 служб.

В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.

Тип службы Время существования
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Временный
IHostApplicationLifetime Отдельная
IWebHostEnvironment Отдельная
Microsoft.AspNetCore.Hosting.IStartup Отдельная
Microsoft.AspNetCore.Hosting.IStartupFilter Временный
Microsoft.AspNetCore.Hosting.Server.IServer Отдельная
Microsoft.AspNetCore.Http.IHttpContextFactory Временный
Microsoft.Extensions.Logging.ILogger<TCategoryName> Отдельная
Microsoft.Extensions.Logging.ILoggerFactory Отдельная
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Отдельная
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Временный
Microsoft.Extensions.Options.IOptions<TOptions> Отдельная
System.Diagnostics.DiagnosticSource Отдельная
System.Diagnostics.DiagnosticListener Отдельная

Дополнительные ресурсы