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

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

Свойство Services объекта WebApplication

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

var svc = app.Services.GetService<Hash>();//получаем сервис

Посмотрим, что представляет из себя это свойство:

public IServiceProvider Services => _host.Services;

Интерфейс IServiceProvider определяет механизм для извлечения объекта сервиса, в частности — это метод

public object? GetService (Type serviceType);

который возвращает нам объект сервиса или null, если сервис не зарегистрирован. Также имеется метод расширения для IServiceProvider :

public static object GetRequiredService (this IServiceProvider provider, Type serviceType);
public static T GetRequiredService<T> (this IServiceProvider provider);

В отличие от предыдущего метода, GetRequiredService, в случае, если сервис не зарегистрирован или был удален, генерирует исключение типа InvalidOperationException.

Оба этих метода, по сути, реализуют т.н. шаблон обнаружения сервисов (service locator). При этом, сами разработчики платформы .NET не рекомендуют использовать этот шаблон для получения зависимостей (сервисов) и, по возможности, получать сервисы другими способами. Посмотрим, какие ещё способы получения зависимостей мы можем использовать.

Свойство HttpContext.RequestServices

По сути, свойство RequestServices — это тот же IServiceProvider, то есть при использовании этого свойства мы также будем использовать service locator, но, возможно, что в тех местах приложения, где нам доступен контекст запроса, например, в компонентах middleware этот способ окажется единственным для получения сервиса. С использованием свойства HttpContext.RequestServices мы могли бы получить сервис следующим образом:

var svc = context.RequestServices.GetService<Hash>();

или

var svc = context.RequestServices.GetRequiredService<Hash>();

Конструкторы классов

Система DI ASP.NET Core использует конструкторы классов для передачи всех зависимостей. Этот способ передачи сервисов можно считать наиболее предпочтительным и именно этот способ мы будем, в дальнейшем, чаще всего использовать в работе над приложениями. Рассмотрим как это работает на примере сервиса, разработанного в прошлой статье. Напомню как выглядел этот сервис (для упрощения, возьмем в качестве сервиса конкретный класс):

public class Hash
{
    public string GetMD5(string data)
    {
        return Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(data)));
    }

    public string GetSHA256(string data)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.ASCII.GetBytes(data)));
    }
}

и метод расширения для его регистрации:

public static class ServiceProviderExtensions
{
    public static void AddHash(this IServiceCollection services)
    {
        services.AddTransient<Hash>();
    }
}

Всё, что делает наш сервис — это с помощью двух методов рассчитывает хэши MD5 и SHA-256. Теперь создадим следующим класс, который также будет выступать в роли сервиса:

public class HashMessage
{
    private Hash _service;

    public HashMessage(Hash service)
    {
        _service = service;
    }

    public string HtmlMessage(string data)
    {
        return $"<b>MD5</b>: {_service?.GetMD5(data)} </br> <b>SHA-256</b>: {_service?.GetSHA256(data)}";
    }

    public string PlainTextMessage(string data)
    {
        return $"MD5: {_service?.GetMD5(data)} \r\n SHA-256: {_service?.GetSHA256(data)}";
    }
}

Здесь мы в конструкторе класса передаем зависимость от нашего сервиса Hash. Аналогичным образом мы могли бы передать в качестве параметра интерфейс (об этом чуть ниже). Теперь добавим в наше приложение новый сервис:

builder.Services.AddTransient<HashMessage>();

Но так как класс HashMessage использует зависимость Hash, которая передается через конструктор, то нам надо также установить и эту зависимость:

builder.Services.AddHash();

И теперь произойдет «магия» DI в ASP.NET Core — все зависимости между сервисами будут установлены и нам даже не придется создавать объект класса Hash. Вот как мы можем воспользоваться нашим новым сервисом:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);

    builder.Services.AddTransient<HashMessage>();
    builder.Services.AddHash();

    var app = builder.Build();

    app.Run(async context =>
    {
        var svc = context.RequestServices.GetRequiredService<HashMessage>();
        var request = context.Request;
        var data = request.Query["data"].ToString();
        
        if (string.IsNullOrEmpty(data))
            await context.Response.WriteAsync("Empty data");
        else
        {
            await context.Response.WriteAsync(svc?.HtmlMessage(data));
        }
    });

    app.Run();
}

Запустим приложение и убедимся, что сервис работает и информация о хэшах выводится в формате HTML

Теперь рассмотрим пример, в котором будет использоваться интерфейс. Рассмотрим следующий пример:

public interface IHashCalculator
{
    public string HashType { get; }
    public string GetHash(string data);
}

public class MD5Hash : IHashCalculator
{
    public string HashType => "MD5";

    public string GetHash(string data)
    {
        return Convert.ToHexString(MD5.HashData(Encoding.ASCII.GetBytes(data)));
    }
}

