Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Инъекция зависимостей в ASP.NET Core - Практический подход

Запись от UnmanagedCoder размещена 11.05.2025 в 18:25
Показов 4074 Комментарии 0

Нажмите на изображение для увеличения
Название: 9120fa54-9d9d-439d-968a-618104a0d90b.jpg
Просмотров: 302
Размер:	155.9 Кб
ID:	10790
Инъекция зависимостей (Dependency Injection, DI) — это техника программирования, которая кардинально меняет подход к управлению зависимостями в приложениях. Представьте модульный дом, где каждая комната не намертво прикручена к фундаменту, а устанавливается на него через специальные крепления. При необходимости вы спокойно меняете гостинную на спальню, не перестраивая весь дом. Вот это и есть суть DI — компоненты приложения не создают свои зависимости напрямую, а получают их извне.

Введение в концепцию инъекции зависимостей



Зависимости в коде — неизбежная реальность, с которой сталкивается любой разработчик. Это как с электроприборами в квартире: все они зависят от розеток, проводов и электростанции — но мы ведь не ставим мини-генератор в каждый тостер? Также и с кодом: один класс обычно требует сервисы другого для нормальной работы. Проблема возникает, когда эти связи становятся жесткими, словно компоненты склеены супер-клеем.

C#
1
2
3
4
5
6
7
8
9
10
// Жесткая связь — класс сам создает зависимость
public class OrderProcessor
{
    private readonly PaymentService _paymentService;
    
    public OrderProcessor()
    {
        _paymentService = new PaymentService(); // Вот она — жесткая связь
    }
}
Этот код выглядит невино, пока не приходит время тестирования или смены реализации платежной системы. Тогда эта "простота" оборачивается кошмаром. С DI же ситуация выглядит иначе:

C#
1
2
3
4
5
6
7
8
9
public class OrderProcessor
{
    private readonly IPaymentService _paymentService;
    
    public OrderProcessor(IPaymentService paymentService)
    {
        _paymentService = paymentService; // Зависимость пришла извне
    }
}
Теперь наш OrderProcessor не знает, какая именно реализация IPaymentService будет использоваться — это решение принимается на более высоком уровне. Это суть принципа инверсии зависимостей (Dependency Inversion Principle) из SOLID: модули высокого уровня не должны зависить от деталей реализации модулей низкого уровня.

ASP.NET Core вывел применение этого принципа на новый уровень. В отличие от классического ASP.NET, где DI существовал как дополнение, в Core инъекция зависимостей — фундаментальная часть фреймворка. Причом не "довесок", а встроеная в ядро экосистемы. Когда я впервые с этим столкнулся при переходе с .NET Framework на Core, меня поразила элегантность реализации. Вместо кучи XML-конфигураций или стороних библиотек DI в ASP.NET Core ограничивается несколькими строчками в Startup.cs или Program.cs. Вот как просто выглядит регистрация сервиса:

C#
1
services.AddScoped<IPaymentService, StripePaymentService>();
Эта одна строчка говорит контейнеру DI: "Когда кто-то просит IPaymentService, дай ему экземпляр StripePaymentService". И всё! Магия случится сама — нужный объект прилетит в конструктор класса без дополнительных действий. Сравнивая с другими фреймворками, не могу не отметить, что в Angular или Spring DI тоже встроенный, но ASP.NET Core, на мой взгляд, нашол золотую середину между гибкостью и простотой. В отличие от Spring, где инъекции могут выглядеть как коллекция загадочных аннотаций, ASP.NET Core предлагает прозрачную модель с минимумом синтаксического шума.

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

Впрочем, у любой медали есть обратная сторона. Неправильное использование DI может привести к невидимым циклическим зависимостям или проблемам производительности. Особенно распространенная ошибка — регистрация всех сервисов как Singleton, когда этого не требуется, что приводит к неконтролируемому росту использования памяти.

Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2
Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными...

ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком?
Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать...

Какая разница между ASP .Net Core и ASP .Net Core MVC?
Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И...

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


Механизмы инъекции зависимостей в ASP.NET Core



ASP.NET Core содержит встроеный DI-контейнер, который выступает в роли умного диспетчера, распределяющего все необходимые сервисы вашего приложения. Думаете о нём как о невидимом помощнике, который знает, что кому нужно, и в нужный момент подает нужные инструменты. В отличие от "дедовских" подходов, где мы часто использовали сторонние решения вроде Autofac, Castle Windsor или Ninject, ASP.NET Core предлагает собственный механизм прямо из коробки. Этот контейнер реализует интерфейс IServiceProvider, который содержит единственый метод GetService(). Такая минималистичная модель имеет свои преимущества — она проста в понимании и, что важнее, крайне эффективна по производительности. Майкрософт специально оптимизировал встроенный провайдер под типичные сценарии использования веб-приложений.

C#
1
2
3
4
public interface IServiceProvider
{
    object GetService(Type serviceType);
}
Базовый интерфейс предельно прост, но вся мощь раскрывается через IServiceCollection, которая используется для регистрации сервисов. Вот где начинается самое интересное: время жизни сервисов.

Сервисы и время жизни



Когда дело доходит до регистрации зависимостей, первое решение, которое мы принимаем — какое время жизни (lifetime) присвоить каждому сервису. ASP.NET Core предлагает три основных типа регистрации, и выбор между ними критически важен:

1. Transient — самый короткоживущий вариант. Каждый раз, когда сервис запрашивается, создаётся новый экземпляр. Как чашка кофе в кафе — для каждого посетителя своя.

C#
1
services.AddTransient<IDataGenerator, RandomDataGenerator>();
2. Scoped — экземпляр создаётся один раз за время жизни "области" (обычно HTTP-запроса). Если десять компонентов в рамках одного запроса запросят этот сервис, все они получат один и тот же объект.

C#
1
services.AddScoped<IOrderRepository, SqlOrderRepository>();
3. Singleton — живёт на протяжении всего времени работы приложения. Создаётся единожды при первом запросе и далее используется для всех последующих запросов.

C#
1
services.AddSingleton<ICacheManager, MemoryCacheManager>();
Выбор правильного времени жизни — не просто технический вопрос, а вопрос правильной работы всего приложения. Я множество раз виделл, как неверный выбор приводил к утечкам памяти или непредсказуемому поведению кода. Например, регистрация компонента с подключением к базе данных как Singleton — это почти гарантированная беда. В лучшем случае вы получите непредсказуемое поведение, в худшем — разрыв соединения с базой при истечении таймаута, который повлияет на всех пользователей.

C#
1
2
// Опасно! Не делайте так в реальном коде!
services.AddSingleton<DbContext, MyAppDbContext>(); // неправильно!
А вот как это должно выглядеть:

C#
1
2
3
// Правельно — DbContext должен быть scoped, чтобы жить внутри HTTP-запроса
services.AddDbContext<MyAppDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

Конфигурирование сервисов



Регистрация сервисов в ASP.NET Core происходит через метод ConfigureServices класса Startup (для приложений до .NET 6) или непосредственно в Program.cs для минималистичного подхода в .NET 6+.

C#
1
2
3
4
5
6
7
8
9
10
11
12
// До .NET 6 (в Startup.cs)
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserService, UserService>();
    services.AddTransient<INotificationService, EmailNotificationService>();
    services.AddSingleton<IConfiguration>(Configuration);
}
 
// В .NET 6+ (Program.cs)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddTransient<INotificationService, EmailNotificationService>();
Однако простой регистрации иногда недостаточно. Часто нужно получить доступ к настройкам или внешним сервисам во время создания объекта. Тут на помощь приходят перегрузки метода регистрации с ламбда-выражениями:

C#
1
2
3
4
5
services.AddTransient<IReportGenerator>(serviceProvider => {
    var config = serviceProvider.GetService<IConfiguration>();
    var templatesPath = config["ReportTemplatePath"];
    return new PdfReportGenerator(templatesPath);
});
Такой подход позволяет динамично настраивать создаваемые сервисы. Это особенно полезно, когда объекту нужны дополнительные параметры, помимо зависимостей, или когда решение о конкретной реализации принимается на основе конфигурации.

Условная регистрация сервисов



Иногда нам нужно использовать разные реализации одного интерфейса в зависимости от условий. Например, в режиме разработки мы хотим использовать заглушки вместо реальных сервисов. ASP.NET Core позволяет элегантно решить эту задачу:

C#
1
2
3
4
5
6
7
8
if (env.IsDevelopment())
{
    services.AddScoped<IPaymentGateway, MockPaymentGateway>();
}
else
{
    services.AddScoped<IPaymentGateway, StripePaymentGateway>();
}
Еще интереснее — возможность регистрировать одновременно несколько реализаций одного интерфейса и затем выбирать нужную по дополнительному параметру. Для этого можно использовать паттерн "Стратегия" в комбинации с DI:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Регистрируем все доступные платежные системы
services.AddTransient<StripePaymentGateway>();
services.AddTransient<PayPalPaymentGateway>();
services.AddTransient<BitcoinPaymentGateway>();
 
// И фабрику, которая выбирает конкретную реализацию
services.AddTransient<Func<string, IPaymentGateway>>(serviceProvider => gatewayName =>
{
    return gatewayName switch
    {
        "stripe" => serviceProvider.GetService<StripePaymentGateway>(),
        "paypal" => serviceProvider.GetService<PayPalPaymentGateway>(),
        "bitcoin" => serviceProvider.GetService<BitcoinPaymentGateway>(),
        _ => throw new ArgumentException($"Unknown gateway: {gatewayName}")
    };
});
А затем инджектим и используем эту фабрику для получения конкретной платёжной системы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PaymentProcessor
{
    private readonly Func<string, IPaymentGateway> _gatewayFactory;
 
    public PaymentProcessor(Func<string, IPaymentGateway> gatewayFactory)
    {
        _gatewayFactory = gatewayFactory;
    }
 
    public async Task ProcessPayment(Order order)
    {
        var gateway = _gatewayFactory(order.PreferredPaymentMethod);
        await gateway.ProcessPayment(order.Amount);
    }
}

Инъекция настроек



Особый случай DI — инъекция объектов конфигурации. Здесь ASP.NET Core предлагает очень удобный механизм строго типизированных настроек через IOptions<T>. Благодаря этому мы можем забыть о строковых ключах и приводить типов — все настройки мапятся напрямую на C# классы. Начинается все с создания класса с настройками:

