ASP.NET Core Web API. Способы получения сервисов

Разработанные нами сервисы можно запрашивать различными способами. До сих пор, во всех предыдущих примерах, использовался только один из способов получения сервиса — через конструктор класса, в котором планируется использование сервиса. В этой части рассмотрим все доступные способы получения сервисов в 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.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии