Middleware со строгой типизацией в ASP.NET Core

  • Михаил
  • 12 мин. на прочтение
  • 114
  • 24 Nov 2022
  • 24 Nov 2022

Большинство разработчиков ASP.NET Core знакомы с традиционным способом создания классов ПО промежуточного слоя, основанным на соглашениях. В подавляющем большинстве примеров используется подход, основанный на соглашениях. Но также доступен строго типизированный подход, основанный на реализации IMiddleware интерфейса. Кажется, это один из самых сокровенных секретов ASP.NET Core, поэтому я подумал, что стоит отдернуть занавески и позволить пролить свет на это.

 

Во-первых, давайте напомним себе, как работает промежуточное ПО, основанное на соглашениях. Два соглашения, которые должны быть применены к основанному на соглашениях классу ПО промежуточного слоя, заключаются в объявлении конструктора, принимающего a RequestDelegate в качестве параметра, представляющего следующее ПО промежуточного слоя в конвейере, и метода с именем Invoke или InvokeAsync, который возвращает a Taskи имеет по крайней мере один параметр, первый будучи HttpContext.

Следующий код иллюстрирует класс промежуточного программного обеспечения, который реализует эти соглашения и регистрирует значение IP-адреса посетителя:
 

public class ConventionalIpAddressMiddleware
{
   private readonly RequestDelegate _next;
   public ConventionalIpAddressMiddleware(RequestDelegate next) => _next = next;

   public async Task InvokeAsync(HttpContext context, ILogger<ConventionalIpAddressMiddleware> logger)
   {
       var ipAddress = context.Connection.RemoteIpAddress;
       logger.LogInformation("Visitor is from {ipAddress}", ipAddress);
       await _next(context);
   }
}     
  • Конструктор принимает в RequestDelegate качестве параметра
  • Метод InvokeAsync возвращает задачу и имеет HttpContext в качестве первого параметра. Любые дополнительные сервисы внедряются в Invoke/InvokeAsync метод послеHttpContext
  • Обработка происходит внутри InvokeAsync метода
  • Вызывается RequestDelegate, передавая управление следующему промежуточному программному обеспечению в конвейере.

Класс промежуточного программного обеспечения добавляется в конвейер с помощью универсального UseMiddleware метода IApplicationBuilder:

app.UseMiddleware<ConventionalIpAddressMiddleware>();

В этом конкретном случае вы, вероятно, захотите зарегистрировать это промежуточное ПО после промежуточного ПО для статических файлов, чтобы оно не регистрировало IP-адрес одного и того же посетителя для каждого запрошенного файла.

ПО промежуточного слоя, которое следует подходу, основанному на соглашениях, создается как одноэлементное при первом запуске приложения, а это означает, что на все время существования приложения создается только один экземпляр. Этот экземпляр используется для каждого запроса, который достигает его. Если ваше промежуточное ПО опирается на ограниченные или временные зависимости, вы должны внедрить их через Invoke метод, чтобы они разрешались каждый раз, когда метод вызывается платформой. Если вы внедрите их через конструктор, они будут захвачены как singleton. Регистратор в этом примере сам по себе является singleton, поэтому его можно предоставить через Invoke метод или конструктор.

Внедрение IMiddleware

Альтернативный подход к написанию классов промежуточного ПО включает реализацию IMiddlewareинтерфейса. Интерфейс IMiddlewareпредоставляет один метод:

Task InvokeAsync(HttpContext context, RequestDelegate next)

Вот IpAddressMiddleware, реализованный как IMiddleware:

public class IMiddlewareIpAddressMiddleware : IMiddleware
{
   private readonly ILogger<IMiddlewareIpAddressMiddleware> _logger;

   public IMiddlewareIpAddressMiddleware(ILogger<IMiddlewareIpAddressMiddleware> logger)
   {
       _logger = logger;
   }
   public async Task InvokeAsync(HttpContext context, RequestDelegate next)
   {
       var ipAddress = context.Connection.RemoteIpAddress;
       _logger.LogInformation("Visitor is from {ipAddress}", ipAddress);
       await next(context);
   }
}
  • Класс промежуточного программного обеспечения реализует IMiddleware интерфейс
  • Зависимости внедряются в конструктор
  • InvokeAsync принимает HttpContext и в RequestDelegate качестве параметров

Реализация InvokeAsyncочень похожа на ту, что была написана с использованием подхода, основанного на соглашениях, за исключением того, что на этот раз параметрами являются HttpContext и RequestDelegate. Любые службы, от которых зависит класс, внедряются через конструктор класса промежуточного ПО, поэтому поля необходимы для хранения экземпляров внедряемой службы. Это промежуточное ПО регистрируется точно так же, как и пример, основанный на соглашениях, с помощью UseMiddleware методов или метода расширения:

app.UseMiddleware<IMiddlewareIpAddressMiddleware>();

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

builder.Services.AddScoped<IMiddlewareIpAddressMiddleware>();

Так почему же существует два разных способа создания классов промежуточного программного обеспечения и какой из них следует использовать? Что ж, подход, основанный на соглашениях, требует, чтобы вы выучили конкретные соглашения и запомнили их. Нет проверки во время компиляции, чтобы убедиться, что промежуточное ПО правильно реализует соглашения. Этот подход известен как слабо типизированный. Как правило, в первый раз вы обнаруживаете, что забыли назвать свой метод Invoke или InvokeAsync, или что первым параметром должен быть HttpContext, когда вы пытаетесь запустить приложение, и оно падает. Если вы чем-то похожи на меня, вы часто обнаруживаете, что вам приходится обращаться к документации, чтобы напомнить себе о деталях соглашения, особенно если вы не так часто создаете промежуточное ПО.

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

Есть еще одно различие между этими двумя подходами. Ранее я упоминал, что ПО промежуточного слоя на основе соглашений всегда создается как одноэлементный экземпляр при первом построении конвейера. IMiddleware компоненты извлекаются из сервисного контейнера и создаются всякий раз, когда они необходимы компоненту, реализующему IMiddlewareFactory интерфейс, и это различие имеет последствия для сервисов, от которых зависит промежуточное ПО, в зависимости от их зарегистрированного срока службы. В этом примере промежуточное ПО имеет ограниченное время жизни, поэтому оно может безопасно принимать службы с заданной областью через свой конструктор.

Следует отметить, что большая часть существующего промежуточного программного обеспечения фреймворка создается с использованием подхода, основанного на соглашениях. В основном это связано с тем, что большая часть этого была написана до того, как IMiddleware была представлена ​​в .NET Core 3.0. Сказав это, нет никаких признаков того, что разработчики фреймворка чувствуют необходимость переноса существующего промежуточного программного обеспечения на IMiddleware.