C#
1
2
3
4
5
6
7
public class SmtpSettings
{
    public string Server { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
Затем регистрируем его в DI-контейнере, связывая с определенной секцией конфигурации:

C#
1
services.Configure<SmtpSettings>(Configuration.GetSection("Smtp"));
И теперь мы можем инжектить эти настройки напрямую в сервисы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EmailService
{
    private readonly SmtpSettings _settings;
 
    public EmailService(IOptions<SmtpSettings> options)
    {
        _settings = options.Value;
    }
 
    public void SendEmail(string to, string subject, string body)
    {
        // Использование _settings для отправки email
    }
}
Такой подход обеспечивает типобезопасность и удобное централизованное управление настройками. Ушли в прошлое дни статических классов с константами или ручного парсинга XML/JSON-конфигураций.

Управление жизненым циклом сервисов



Мы уже рассмотрели основные типы времени жизни (Transient, Scoped, Singleton), но что происходит, когда эти сервисы должны освободить ресурсы? ASP.NET Core автоматически отслеживает сервисы, реализующие интерфейс IDisposable, и вызывает метод Dispose() в нужный момент.На практике это означает, что:
  1. Transient-сервисы избавляются от ресурсов, когда избавляются от соответствующего Scoped-сервиса, который их использовал.
  2. Scoped-сервисы освобождаются в конце HTTP-запроса или аналогичного "скоупа".
  3. Singleton-сервисы освобождаются только при закрытии приложения.

Это поведение создаёт важное правило, которое я выучил на собственных ошибках: никогда не инжектите короткоживущие (Scoped) зависимости в долгоживущие (Singleton) сервисы! Это нарушает модель освобождения ресурсов и может привести к утечкам памяти или, что хуже, к неправельному поведению, которое сложно отследить.

C#
1
2
3
4
5
6
7
8
9
10
// Антипаттерн! Scoped-сервис в Singleton-сервисе
public class GlobalCache // зарегистрирован как Singleton
{
private readonly MyDbContext _dbContext; // DbContext обычно Scoped!
 
public GlobalCache(MyDbContext dbContext)
{
    _dbContext = dbContext; // Опасность! Этот контекст никогда не обновится!
}
}
К счастью, ASP.NET Core содержит встроенную защиту, которая выбросит исключение при попытке сделать такую инъекцию. Но это работает только если инъекция происходит напрямую, а не через фабрику или другой промежуточный механизм.

Работа с IServiceProvider



Хотя основной механизм DI в ASP.NET Core — это внедрение через конструкторы, иногда требуются более гибкие подходы. Тут на помошь приходит IServiceProvider — интерфейс, позволяющий получать сервисы программно.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ComplexService
{
private readonly IServiceProvider _serviceProvider;
 
public ComplexService(IServiceProvider serviceProvider)
{
    _serviceProvider = serviceProvider;
}
 
public void ProcessSpecialCase(string caseType)
{
    // Получаем нужный обработчик в зависимости от типа кейса
    var handler = _serviceProvider.GetService(GetHandlerType(caseType));
    
    // Делаем что-то с handler
}
 
private Type GetHandlerType(string caseType)
{
    // Логика определения типа обработчика
    return typeof(DefaultCaseHandler);
}
}
Однако этот подход нужно исползовать с осторожностью. За всю свою карьеру я пришел к выводу, что если кажется, будто нужен доступ к IServiceProvider напрямую, скорее всего проблема в дизайне приложения.
Частое использование ServiceLocator (а именно это и есть GetService()) считается антипаттерном, поскольку скрывает зависимости класса и делает код менее тестируемым. Но есть случаи, когда это оправдано:
1. Фабрики, создающие объекты по запросу.
2. Сложные сценарии с условной логикой создания зависимостей.
3. Работа с устаревшими API, которые нельзя изменить.
Для безопасной работы с нужными скоупами при использовании IServiceProvider существует паттерн "создание скоупа":

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BackgroundProcessor
{
private readonly IServiceScopeFactory _scopeFactory;
 
public BackgroundProcessor(IServiceScopeFactory scopeFactory)
{
    _scopeFactory = scopeFactory;
}
 
public async Task Process()
{
    // Создаем новый скоуп, аналогичный HTTP-запросу
    using (var scope = _scopeFactory.CreateScope())
    {
        // Получаем scoped-сервис из нового скоупа
        var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
        
        // Работаем с сервисом
        var items = await dbContext.Items.ToListAsync();
        // ...
    } // При выходе из using скоуп закрывается, а dbContext уничтожается
}
}
Этот паттерн особенно важен для фоновых служб, которые работают вне контекста HTTP-запроса, но должны использовать scoped-сервисы.

Ограничения встроенного DI-контейнера



Несмотря на все преимущества, встроенный контейнер ASP.NET Core имеет определенные ограничения:
1. Отсутствие встроенной поддержки именованных (named) регистраций.
2. Ограниченые возможности для интерцепции и декорирования.
3. Отсутствие поддержки сканирования сборок для автоматической регистрации.

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

C#
1
2
3
// В Autofac это выглядело бы так:
builder.RegisterType<StripePaymentGateway>().Named<IPaymentGateway>("stripe");
builder.RegisterType<PayPalPaymentGateway>().Named<IPaymentGateway>("paypal");
Для декорирования сервисов при использовании встроенного контейнера также требуются дополнительные усилия:

C#
1
2
3
4
5
services.AddTransient<IOriginalService, OriginalService>();
services.AddTransient<IOriginalService>(serviceProvider => {
    var original = serviceProvider.GetRequiredService<OriginalService>();
    return new DecoratorService(original);
});
Не самый элегантный код, который в более продвинутых контейнерах решается одной строкой. Тем не менее, для большинства приложений встроенного DI-контейнера ASP.NET Core вполне достаточно, и его производительность часто выше, чем у больших и навороченных альтернатив. Если же функциональности не хватает, ASP.NET Core позволяет заменить встроенный контейнер на любой другой, реализующий интерфейс IServiceProvider. Для этого нужно создать адаптер между вашим любимым контейнером и системой ASP.NET Core, но это тема для отдельного обсуждения.

Контроллеры и сервисы: классический тандем



Самый распространенный сценарий — инъекция сервисов в контроллеры. Это настолько базовый паттерн в ASP.NET Core, что его часто воспринимают как должное. Но именно здесь начинается понимание всей элегантности подхода.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly ILogger<UsersController> _logger;
 
    public UsersController(IUserService userService, ILogger<UsersController> logger)
    {
        _userService = userService;
        _logger = logger;
    }
 
    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        _logger.LogInformation("Fetching user with ID: {UserId}", id);
        
        var user = await _userService.GetByIdAsync(id);
        if (user == null)
            return NotFound();
            
        return Ok(user);
    }
}
Контроллер ничего не знает о том, какая именно реализация IUserService будет использоваться — это может быть сервис, работающий с базой данных, API или даже с кэшем. Смена одной реализации на другую не потребует изменения ни строчки кода в контроллере. Но что действительно крутое — как в ASP.NET Core устроена многослойная инъекция. Сервисы могут содержать свои зависимости, которые тоже инжектятся автоматически. Получается своеобразная "матрёшка" зависимостей:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class UserService : IUserService
{
    private readonly IUserRepository _repository;
    private readonly ICacheService _cache;
    
    public UserService(IUserRepository repository, ICacheService cache)
    {
        _repository = repository;
        _cache = cache;
    }
    
    public async Task<UserDto> GetByIdAsync(int id)
    {
        // Сначала проверяем кэш
        var cachedUser = await _cache.GetAsync<UserDto>($"user:{id}");
        if (cachedUser != null)
            return cachedUser;
        
        // Если в кэше нет — получаем из репозитория
        var user = await _repository.GetByIdAsync(id);
        if (user != null)
        {
            // Сохраняем в кэш на будущее
            await _cache.SetAsync($"user:{id}", user, TimeSpan.FromMinutes(10));
        }
        
        return user;
    }
}
А IUserRepository может опираться на DbContext или другие инжектированные зависимости. Эта цепочка может идти достаточно глубоко, и всё это работает без необходимости явной настройки каждой связи.

DI в многоуровневой архитектуре



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

C#
1
2
3
4
5
|-- Presentation Layer (Controllers, Views)
|   |-- depends on -->
|-- Business Logic Layer (Services)
|   |-- depends on -->
|-- Data Access Layer (Repositories)
Каждый слой общается с нижестоящим через абстракции (интерфейсы), а DI осуществляет связывание конкретных реализаций. Такой подход делает каждый слой заменяемым и независимо тестируемым. Одна из ситуаций, где я оценил этот подход — при миграции устаревшей базы данных. Мы полностью заменили слой доступа к данным, но поскольку он был отделен абстракциями, ни сервисы, ни контроллеры не пришлось переписывать.

Инъекция в фильтры и middleware



ASP.NET Core позволяет инжектить зависимости даже в такие экзотические компоненты, как фильтры действий. Это намного мощнее, чем может показаться на первый взгляд.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AuditLogActionFilter : IActionFilter
{
    private readonly IAuditLogger _auditLogger;
 
    public AuditLogActionFilter(IAuditLogger auditLogger)
    {
        _auditLogger = auditLogger;
    }
 
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Логирование до выполнения действия
        _auditLogger.LogAction(
            context.HttpContext.User.Identity.Name,
            context.ActionDescriptor.DisplayName);
    }
 
    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Можно логировать результат действия
    }
}
Регистрация такого фильтра требует немного больше работы:

