Инъекция зависимостей (DI) помогает нам изменять поведение частей нашего приложения на лету. Это особенно удобно, когда вы хотите протестировать свои доменные сервисы на подражаемом хранилище данных. Но что если вам нужно изменить поведение вашего API на основе заголовка запроса?
Вчера я обсуждал эту тему с моим коллегой Робертом Краненбургом. Он показал пример консольного приложения, изменяющего свое поведение в зависимости от аргумента. Я взял эту идею и преобразовал ее в код .NET Core 3.1 для Web API.
- Давайте изменим сообщение
- (3) типы времени жизни
- Доступ к заголовкам запроса
- конфигурация DI
- Результат
- Заключительные мысли
- Дальнейшее чтение
Давайте изменим сообщение
Наша цель: изменить сообщение ответа в зависимости от наличия cookie с именем «hidden», используя инъекцию зависимостей.
Сначала нам нужно определить контроллер и интерфейс сервиса:
public interface IMessageService
{
string GetMessage();
}
[ApiController]
[Route("")]
public class MyController : ControllerBase
{
private readonly IMessageService _messageService;
public MyController(IMessageService messageService)
{
_messageService = messageService;
}
[HttpGet]
public string Get()
{
return _messageService.GetMessage();
}
}
.NET Core использует инъекцию конструктора и предоставит экземпляр для каждого параметра конструктора. Он будет инжектировать экземпляр IMessageService
в контроллер.
Теперь давайте определим 2 реализации IMessageService
:
Давайте посмотрим код:
public class DefaultMessageService : IMessageService
{
public string GetMessage() => "Hello world!";
}
public class HiddenMessageService : IMessageService
{
private readonly ISecretKey _key;
public HiddenMessageService(ISecretKey key)
{
_key = key;
}
public string GetMessage() =>
"The answer to life the universe and everything: " +
_key.GetKey();
}
public interface ISecretKey
{
public string GetKey();
}
public class SecretKey : ISecretKey
{
public string GetKey() => "42";
}
Прежде чем мы настроим инъекцию зависимостей, давайте углубимся во время жизни.
3 типа времени жизни
Служба DI добавляется со временем жизни:
Поскольку мы хотим изменять поведение на основе информации о запросе, имеет смысл использовать scoped service.
Доступ к заголовкам запросов
В старые добрые времена мы могли делать все, что хотели, со статическим HttpContext.Current
и на этом все заканчивалось. В .NET Core мы используем IHttpContextAccessor
и внедрение зависимостей для взаимодействия с HttpContext
. Мы можем использовать AddHttpContextAccessor
, чтобы настроить это.
Конфигурация DI
Перейдем к файлу Startup.cs
и настроим инъекцию зависимостей:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpContextAccessor();
services.AddTransient<DefaultMessageService>();
services.AddTransient<ISecretKey, SecretKey>();
services.AddTransient<HiddenMessageService>();
services.AddScoped<IMessageService>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>();
var isHidden = context.HttpContext?.Request.Cookies.ContainsKey("hidden") == true;
if (isHidden)
{
return provider.GetRequiredService<HiddenMessageService>();
}
return provider.GetRequiredService<DefaultMessageService>();
});
}
Почему мы добавляем DefaultMessageService
и HiddenMessageService
без интерфейсов в коллекцию сервисов? Это помогает нам использовать DI в самих классах. HiddenMessageService
нуждается в этом, потому что именно так мы получаем экземпляр ISecretKey
. Добавление этих классов значительно облегчит нам жизнь, когда нам понадобится их инстанцировать. Если бы мы «создавали» их сами, нам пришлось бы решать все зависимости, что привело бы к большому количеству ненужного кода.
IMessageService
создается через фабричный метод. Этот метод получает IServiceProvider
в качестве входных данных, чтобы найти все необходимые сервисы. Мы используем этот провайдер, чтобы найти HTTP-доступ и проверить, присутствует ли cookie с именем hidden. Если да, мы разрешаем IMessageService
как HiddenMessageService
; в противном случае как DefaultMessageService
.
Результат
Сложив код вместе, мы получаем следующее поведение:
Содержимое сообщения изменяется в зависимости от наличия cookie с именем hidden.
Заключительные мысли
Легко изменить поведение вашей программы на основе заголовка запроса или cookie. Но все может быстро запутаться, если вы используете инъекцию зависимостей. Стартовые классы быстро становятся большими, когда вам нужно внедрить много классов.
Чтобы упростить работу, можно использовать метод расширения, например, такой:
public static class HiddenCookieExtensions
{
public static IServiceCollection AddScopedByHiddenCookie<TService, THiddenImplementation, TDefaultImplementation>(this IServiceCollection services)
where TService: class
where THiddenImplementation: TService
where TDefaultImplementation: TService
{
services.AddScoped<TService>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>();
var isHidden = context?.HttpContext?.Request.Cookies.ContainsKey("hidden") == true;
if (isHidden)
{
return provider.GetRequiredService<THiddenImplementation>();
}
return provider.GetRequiredService<TDefaultImplementation>();
});
return services;
}
}
Который можно использовать следующим образом:
services.AddScopedByHiddenCookie<IMessageService, HiddenMessageService, DefaultMessageService>();
В этом примере мы использовали cookie, но любой заголовок может быть использован для изменения поведения вашего приложения.
Дополнительное чтение
Работая над этой темой, я нашел несколько отличных источников для чтения:
- Инъекция зависимостей в ASP.NET Core
- .NET Core Dependency Injection Lifetimes Explained
- Инъекция зависимостей (с IOptions) в консольных приложениях в .NET Core