Связующее ПО (Middleware)

Steve Smith и Rick Anderson

Связующее ПО определено в спецификации OWIN. Эти компоненты образуют некий “трубопровод” между сервером и приложением, чтобы инспектировать, проводить и изменять запросы и ответы для конкретных целей. Связующее ПО состоит из компонентов приложения, которые включены в ASP.NET HTTP.

Что такое связующее ПО

Связующее ПО - это компоненты, которые собраны в поток, чтобы обрабатывать запросы и ответы. Каждый компонент может передавать запрос следующему компоненту в потоке и выполнять определенные действия до и после вызова следующего компонента. Делегаты запросов используются для создания этих потоков, которые служат затем для обработки каждого HTTP запроса.

Делегаты запросов настраиваются с помощью методов расширения Run, Map и Use для IApplicationBuilder, который передается в метод Configure класса Startup. Отдельный делегат запроса может быть указан как анонимный метод либо же может быть определен в многократно используемом классе. Этими многократно используемыми классами являются middleware или middleware components. Каждый компонент связующего ПО в потоке запросов отвечает за вызов следующего компонента в цепочке либо обходит несколько компонентов, чтобы достичь нужного.

В Migrating HTTP Modules to Middleware показана разница между потоками запросов в ASP.NET 5 и предыдущих версиях.

Создание потока при помощи IApplicationBuilder

Поток запросов в ASP.NET состоит из последовательности делегатов запроса, которые вызываются один за другим, как показано на диаграмме:

../_images/request-delegate-pipeline.png

У каждого делегата есть возможность выполнять операции до и после следующего делегата. Любой делегат может не передавать запрос следующему делегату - вместо этого он может обработать запрос сам. Это позволяет избежать ненужной работы. Например, функция авторизации связующего ПО может не вызывать следующий делегат в потоке, если авторизация не произошла, а просто вернуть ответ “Not Authorized”. Делегаты обработки исключений должны вызываться в начале потока, чтобы они могли ловить исключения, которые происходят позже.

Вы можете увидеть пример настройки потока запросов в шаблоне веб сайта по умолчанию Visual Studio 2015. Метод Configure добавляет следующие компоненты связующего ПО:

  1. Обработку ошибок
  2. Сервер статических файлов
  3. Authentication
  4. MVC
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
    });
}

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

static file module предоставляет проверку без авторизации. Все файлы, которые он обрабатывает, находятся в открытом доступе. Если вы хотите провести авторизацию:

  1. Храните их за пределами wwwroot и любой директории, к которой имеет доступ связующее ПО для статических файлов.
  2. Передавайте их через действие контроллера, возвращая FileResult.

Запрос, который обрабатывается модулем статических файлов, закроет поток. (см. Работа со статическими файлами.) Если запрос не обрабатывается таким модулем, он передается `Identity<https://docs.asp.net/projects/api/en/latest/autoapi/Microsoft/AspNet/Builder/BuilderExtensions/index.html#methods>`__, который выполняет аутентификацию. Если запрос не аутонтифицирован, поток закроется. Если аутентификация прошла, вызывается последний компонент в потоке, то есть MVC фреймворк.

Примечание

Порядок, в котором вы добавляете связующее ПО, - это, как правило, порядок, в котором компоненты обрабатывают запрос, и обратный порядок для ответа. Это может быть критичным для безопасности, производительности и функциональности.

В самом простом ASP.NET приложении настраивается один делегат запросов, который обрабатывает все запросы. В данном случае это не “поток” запросов, поскольку существует одна анонимная функция, которая вызывается в ответ на любой HTTP запрос.

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello, World!");
});

Первый делегат App.Run обрывает поток. В следующем примере будет запущен только первый делегат (“Hello, World!”) will run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void Configure(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello, World!");
    });

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello, World, Again!");
    });

Вы можете связать несколько разных делегатов запроса с помощью параметра next. Вы можете оборвать поток, не вызывая параметр next.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void ConfigureLogInline(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
    loggerfactory.AddConsole(minLevel: LogLevel.Information);
    var logger = loggerfactory.CreateLogger(_environment);
    app.Use(async (context, next) =>
    {
        logger.LogInformation("Handling request.");
        await next.Invoke();
        logger.LogInformation("Finished handling request.");
    });

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from " + _environment);
    });
}

Предупреждение

Будьте осторожны при изменении HttpResponse после вызова next, поскольку один из компонентов в потоке может предназначаться для ответа, и он должен быть отправлен клиенту.

Примечание

Метод ConfigureLogInline вызывается тогда, когда приложение запускается в среде, установленной на LogInline. См. Работа с несколькими средами. Мы будем использовать варианты Configure[Environment] для представления этого. Самый простой способ запустить примеры в Visual Studio - это использовать команду web, которая настраивается в project.json. См. Запуск приложения.

В примере выше вызов await next.Invoke() вызовет следующий делегат await context.Response.WriteAsync("Hello from " + _environment);. Клиент получит соответствующий ответ (“Hello from LogInline”) и серверный выход на консоли включит в себя сообщения “до” и “после”, как вы видите вот здесь:

../_images/console-loginline.png

Run, Map и Use