C#
1
2
3
4
5
services.AddScoped<AuditLogActionFilter>();
services.AddMvc(options =>
{
    options.Filters.AddService<AuditLogActionFilter>();
});
Этот потрясающий механизм работает и для middleware-компонентов, что позволяет создавать модульные и тестируемые компоненты конвейера обработки запросов.

SignalR и реалтайм-сценарии



Когда дело доходит до работы с реалтайм-компонентами вроде SignalR, DI становится просто незаменимым. SignalR-хабы созданы для обработки долгоживущих соединений, и правильное внедрение зависимостей может существенно повлиять на производительность.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ChatHub : Hub
{
    private readonly IChatService _chatService;
    private readonly IUserConnectionManager _connectionManager;
 
    public ChatHub(IChatService chatService, IUserConnectionManager connectionManager)
    {
        _chatService = chatService;
        _connectionManager = connectionManager;
    }
 
    public async Task SendMessage(string message)
    {
        var user = Context.User.Identity.Name;
        var formattedMessage = await _chatService.FormatMessage(user, message);
        
        // Сохраняем сообщение
        await _chatService.SaveMessageAsync(user, message);
        
        // Отправляем всем подключенным клиентам
        await Clients.All.SendAsync("ReceiveMessage", user, formattedMessage);
    }
}
Я однажды работал над проектом, где SignalR использовался для обновления торговой площадки в реальном времени. Инъекция сервисов кэша и оптимизированных репозиториев позволила нам достичь впечатляющей производительности даже при тысячах одновременных подключений.

Тестирование с DI



Наверное, самым недооцененным аспектом DI является его влияние на тестируемость. Представьте, что у вас есть контроллер который использует какой-нибудь сервис платежной системы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[ApiController]
[Route("[controller]")]
public class PaymentController : ControllerBase
{
    private readonly IPaymentGateway _paymentGateway;
    
    public PaymentController(IPaymentGateway paymentGateway)
    {
        _paymentGateway = paymentGateway;
    }
    
    [HttpPost]
    public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
    {
        var result = await _paymentGateway.ProcessPaymentAsync(
            request.CardNumber,
            request.ExpiryDate,
            request.Cvv,
            request.Amount);
            
        if (result.Success)
            return Ok(new { TransactionId = result.TransactionId });
            
        return BadRequest(new { Error = result.ErrorMessage });
    }
}
При тестировании мы можем легко подменить реальный платежный шлюз на тестовый вариант:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[Fact]
public async Task ProcessPayment_WithValidCard_ReturnsOk()
{
    // Arrange
    var mockGateway = new Mock<IPaymentGateway>();
    mockGateway.Setup(g => g.ProcessPaymentAsync(
        It.IsAny<string>(),
        It.IsAny<string>(),
        It.IsAny<string>(),
        It.IsAny<decimal>()
    )).ReturnsAsync(new PaymentResult { 
        Success = true, 
        TransactionId = "test-tx-123" 
    });
    
    var controller = new PaymentController(mockGateway.Object);
    var request = new PaymentRequest
    {
        CardNumber = "4111111111111111",
        ExpiryDate = "12/25",
        Cvv = "123",
        Amount = 99.99m
    };
    
    // Act
    var result = await controller.ProcessPayment(request);
    
    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    dynamic value = okResult.Value;
    Assert.Equal("test-tx-123", value.TransactionId);
}
Этот подход позволяет тестировать контроллеры, сервисы и другие компоненты в изоляции, без необходимости поднятия реальных баз данных, API или других внешних зависимостей. А если вы используете интеграционные тесты ASP.NET Core, можно подменять сервисы прямо в тестовой среде:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class PaymentApiTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _factory;
    
    public PaymentApiTests(WebApplicationFactory<Startup> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Заменяем реальный сервис платежей на тестовый
                services.RemoveAll<IPaymentGateway>();
                services.AddScoped<IPaymentGateway, TestPaymentGateway>();
            });
        });
    }
    
    [Fact]
    public async Task PaymentEndpoint_WithValidRequest_ReturnsSuccess()
    {
        // Создаем HTTP-клиент с настроенными тестовыми сервисами
        var client = _factory.CreateClient();
        
        // Далее тестируем API через этот клиент
    }
}
В нашем проекте мы пошли ещё дальше и создали набор тестовых расширений для DI-контейнера, позволяющих легко переключаться между реальными и моковыми реализациями в тестовой среде. Это существенно упростило тестирование сложных взаимодействий между компонентами.

DI в Blazor-компонентах



Если вы окунулись в Blazor — фреймворк для создания интерактивных веб-приложений на C#, то там инъекция зависимостей работает с некоторыми особенностями. Blazor поддерживает DI как для серверной модели, так и для WebAssembly.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@page "/counter"
@inject ICounterService CounterService
 
<h1>Counter</h1>
 
<p>Current count: @currentCount</p>
 
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
 
@code {
private int currentCount = 0;
 
protected override async Task OnInitializedAsync()
{
    currentCount = await CounterService.GetInitialCountAsync();
}
 
private async Task IncrementCount()
{
    currentCount = await CounterService.IncrementCountAsync(currentCount);
}
}
Директива @inject здесь эквивалентна конструктору с параметрами в обычных классах. Но есть и альтернативный синтаксис с атрибутом [Inject]:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@page "/counter"
 
<h1>Counter</h1>
 
<p>Current count: @currentCount</p>
 
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
 
@code {
[Inject]
private ICounterService CounterService { get; set; }
 
private int currentCount = 0;
 
// ... остальной код как выше
}
Это особенно удобно, когда вы работаете с Blazor UI-компонентами, которые могут использоваться многократно и в разных контекстах.

Serverless и DI: казалось бы, несовместимое



Одна из наиболее нетривиальных областей для применения DI — Azure Functions и другие serverless-решения. В ранних версиях работать с DI там было... мягко говоря, непросто. Но сейчас ситуация улучшилась. Вот пример настройки DI для Azure Functions:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Startup.cs
[assembly: FunctionsStartup(typeof(MyNamespace.Startup))]
namespace MyNamespace
{
public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddHttpClient();
        builder.Services.AddScoped<IDataService, DataService>();
        builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
    }
}
}
А вот как выглядит сама функция, использующая инъекцию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ProcessDataFunction
{
private readonly IDataService _dataService;
private readonly ICacheService _cacheService;
 
public ProcessDataFunction(IDataService dataService, ICacheService cacheService)
{
    _dataService = dataService;
    _cacheService = cacheService;
}
 
[FunctionName("ProcessData")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("Processing data request");
    
    // Проверяем кэш
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    var cacheKey = ComputeHash(requestBody);
    
    if (_cacheService.TryGetValue(cacheKey, out var cachedResult))
    {
        log.LogInformation("Cache hit!");
        return new OkObjectResult(cachedResult);
    }
    
    // Обрабатываем данные
    var data = JsonConvert.DeserializeObject<DataPayload>(requestBody);
    var result = await _dataService.ProcessAsync(data);
    
    // Сохраняем в кэш
    _cacheService.Set(cacheKey, result, TimeSpan.FromMinutes(15));
    
    return new OkObjectResult(result);
}
 
private static string ComputeHash(string input)
{
    // Хеш-функция для создания ключей кэша
    using var md5 = MD5.Create();
    var hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
    return Convert.ToBase64String(hashBytes);
}
}
Что замечательно в этом подходе — функции становятся более тестируемыми и модульными, а общая функциональность, такая как кеширование или доступ к данным, может быть переиспользована между различными функциями.