public class SHA256Hash : IHashCalculator
{
    public string HashType => "SHA-256";

    public string GetHash(string data)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.ASCII.GetBytes(data)));
    }
}

public class HashMessage
{
    private IHashCalculator _service;

    public HashMessage(IHashCalculator service)
    {
        _service = service;
    }

    public string HtmlMessage(string data)
    {
        return $"<b>{_service.HashType}</b>: {_service?.GetHash(data)}";
    }

    public string PlainTextMessage(string data)
    {
        return $"{_service.HashType}: {_service?.GetHash(data)}";
    }
}


public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddTransient<HashMessage>();
        builder.Services.AddTransient<IHashCalculator, SHA256Hash>();
        builder.Services.AddTransient<IHashCalculator, MD5Hash>();
        

        var app = builder.Build();

        app.Run(async context =>
        {
            var svc = context.RequestServices.GetRequiredService<HashMessage>();
            var request = context.Request;
            var data = request.Query["data"].ToString();
            
            if (string.IsNullOrEmpty(data))
                await context.Response.WriteAsync("Empty data");
            else
            {
                await context.Response.WriteAsync(svc?.HtmlMessage(data));
            }
        });

        app.Run();
    }
}

Здесь у нас определен интерфейс IHashCalculator и два класса, реализующих его — MD5Hash и SHA256Hash. Обратите внимание на то, как мы регистрируем зависимости в контейнере DI:

builder.Services.AddTransient<HashMessage>(); 
builder.Services.AddTransient<IHashCalculator, SHA256Hash>(); 
builder.Services.AddTransient<IHashCalculator, MD5Hash>();

Что касалось примера с классами, то там всё было предельно понятно — есть класс вычисления хэшей и класс, формирующий строку на основании полученных хэшей. А каким будет поведение ASP.NET Core в случае, когда в класс HashMessage передается не конкретный класс, а интерфейс и несколько его реализаций — какая реализация будет выбрана?

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

Метод Invoke/InvokeAsync компонентов middleware

Когда мы создавали классы middleware, то затрагивали вопросы передачи параметров в эти классы. Собственно, в нашем случае, в качестве параметра будет выступать зависимость. Создадим класс middleware, в метод InvokeAsync которого будет передаваться сервис:

public class HashMiddleware
{
    public HashMiddleware(RequestDelegate _)
    {
  
    }

    public async Task InvokeAsync(HttpContext context, HashMessage service) 
    {
        context.Response.ContentType = "text/html; charset=utf-8";
        var request = context.Request;
        var data = request.Query["data"].ToString();

        if (string.IsNullOrEmpty(data))
            await context.Response.WriteAsync("Empty data");
        else
        {
            await context.Response.WriteAsync(service?.HtmlMessage(data));
        }
    }
}

Здесь в метод InvokeAsync мы передаем вторым параметром сервис HashMessage. Теперь напишем метод расширения для встраивания этого middleware в конвейер запроса:

public static class HashMiddlewareExtensions
{
    public static IApplicationBuilder UseHash(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HashMiddleware>();
    }
}

И используем его:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddTransient<HashMessage>();
        builder.Services.AddTransient<IHashCalculator, SHA256Hash>();

        var app = builder.Build();

        app.UseHash();

        app.Run();
    }
}

Так как в реализацией IHashCalculator является SHA256Hash, то в приложении мы увидим:

Также никто не запрещает передавать в middleware сервис через конструктор класса. Например так:

public static class HashMiddlewareExtensions
{
    public static IApplicationBuilder UseHash(this IApplicationBuilder builder, HashMessage service)
    {
        return builder.UseMiddleware<HashMiddleware>(service);
    }
}

public class HashMiddleware
{
    HashMessage _service;

    public HashMiddleware(RequestDelegate _, HashMessage service)
    {
        _service = service;
    }

    public async Task InvokeAsync(HttpContext context) 
    {
        context.Response.ContentType = "text/html; charset=utf-8";
        var request = context.Request;
        var data = request.Query["data"].ToString();

        if (string.IsNullOrEmpty(data))
            await context.Response.WriteAsync("Empty data");
        else
        {
            await context.Response.WriteAsync(_service?.HtmlMessage(data));
        }
    }
}

Правда, при этом, нам придется задействовать service locator в приложении так как встраивание middleware в конвейер будет таким:

app.UseHash(app.Services.GetRequiredService<HashMessage>());

поэтому, всё же, по возможности, лучше использовать первый вариант передачи сервиса в middleware через методы Invoke/InvokeAsync.

Итого

Сегодня мы рассмотрели несколько вариантов того, каким образом можно получить ту или иную зависимость (сервис) в ASP.NET Core. Предпочтительным вариантом получения зависимостей является их передача через конструктор класса или, если мы передаем зависимость в middleware — через методы Invoke и InvokeAsync так как в этом случае нам не приходится задействовать в работе service locator.

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