Конфигурация HTTP потока происходит с помощью Run, Map и Use. По соглашению метод Run просто является сокращенным способом добавления связующего ПО в поток, когда не вызывается другое связующее ПО (то есть, next не будет вызван). Соответственно, Run надо вызывать в конце потока. Run - это соглашение, и некоторые компоненты связующего ПО могут использовать свои методы Run[Middleware], которые всегда должны запускаться в конце потока. Следующие два примера (в одном используется Run, а в другом Use) эквивалентны, поскольку во втором не используется параметр next:

public void ConfigureEnvironmentOne(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from " + _environment);
    });
}

public void ConfigureEnvironmentTwo(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        await context.Response.WriteAsync("Hello from " + _environment);
    });
}

Примечание

Интерфейс IApplicationBuilder сам по себе использует один метод Use, так что технически не все они являются методами расширения.

Мы уже рассмотрели несколько примеров того, как создавать поток запросов с помощью Use. Map* же используется для разветвления потока. Текущая реализация поддерживает разветвление, которое основано на пути запросов или использовании предикатов. Метод расширения Map используется для сочетания делегатов запросов в соответствии с путем запроса. Map просто принимает путь и функцию, которая настраивает отдельные части связующего ПО. В следующем примере любой запрос с базовым путем /maptest будет обработан в потоке, настроенном в методе HandleMapTest.

private static void HandleMapTest(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map Test Successful");
    });
}

public void ConfigureMapping(IApplicationBuilder app)
{
    app.Map("/maptest", HandleMapTest);

}

Примечание

Когда используется Map, соответствующие сегменты пути удаляются из HttpRequest.Path и добавляются в HttpRequest.PathBase для каждого запроса.

Метод MapWhen поддерживает разветвление связующего ПО на основе предикатов, что помогает настроить отдельные потоки в очень гибкой манере. Любой предикат типа Func<HttpContext, bool> может быть использован для отправки запросов в новую ветку потока. В следующем примере используется простой предикат для определения наличия переменной строки запроса branch:

private static void HandleBranch(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Branch used.");
    });
}

public void ConfigureMapWhen(IApplicationBuilder app)
{
    app.MapWhen(context => {
        return context.Request.Query.ContainsKey("branch");
    }, HandleBranch);

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from " + _environment);
    });
}

Такая конфигурация делает так, что любой запрос, который включает в себя значение строки запроса branch, будет использовать поток, определенный в методе HandleBranch (в данном случае ответ “Branch used.”). Все другие запросы будут обрабатываться делегатом, определенном в строке 17.

Map также можно вкладывать:

app.Map("/level1", level1App => {
    level1App.Map("/level2a", level2AApp => {
        // "/level1/level2a"
        //...
    });
    level1App.Map("/level2b", level2BApp => {
        // "/level1/level2b"
        //...
    });
});

Встроенное связующее ПО

ASP.NET поставляется со следующими компонентами связующего ПО:

Создание связующего ПО

См. CodeLabs middleware tutorial.

При необходимости использовать более сложного функционала обработки запросов лучше всего реализовать связующее ПО в своем классе и использовать IApplicationBuilder, который вызывается из метода Configure. Простое связующее ПО логирования, показанное в предыдущем примере, может быть конвертировано в класс, который принимает в свой конструктор RequestDelegate и поддерживает метод Invoke:

RequestLoggerMiddleware.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace MiddlewareSample
{
    public class RequestLoggerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;

        public RequestLoggerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
        {
            _next = next;
            _logger = loggerFactory.CreateLogger<RequestLoggerMiddleware>();
        }

        public async Task Invoke(HttpContext context)
        {
            _logger.LogInformation("Handling request: " + context.Request.Path);
            await _next.Invoke(context);
            _logger.LogInformation("Finished handling request.");
        }
    }
}

Связующее ПО следует Explicit Dependencies Principle и использует все свои зависимости в конструкторе. Связующее ПО может применять `UseMiddleware<T>`_, чтобы внедрить сервисы напрямую в свои конструкторы, как показано в примере ниже. Такие сервисы заполняются автоматически, а метод расширения принимает массив аргументов params, который используется для не внедренных параметров.

RequestLoggerExtensions.cs
public static class RequestLoggerExtensions
{
    public static IApplicationBuilder UseRequestLogger(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggerMiddleware>();
    }
}

С помощью метода расширения и нужного класса связующего ПО метод Configure становится очень простым и понятным.

public void ConfigureLogMiddleware(IApplicationBuilder app,
    ILoggerFactory loggerfactory)
{
    loggerfactory.AddConsole(minLevel: LogLevel.Information);

    app.UseRequestLogger();

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from " + _environment);
    });
}

Хотя RequestLoggerMiddleware требует параметр ILoggerFactory в своем конструкторе, ни класс Startup, ни метод расширения UseRequestLogger не должны напрямую поставлять его. Вместо этого он напрямую поставляется через внедрение зависимости в UseMiddleware<T>.

Тестирование связующего ПО (при установке переменной среды Hosting:Environment на LogMiddleware) выдаст следующий результат (с помощью WebListener):

../_images/console-logmiddleware.png

Примечание

UseStaticFiles (который создает StaticFileMiddleware) также использует UseMiddleware<T>. В данном случае передается параметр StaticFileOptions, а другие параметры передаются UseMiddleware<T> и с помощью DI.

Поделись хорошей новостью с друзьями!
Следи за новостями!