Кастомные ASP.NET Core тэги и DI



Еще один интересный сценарий — создание собственных TagHelper'ов в MVC/Razor Pages с использованием DI. Предположим, мы хотим создать тэг, который автоматически отображает данные пользователя:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class UserProfileTagHelper : TagHelper
{
private readonly IUserService _userService;
 
public UserProfileTagHelper(IUserService userService)
{
    _userService = userService;
}
 
[HtmlAttributeName("user-id")]
public int UserId { get; set; }
 
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    var user = await _userService.GetByIdAsync(UserId);
    
    output.TagName = "div";
    output.Attributes.SetAttribute("class", "user-profile");
    
    if (user == null)
    {
        output.Content.SetHtmlContent("<p>User not found</p>");
        return;
    }
    
    output.Content.SetHtmlContent($@"
        <div class='profile-header'>
            <img src='{user.AvatarUrl}' alt='{user.Name}' />
            <h3>{user.Name}</h3>
        </div>
        <div class='profile-details'>
            <p>Joined: {user.JoinedDate.ToShortDateString()}</p>
            <p>Posts: {user.PostCount}</p>
        </div>
    ");
}
}
А затем использовать его в Razor-страницах:

[/CSHARP]html
<user-profile user-id="123"></user-profile>
[/CSHARP]

Встроенный механизм DI в ASP.NET Core автоматически внедрит нужный IUserService в наш TagHelper.
Как видите, инъекция зависимотей проникла практически во все уголки ASP.NET Core экосистемы, делая даже самые нишевые компоненты более модульными и тестируемыми.

Одна из историй из реальной жизни: на проекте мы обнаружили, что наши TagHelper'ы дублировали большую часть логики, которая уже существовала в сервисном слое. После рефакторинга с использованием DI для инъекции общих сервисов размер кода уменьшился почти в два раза, а его качество заметно выросло. Кстати, и тесты стало писать намного проще. Впрочем, нельзя не отметить, что иногда излишнее усердие в применении DI может привести к так называемому "DI hell", когда зависимости становятся настолько запутанными, что трудно отследить жизненый цикл объектов. Поэтому всегда стоит стремиться к балансу между гибкостью и простотой.

Борьба с циклическими зависимостями



Циклические зависимости – это ситуации, когда класс A зависит от класса B, а класс B зависит от класса A (прямо или косвенно). Это архитектурная проблема, которая может вызвать головную боль у самого опытного разработчика.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ServiceA
{
    private readonly ServiceB _serviceB;
    
    public ServiceA(ServiceB serviceB)
    {
        _serviceB = serviceB;
    }
}
 
public class ServiceB
{
    private readonly ServiceA _serviceA;
    
    public ServiceB(ServiceA serviceA)
    {
        _serviceA = serviceA;
    }
}
При попытке создать любой из этих двух сервисов контейнер DI зайдет в бесконечную рекурсию. ASP.NET Core выбросит исключение InvalidOperationException с сообшением о циклической зависимости.

Как решать эту проблему? Существует несколько подходов:

1. Рефакторинг архитектуры – идеальное решение, но не всегда возможное в унаследованом коде.
2. Разрыв цикла с помощью интерфейса – вынесте общую функциональность в третий сервис, от которого будут зависеть оба класса.
3. Отложенное разрешение зависимостей – вместо инъекции через конструктор используйте провайдер сервисов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ServiceA
{
    private readonly IServiceProvider _serviceProvider;
    
    public ServiceA(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void DoSomething()
    {
        // Получаем ServiceB только когда нужно
        var serviceB = _serviceProvider.GetRequiredService<ServiceB>();
        serviceB.Execute();
    }
}
Последний вариант – нешёл решение из принципов чистого DI, но на практике иногда это единственый выход.
Я однажды столкнулся с ситуацией, когда в большом проекте два модуля оказались циклически зависимы. Полный рефакторинг был невозможен из-за сроков. Мы применили гибридный подход: критические зависимости оставили в конструкторе, а циклические инжектили через фабрику на момент использования.

Generic-сервисы и открытые дженерики



Одна из мощнейших, но редко используемых фич DI в ASP.NET Core – возможность регистрации и использования дженерик-сервисов. Это особенно полезно, когда у вас есть абстракция, которая параметризуется различными типами.
Представьте, что у нас есть универсальный репозиторий:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(int id);
}
 
public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    
    public Repository(DbContext context)
    {
        _context = context;
    }
    
    // Реализация методов
}
Мы можем зарегистрировать этот репозиторий для всех возможных типов:

C#
1
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
Это заставит DI-контейнер создавать экземпляры нужного репозитория на лету, для любого запрошенного типа:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserService
{
    private readonly IRepository<User> _userRepository;
    
    public UserService(IRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }
}
 
public class ProductService
{
    private readonly IRepository<Product> _productRepository;
    
    public ProductService(IRepository<Product> productRepository)
    {
        _productRepository = productRepository;
    }
}
Обратите внимание на синтаксис typeof(IRepository<>) – пустые угловые скобки означают открытый дженерик. Это говорит DI-контейнеру: "Для любого закрытого типа IRepository<T> используй соответствующий Repository<T>".

Фабрики и делегаты в DI



ASP.NET Core позволяет регистрировать не только конкретные типы, но и фабричные методы для их создания. Это чрезвычайно полезно, когда создание объекта требует сложной логики или параметров, недоступных через DI. Базовый пример – регистрация с использованием лябда-выражения:

