Содержание
Разработанные нами сервисы можно запрашивать различными способами. До сих пор, во всех предыдущих примерах, использовался только один из способов получения сервиса — через конструктор класса, в котором планируется использование сервиса. В этой части рассмотрим все доступные способы получения сервисов в ASP.NET Core Web API.
Service Locator Pattern (локатор сервисов)
Этот шаблон также называют шаблоном обнаружения служб. Для получения ссылки на сервис используется метод GetService
интерфейса IServiceProvider
. Сами разработчики из Microsoft рекомендуют не использовать service locator, если возможны другие способы получения зависимости, а некоторые разработчике не из Microsoft даже называют service locator антипаттерном, аргументируя это тем, что service locator нарушает принципы SOLID и инкапсуляцию, затрудняет поддержку и т.д. Мы не будем теоретизировать по поводу того паттерн ли это или антипаттерн, а просто рассмотрим пример, когда без service locator обойтись невозможно.
Свойство HttpContext.RequestServices
Воспользуемся уже имеющимся у нас компонентом middleware для проверки токена. Код приложения (без использования сервисов):
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); //псевдо-БД с действительными значениями токенов string[] valid_tokens = ["12345", "abcd", "password"]; // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.Use(CheckToken); app.MapControllers(); app.Run(); async Task CheckToken(HttpContext context, RequestDelegate next) { //пробуем получить значение токена из запроса пользователя var token = context.Request.Query["token"];//пытаемся получить токен из параметров URI if (string.IsNullOrWhiteSpace(token)) { token = context.Request.Headers["X-Auth"];//пытаемся получить токен из заголовка X-Auth } if (string.IsNullOrWhiteSpace(token)) //не смогли получить токен context.Response.StatusCode = 401; //возвращаем код 401 пользователю else //получили токен - необходимо проверить, что токен действителен { if (valid_tokens.Contains<string>(token)) await next(context); //токен верный - пропускаем запрос дальше до следующего middleware else context.Response.StatusCode = 419; //419 - Authentication Timeout } }
В данном случае, наш компонент middleware представлено методом CheckToken()
. Теперь, допустим, что нам потребовалось в этом компоненте middleware не только произвести проверку, но и передать далее по конвейеру хэш переданного токена. Для генерации хэша у нас уже также имеется сервис:
public interface IHash { public string GetHash(string data); } public class MD5Hash : IHash { public string GetHash(string data) { return Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(data))); } } public class SHA1Hash : IHash { public string GetHash(string data) { return Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(data))); } }
Который мы регистрируем в нашем приложении:
builder.Services.AddTransient<IHash, MD5Hash>();
и хотим получить сервис в методе CheckToken()
. В данном случае, единственно возможным способом получить сервис — это использовать service locator:
async Task CheckToken(HttpContext context, RequestDelegate next) { var token = context.Request.Query["token"]; if (string.IsNullOrWhiteSpace(token)) { token = context.Request.Headers["X-Auth"]; } if (string.IsNullOrWhiteSpace(token)) context.Response.StatusCode = 401; else { if (valid_tokens.Contains<string>(token)) { var svc = context.RequestServices.GetService<IHash>();//запрашиваем сервис context.Items.Add("hash", svc.GetHash(token));//записываем хэш токена await next(context); } else context.Response.StatusCode = 419; } }
Здесь мы воспользовались свойством HttpContext.RequestServices
и, вызвав метод GetService()
запросили необходимый нам сервис.
В ASP.NET Core имеется несколько методов GetService()
. В нашем случае — это один из методов расширения для IServiceProvider
, который возвращает экземпляр сервиса или null
, если сервис не обнаружен:
public static class ServiceProviderServiceExtensions { public static T? GetService<T>(this IServiceProvider provider) { ThrowHelper.ThrowIfNull(provider); return (T?)provider.GetService(typeof(T)); } }
У самого IServiceProvider
также имеется одноименный метод, работающий аналогичным образом.
Если нам необходимо генерировать исключение в случае отсутствия необходимого сервиса, то можно воспользоваться методом расширения GetRequiredService()
:
var svc = context.RequestServices.GetRequiredService<IHash>();//запрашиваем сервис
Этот метод сгенерирует исключение InvalidOperationException
, если требуемый сервис не будет зарегистрирован к контейнере.
Свойство WebApplication.Services
Ещё один способ задействовать service locator — воспользоваться свойством WebApplication.Services
где доступен объект этого класса, например, в файле Program.cs. Перепишем наш пример следующим образом:
using WebApplication2.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddTransient<IHash, MD5Hash>(); //регистрируем сервис var app = builder.Build(); var svc = app.Services.GetService<IHash>(); //запрашиваем сервис //псевдо-БД с действительными значениями токенов string[] valid_tokens = ["12345", "abcd", "password"]; if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.Use(CheckToken); //используем middleware app.MapControllers(); app.Run(); async Task CheckToken(HttpContext context, RequestDelegate next) { var token = context.Request.Query["token"]; if (string.IsNullOrWhiteSpace(token)) { token = context.Request.Headers["X-Auth"]; } if (string.IsNullOrWhiteSpace(token)) context.Response.StatusCode = 401; else { if (valid_tokens.Contains<string>(token)) { //var svc = context.RequestServices.GetService<IHash>();//запрашиваем сервис context.Items.Add("hash", svc.GetHash(token));//записываем хэш токена await next(context); } else context.Response.StatusCode = 419; } }
По сути, в этом примере мы также задействуем методы расширения IServiceProvider
, но, при этом, используем свойство WebApplication.Services
в момент, когда у нас ещё нет доступа к контексту запроса.
Рассмотренные выше примеры показывают возможность использования service locator для получения зависимостей в компонентах middleware. Однако, как мы уже знаем, компоненты middleware можно разрабатывать различными способами. Посмотрим, как мы можем запрашивать наши сервисы в компонентах middleware без использования service locator.
Запрос сервисов в компонентах middleware без использования service locator
Методы Invoke/InvokeAsync
При создании компонентов middleware с активацией по соглашению запросить необходимый для работы компонента сервис можно через параметры метода Invoke
/InvokeAsync
, например так:
public class CheckTokenMiddleware { //псевдо-БД с действительными значениями токенов private readonly string[] valid_tokens = ["12345", "abcd", "password"]; private readonly RequestDelegate _next; public CheckTokenMiddleware(RequestDelegate next) { _next = next; } //в методе запрашивается сервис IHash public async Task InvokeAsync(HttpContext context, IHash hash) { var token = context.Request.Query["token"]; if (string.IsNullOrWhiteSpace(token)) { token = context.Request.Headers["X-Auth"]; } if (string.IsNullOrWhiteSpace(token)) context.Response.StatusCode = 401; else { if (valid_tokens.Contains<string>(token)) { context.Items.Add("hash", hash.GetHash(token));//записываем хэш токена await _next(context); } else context.Response.StatusCode = 419; } } }
Технически, так как у нас есть доступ к контексту запроса, то мы могли бы как и в предыдущих примерах использовать service locator, но здесь мы пошли более предпочтительным путём запроса зависимости и запросили сервис через параметры метода:
public async Task InvokeAsync(HttpContext context, IHash hash)
Конструктор компонента при использовании фабричного класса middleware
При использовании строгой типизации запрос зависимости в фабричном классе middleware производится в конструкторе класса следующим образом:
public class CheckTokenMiddleware: IMiddleware { //псевдо-БД с действительными значениями токенов private readonly string[] valid_tokens = ["12345", "abcd", "password"]; private readonly IHash _hash; public CheckTokenMiddleware(IHash hash) { _hash = hash; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var token = context.Request.Query["token"]; if (string.IsNullOrWhiteSpace(token)) { token = context.Request.Headers["X-Auth"]; } if (string.IsNullOrWhiteSpace(token)) context.Response.StatusCode = 401; else { if (valid_tokens.Contains<string>(token)) { context.Items.Add("hash", _hash.GetHash(token));//записываем хэш токена await next(context); } else context.Response.StatusCode = 419; } } }
Так как при использовании фабричных классов middleware мы не можем передавать в такие компоненты параметры, то запрос зависимости через конструктор такого компонента является тем самым путем, когда мы можем получать внутри такого компонента какие-либо данные извне. Например, для рассматриваемого компонента, помимо сервиса IHash
мы могли бы создать ещё один сервис, который бы получал из нормальной базы данных сведения о доступных в приложении токенах.
Запрос сервисов в контроллерах
Конструктор контроллера
Внутри контроллера у нас имеется доступ к контексту запроса через свойство HttpContext
, то мы также как и в компонентах middleware могли бы пользоваться service locator, однако рекомендуемым является способ запроса зависимости именно через конструктор. Например, в контроллере WeatherForecastController
, который создается по умолчанию при создании нового проекта ASP.NET Core Web API уже продемонстрирован этот способ:
public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; }
В данном случае, в контроллере запрашивается зависимость — сервис логирования ILogger<WeatherForecastController>
, который мы можем в дальнейшем использовать в действиях контроллера.
Атрибуты FromService/FromKeyedService
Иногда бывает так, что сервис необходимо использовать только в одном месте (или нескольких местах) контроллера и нет необходимости для этого заводить в классе контроллера отдельное поле. В этом случае, мы можем использовать атрибут FromService
для получения сервиса непосредственно через параметры метода контроллера:
public IEnumerable<WeatherForecast> Get([FromServices] IHash hash) { var hashStrinng = hash.GetHash("hello, world"); //прочий код метода }
В случае, если нам необходимо использовать сервис с ключом, то необходимо использовать атрибут FromKeyedService
, например, так:
public IEnumerable<WeatherForecast> Get([FromKeyedServices("service_key")] IHash hash) { var hashStrinng = hash.GetHash("hello, world"); //прочий код метода }
В данном случае, строка service_key
является ключом сервиса IHash
.
Итого
В зависимости от того, в какой части приложения нам требуется использование сервиса, мы можем применять различные способы их запроса. Так, если необходимо запросить сервис внутри компонента middleware, то мы можем использовать service locator, запросить сервис через параметры методов Invoke
/InvokeAsync
или через конструктор класса, если используется фабричный класс компонента middleware. В случае необходимости запроса зависимости внутри контроллера мы можем использовать также service locator, запрос в конструкторе контроллера или с использованием атрибутов FromService
/FromKeyedService
. Использование шаблона service locator считается наименее рекомендуемым способом запроса зависимостей, однако, в случае компонента middleware, которой представляет собой обычный метод, service locator является единственно возможным способом получения зависимости внутри этого метода на текущий момент развития ASP.NET Core.