C#
1
2
3
4
5
6
7
8
9
10
services.AddTransient<IComplexService>(sp => {
    var config = sp.GetRequiredService<IConfiguration>();
    var dbContext = sp.GetRequiredService<AppDbContext>();
    var logger = sp.GetRequiredService<ILogger<ComplexService>>();
    
    var connectionString = config.GetConnectionString("SpecialApi");
    var timeout = config.GetValue<TimeSpan>("ApiTimeout");
    
    return new ComplexService(dbContext, logger, connectionString, timeout);
});
Но настоящая магия начинается, когда вы внедряете фабрику как зависимость. ASP.NET Core позволяет инжектить Func<T>, который действует как фабрика для создания экземпляров:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DynamicServiceConsumer
{
    private readonly Func<IServiceType> _serviceFactory;
    
    public DynamicServiceConsumer(Func<IServiceType> serviceFactory)
    {
        _serviceFactory = serviceFactory;
    }
    
    public void ProcessSomething()
    {
        // Создаем новый экземпляр каждый раз
        var service = _serviceFactory();
        service.DoWork();
    }
}
Это особенно полезно, когда вам нужно создавать экземпляры сервиса внутри цикла или вы хотите создавать новый экземпляр по требованию, не загразняя контейнер слишком большим количеством инъекций.
Для параметризированных фабрик вы можете использовать делегаты с аргументами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Регистрация фабрики, которая принимает параметры
services.AddTransient<Func<string, int, IConfigurableService>>((sp, name, value) => {
    return new ConfigurableService(name, value);
});
 
// Использование
public class ServiceWithFactory
{
    private readonly Func<string, int, IConfigurableService> _factory;
    
    public ServiceWithFactory(Func<string, int, IConfigurableService> factory)
    {
        _factory = factory;
    }
    
    public void Execute()
    {
        var service1 = _factory("Service1", 100);
        var service2 = _factory("Service2", 200);
        
        // ...
    }
}
В одном из моих проектов мы использовали этот подход для динамического создания обработчиков команд в паттерне CQRS. Фабрика принимала тип команды и возвращала соответствующий обработчик, что позволяло нам создавать их по требованию, не перегружая DI-контейнер.

Динамическая реконфигурация DI-контейнера



В большинстве случаев DI-контейнер настраивается один раз при запуске приложения. Однако иногда возникают ситуации, когда требуется изменить регистрации сервисов на лету. Стандартный DI-контейнер ASP.NET Core не поддерживает такую функциональность напрямую, но есть обходные пути. Одно из решений – использование паттерна "Композитный сервис". Вместо прямой замены сервиса в контейнере, мы регистрируем прокси, который может переключаться между различными реализациями:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class CompositeMessageService : IMessageService
{
    private readonly IServiceProvider _serviceProvider;
    private string _currentImplementationKey = "default";
    
    // Словарь с доступными реализациями
    private readonly Dictionary<string, Type> _implementations = new()
    {
        ["default"] = typeof(DefaultMessageService),
        ["premium"] = typeof(PremiumMessageService),
        ["fallback"] = typeof(FallbackMessageService)
    };
    
    public CompositeMessageService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void SwitchImplementation(string key)
    {
        if (_implementations.ContainsKey(key))
        {
            _currentImplementationKey = key;
        }
        else
        {
            throw new ArgumentException($"Implementation {key} not found");
        }
    }
    
    public string GetMessage()
    {
        // Создаем нужную реализацию налету
        var implementationType = _implementations[_currentImplementationKey];
        var implementation = (IMessageService)ActivatorUtilities
            .CreateInstance(_serviceProvider, implementationType);
            
        return implementation.GetMessage();
    }
}
Таким образом, мы можем менять реализации без перезапуска приложения:

C#
1
2
3
4
var messageService = serviceProvider.GetRequiredService<CompositeMessageService>();
 
// Переключаемся на premium-реализацию
messageService.SwitchImplementation("premium");
Более элегантное решение – использование сторонних DI-контейнеров, которые поддерживают динамическую реконфигурацию, таких как Autofac.

Асинхронная инициализация сервисов



Еще одна интересная проблема – как быть, если сервису для инициализации нужно выполнить асинхронную операцию? Стандартный конструктор не может быть асинхронным, что создаёт определённые сложности.
Один из подходов – использовать IHostedService для инициализации сервисов при запуске приложения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DataInitializerService : IHostedService
{
    private readonly DatabaseService _databaseService;
    
    public DataInitializerService(DatabaseService databaseService)
    {
        _databaseService = databaseService;
    }
    
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Асинхронная инициализация
        await _databaseService.InitializeAsync();
    }
    
    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}
 
// Регистрация
services.AddSingleton<DatabaseService>();
services.AddHostedService<DataInitializerService>();
Другой подход – использовать паттерн "Ленивая инициализация" с помощью Lazy<T>:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LazyDatabaseService
{
    private readonly Lazy<Task<DbConnection>> _connection;
    
    public LazyDatabaseService(IConfiguration configuration)
    {
        _connection = new Lazy<Task<DbConnection>>(async () => {
            var connection = new SqlConnection(configuration.GetConnectionString("Default"));
            await connection.OpenAsync();
            return connection;
        });
    }
    
    public async Task<DbConnection> GetConnectionAsync()
    {
        return await _connection.Value;
    }
}
Этот подход позволяет отложить тяжелую инициализацию до момента первого использования, что также может быть полезно для оптимизации времени запуска приложения.

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

Интеграция сторонних DI-контейнеров



Встроенный DI-контейнер ASP.NET Core вполне функционален для большинства приложений, но иногда его возможностей недостаточно. На помощь приходят сторонние контейнеры с расширенным функционалом: Autofac, Ninject, StructureMap, DryIoc и другие. ASP.NET Core спроектирован так, чтобы легко интегрироваться с ними.
Взять, например, Autofac – один из самых популярных контейнеров с богатым функционалом:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Установите пакеты:
// Autofac
// Autofac.Extensions.DependencyInjection
 
public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // Добавляем стандартные сервисы ASP.NET Core
        services.AddControllers();
        
        // Создаём контейнер Autofac
        var builder = new ContainerBuilder();
        
        // Регистрируем сервисы в Autofac
        builder.RegisterType<CustomService>().As<ICustomService>()
               .InstancePerLifetimeScope(); // Эквивалент AddScoped
        
        // Именованные регистрации (не поддерживается встроенным контейнером)
        builder.RegisterType<SqlOrderRepository>().Named<IOrderRepository>("sql");
        builder.RegisterType<MongoOrderRepository>().Named<IOrderRepository>("mongo");
        
        // Декорирование сервисов (не поддерживается встроенным контейнером)
        builder.RegisterType<BaseNotificationService>().As<INotificationService>();
        builder.RegisterDecorator<LoggingNotificationDecorator, INotificationService>();
        
        // Регистрация модулей
        builder.RegisterModule(new DataAccessModule());
        
        // Добавляем сервисы из стандартной коллекции в Autofac
        builder.Populate(services);
        
        // Строим контейнер и возвращаем его как IServiceProvider
        var container = builder.Build();
        return new AutofacServiceProvider(container);
    }
}
Что даёт нам использование Autofac вместо стандартного контейнера?

1. Именованные регистрации – когда нужно несколько реализаций одного интерфейса и выбирать между ними не по типу, а по имени.

C#
1
2
3
4
5
6
7
8
9
10
// Внедрение именованных сервисов
public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    
    public OrderProcessor([Named("mongo")] IOrderRepository repository)
    {
        _repository = repository;
    }
}
2. Декораторы – для добавления функциональности существующим сервисам без их модификации (принцип Open/Closed из SOLID).
3. Модули – позволяют группировать регистрации и переиспользовать в разных приложениях.
4. Сканирование сборок – автоматическая регистрация типов по определенным критериям:

C#
1
2
3
4
5
// Регистрируем все классы, реализующие IHandler в сборке
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
       .Where(t => t.IsAssignableTo<IHandler>())
       .AsImplementedInterfaces()
       .InstancePerDependency();
На одном из проектов мы перешли с встроенного контейнера на Autofac именно из-за необходимости использовать декораторы. Нам требовалось добавить логирование, кеширование и проверку прав доступа ко всем нашим сервисам, не модифицируя их код. С Autofac это заняло буквально пару часов, тогда как со встроенным контейнером пришлось бы писать фабрики для каждого сервиса.

Продвинутые паттерны проектирования с DI



DI – это не изолированный паттерн, он прекрасно сочетается с другими подходами к проектированию, усиливая их преимущества.

Паттерн "Декоратор" через DI



Мы уже упоминали декораторы с Autofac, но их можно реализовать и с встроенным контейнером:

C#
1
2
3
4
5
6
7
8
9
10
11
// Регистрируем базовую реализацию
services.AddScoped<IMailService, SmtpMailService>();
 
// Затем декорируем её
services.AddScoped<IMailService>(serviceProvider => {
    var originalService = serviceProvider.GetRequiredService<SmtpMailService>();
    return new LoggingMailServiceDecorator(
        originalService, 
        serviceProvider.GetRequiredService<ILogger<LoggingMailServiceDecorator>>()
    );
});
Я часто использую этот паттерн для добавления кросс-функциональных аспектов: логирования, кеширования, ретраев, проверок безопасности – без изменения основного кода.

Паттерн "Стратегия" с DI



Стратегия позволяет выбирать алгоритм на лету. С DI это становится особенно элегантно:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Регистрируем все стратегии
services.AddTransient<CreditCardPaymentStrategy>();
services.AddTransient<PayPalPaymentStrategy>();
services.AddTransient<CryptoPaymentStrategy>();
 
// Регистрируем фабрику стратегий
services.AddTransient<Func<string, IPaymentStrategy>>(serviceProvider => strategyName => {
    return strategyName switch {
        "credit_card" => serviceProvider.GetService<CreditCardPaymentStrategy>(),
        "paypal" => serviceProvider.GetService<PayPalPaymentStrategy>(),
        "crypto" => serviceProvider.GetService<CryptoPaymentStrategy>(),
        _ => throw new ArgumentException($"Unknown strategy: {strategyName}")
    };
});
 
// Использование
public class PaymentProcessor
{
    private readonly Func<string, IPaymentStrategy> _strategyFactory;
    
    public PaymentProcessor(Func<string, IPaymentStrategy> strategyFactory)
    {
        _strategyFactory = strategyFactory;
    }
    
    public void ProcessPayment(Order order)
    {
        var strategy = _strategyFactory(order.PaymentMethod);
        strategy.Process(order.Amount);
    }
}

Композиция через DI



DI помогает создавать композитные объекты, которые делегируют работу специализированным сервисам:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UserManager
{
    private readonly IUserValidator _validator;
    private readonly IPasswordHasher _hasher;
    private readonly IUserRepository _repository;
    private readonly IEmailService _emailService;
    
    public UserManager(
        IUserValidator validator,
        IPasswordHasher hasher,
        IUserRepository repository,
        IEmailService emailService)
    {
        _validator = validator;
        _hasher = hasher;
        _repository = repository;
        _emailService = emailService;
    }
    
    public async Task RegisterUser(string username, string password, string email)
    {
        // Каждый компонент делает своё дело
        _validator.ValidateNewUser(username, email);
        var hashedPassword = _hasher.HashPassword(password);
        
        var user = new User {
            Username = username,
            PasswordHash = hashedPassword,
            Email = email
        };
        
        await _repository.CreateUserAsync(user);
        await _emailService.SendWelcomeEmailAsync(email, username);
    }
}
Этот подход идеально подходит для бизнес-сервисов, оркестрирующих работу более специализированных компонентов.

Обработка ошибок и логирование в DI-контейнере



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

Обработка ошибок разрешения



ASP.NET Core предоставляет несколько способов для обработки ситуаций, когда сервис не удаётся разрешить:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Безопасное получение сервиса, возвращает null если сервис не найден
var service = serviceProvider.GetService<IMyService>();
if (service != null) {
    // Используем сервис
}
 
// GetRequiredService выбросит исключение, если сервис не найден
try {
    var service = serviceProvider.GetRequiredService<IMyService>();
    // Используем сервис
} catch (InvalidOperationException ex) {
    // Обрабатываем ошибку
    logger.LogError(ex, "Failed to resolve IMyService");
}
Для более сложных сценариев можно зарегистрировать фабрику, которая обрабатывает ошибки разрешения:

C#
1
2
3
4
5
6
7
8
9
10
services.AddTransient<IMyService>(sp => {
    try {
        // Пытаемся получить предпочтительную реализацию
        return sp.GetService<OptimalServiceImplementation>();
    }
    catch {
        // В случае ошибки используем запасной вариант
        return new FallbackServiceImplementation();
    }
});

Логирование операций DI-контейнера



Диагностика проблем с DI может быть непростой. Полезно включить логирование:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public void ConfigureServices(IServiceCollection services)
{
    services.AddLogging(builder => {
        builder.AddConsole();
        builder.AddDebug();
        
        // Установите минимальный уровень логирования для Microsoft.Extensions.DependencyInjection
        builder.AddFilter("Microsoft.Extensions.DependencyInjection", LogLevel.Debug);
    });
    
    // Остальные регистрации...
}
В сложных проектах я иногда создаю специальные диагностические сервисы для анализа DI-контейнера:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class DiagnosticsTool
{
    private readonly IServiceProvider _serviceProvider;
    
    public DiagnosticsTool(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public bool CanResolve<T>()
    {
        try {
            _serviceProvider.GetService<T>();
            return true;
        } catch {
            return false;
        }
    }
    
    public void LogAllRegisteredServices(ILogger logger)
    {
        // В стандартном контейнере нет прямого доступа к регистрациям
        // С Autofac или другими контейнерами это было бы проще
        logger.LogInformation("Starting service resolution diagnostics");
        
        // Проверяем известные сервисы
        CheckAndLogService<IUserService>("IUserService");
        CheckAndLogService<IOrderService>("IOrderService");
        // ... и так далее
        
        void CheckAndLogService<T>(string serviceName)
        {
            try {
                var service = _serviceProvider.GetService<T>();
                logger.LogInformation(
                    "Service {ServiceName} resolved successfully. Implementation: {Implementation}",
                    serviceName,
                    service?.GetType().FullName ?? "null"
                );
            } catch (Exception ex) {
                logger.LogError(
                    ex,
                    "Failed to resolve {ServiceName}",
                    serviceName
                );
            }
        }
    }
}
На практике такие инструменты особенно полезны при отладке сложных DI-конфигураций или при работе с унаследованными проектами.

Оптимизация производительности DI в высоконагруженных системах



DI-контейнер, как ни странно, может стать узким местом производительности. Когда приложение обрабатывает тысячи запросов в секунду, даже небольшие накладные расходы на резолвинг зависимостей могут превратиться в заметную проблему.
Несколько лет назад я столкнулся с системой, которая тратила почти 15% времени обработки запроса только на разрешение зависимостей. Первое, что мы сделали — оптимизировали граф зависимостей:

C#
1
2
3
4
5
6
7
8
9
// Было: длинная цепочка зависимостей
services.AddScoped<IUserValidator>();
services.AddScoped<IUserRepository>(); 
services.AddScoped<IUserService>();
services.AddScoped<IUserPermissionService>();
services.AddScoped<IUserController>();
 
// Стало: схлопнутые зависимости для критичного пути
services.AddScoped<IUserFacade>(); // Фасад объединяет несколько сервисов
Правильное время жизни сервисов тоже играет огромную роль. Вместо оголтелого использования Transient для всего подряд мы перевели неизменяемые сервисы без состояния в Singleton:

C#
1
2
// Этот сервис не хранит состояния и не меняется — идеальный кандидат в Singleton
services.AddSingleton<IDataValidator, DataValidator>();
В особо критичных местах мы применили технику предварительного разрешения. Вместо того чтобы позволять контейнеру резолвить глубокие графы зависимостей на каждый запрос, мы создавали финальные объекты заранее:

C#
1
2
3
4
5
6
7
8
9
10
11
// Создаем и кешируем фабрику контроллеров при старте приложения
services.AddSingleton<Func<ControllerContext, UserController>>(sp => 
{
var userService = sp.GetRequiredService<IUserService>();
var logger = sp.GetRequiredService<ILogger<UserController>>();
 
return (context) => new UserController(userService, logger) 
{
    ControllerContext = context
};
});
Самый радикальный приём — но и самый эффективный для по-настоящему высоконагруженных систем — это вообще не использовать DI-контейнер для критических путей. Вместо этого можно применить паттерн "Амбиентный контекст":

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class ServiceLocator
{
private static readonly ConcurrentDictionary<Type, object> _services = new();
 
// Инициализация при старте
public static void Initialize(IServiceProvider provider)
{
    // Резолвим и сохраняем только нужные сервисы
    _services[typeof(IUserRepository)] = provider.GetRequiredService<IUserRepository>();
    // ...другие нужные сервисы
}
 
// Быстрый доступ без накладных расходов DI
public static T Get<T>() where T : class
{
    return _services.TryGetValue(typeof(T), out var service) 
        ? (T)service 
        : throw new InvalidOperationException($"Service {typeof(T).Name} not registered");
}
}
Да, это нарушает многие принципы чистой архитектуры, но это осознаный компромис для тех случаев, когда производительность критически важна.

Внедрение зависимостей в middleware-компоненты



Middleware-компоненты в ASP.NET Core представляют особый случай для DI. Они не создаются напрямую через контейнер, а инстанциируются через делегаты. При этом им часто нужен доступ к другим сервисам.
Стандартный подход для middleware с зависимостями выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class CustomMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CustomMiddleware> _logger;
private readonly IConfiguration _config;
 
// Получаем зависимости через конструктор
public CustomMiddleware(RequestDelegate next, ILogger<CustomMiddleware> logger, IConfiguration config)
{
    _next = next;
    _logger = logger;
    _config = config;
}
 
public async Task InvokeAsync(HttpContext context)
{
    _logger.LogInformation("Request incoming: {Path}", context.Request.Path);
    
    // Делаем что-то с контекстом
    context.Items["Timestamp"] = DateTime.UtcNow;
    
    // Передаем управление следующему middleware
    await _next(context);
}
}
 
// Регистрация через метод расширения
public static class CustomMiddlewareExtensions
{
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app)
{
    return app.UseMiddleware<CustomMiddleware>();
}
}

ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...

ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS
Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует...

ASP.NET MVC или ASP.NET Core
Добрый вечер, подскажите что лучшие изучать ASP.NET MVC или ASP.NET Core ? Как я понимаю ASP.NET...

Что выбрать ASP.NET или ASP.NET Core ?
Добрый день форумчане, хотелось бы услышать ваше мнение, какой из перечисленных фреймворков лучше...

ASP.NET Core или ASP.NET MVC
Здравствуйте После изучение основ c# я решил выбрать направление веб разработки. Подскажите какие...

Стоит ли учить asp.net, если скоро станет asp.net core?
Всем привет Если я правильно понимаю, лучше учить Core ?

ASP.NET или ASP.NET Core
Добрый вечер, подскажите новичку в чем разница между asp.net и asp.net core, нужно ли знать оба...

Почему скрипт из ASP.NET MVC 5 не работает в ASP.NET Core?
В представлении в версии ASP.NET MVC 5 был скрипт: @model RallyeAnmeldung.Cars ...

Объясните про внедрение зависимостей в .net core. Разложите все по полочкам
Если сервис зарегистрировать как singleton (AddSingleton). То он будет один для всех пользователей?...

Asp.net core rc 2 и Entity Framework core
Добрый день, кто-нибудь уже перешел на новую версию фреймверка? Хотелось бы получить пример. ...

ASP.NET Core + EF Core: ошибка при обновлении БД после создания миграции
Всем привет! Начал осваивать ASP.NET Core: создал проект &quot;Веб-приложение&quot; без Identity. Сразу же...

Пагинация. Как установить колличество позиций на странице? Razor Pages с EF Core в ASP.NET Core
Изучаю учебник - Razor Pages с Entity Framework Core в ASP.NET Core // docs.microsoft.com/ru-ru/ ...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Когда логика программы не спасает от человеческих ошибок
Maks 18.08.2026
В последнее время всё чаще и чаще сталкиваюсь с таким явлением, как абсолютная невнимательность (или глупость) пользователей. Проявляется это чаще всего на работе в коллективе. Допустим, человек с. . .
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru