ПО промежуточного слоя ASP.NET Core

Авторы: Рик Андерсон (Rick Anderson) и Стив Смит (Steve Smith)

ПО промежуточного слоя — это программное обеспечение, выстраиваемое в виде конвейера приложения для обработки запросов и откликов. Каждый компонент:

  • определяет, нужно ли передать запрос следующему компоненту в конвейере;
  • может выполнять работу как до, так и после вызова следующего компонента в конвейере.

Для построения конвейера запросов используются делегаты запроса. Они обрабатывают каждый HTTP-запрос.

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

В статье миграция обработчиков и модулей HTTP в ASP.NET Core по промежуточного слоя поясняются различия между конвейерами запросов в ASP.NET Core и ASP.NET 4.x, а также приводятся дополнительные примеры ПО промежуточного слоя.

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

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

Шаблон обработки запросов, показывающий входящий запрос, обработку в трех компонентах промежуточного слоя и запрос, покидающий приложение. Каждый компонент промежуточного слоя выполняет свою логику и передает запрос следующему компоненту промежуточного слоя в операторе next(). После обработки в третьем компоненте промежуточного слоя запрос возвращается через два предыдущих компонента в обратном порядке для дополнительной обработки после их операторов next(), прежде чем он покинет приложение в качестве отклика клиенту.

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

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

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();

Несколько делегатов запроса можно соединить в цепочку с помощью Use. Параметр next представляет следующий делегат в конвейере. Замыкать конвейер можно не вызывая параметр next. Обычно действия можно выполнять как до, так и после next делегата, как показано в этом примере:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that doesn't write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();

Если делегат не передает запрос следующему делегату, это называется замыканием конвейера запросов. Замыкание часто является предпочтительным, так как позволяет избежать ненужной работы. Например, компонент промежуточного слоя для статических файлов может выступать в роли терминального промежуточного слоя, обрабатывая запрос статического файла и замыкая оставшуюся часть конвейера. Компоненты промежуточного слоя, предшествующие терминальному промежуточному слою, по-прежнему обрабатывают код после их инструкций next.Invoke. Но учитывайте следующее предупреждение о попытке записи в ответ, который уже был отправлен.

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

Не вызывайте next.Invoke после отправки отклика клиенту. Изменения HttpResponse после запуска отклика приведут к возникновению исключения. Таким изменением может быть задание заголовков и кода состояния. Запись в тело отклика после вызова next:

  • может вызвать нарушение протокола, например, при записи больше указанного значения Content-Length;
  • может привести к нарушению формата, например, при записи нижнего колонтитула HTML в CSS-файл.

HasStarted удобно использовать для обозначения того, были ли отправлены заголовки или выполнена запись в тело отклика.

Делегаты Run не получают параметр next. Первый делегат Run всегда является конечным и завершает конвейер. Run является соглашение. Некоторые компоненты промежуточного слоя могут предоставлять методы Run[Middleware], которые выполняются в конце конвейера:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that doesn't write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();

Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от английского, сообщите нам на странице обсуждения этой проблемы на сайте GitHub.

В предыдущем примере делегат Run записывает "Hello from 2nd delegate." в ответ и завершает конвейер. Если добавить другой делегат Use или Run после делегата Run, он не будет вызван.

Рекомендуется перегрузка app.Use, требующая передачи контекста далее

Метод расширения app.Use без выделения:

  • Требует передачи контекста в next.
  • Сохраняет два внутренних выделения на один запрос, которые требуются при другой перегрузке.

Дополнительные сведения см. в этой статье об ошибке на GitHub.

Порядок ПО промежуточного слоя

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

Конвейер ПО промежуточного слоя ASP.NET Core

ПО промежуточного слоя конечной точки на предыдущей схеме выполняет конвейер фильтра для соответствующего типа приложения — MVC или Razor Pages.

ПО промежуточного слоя маршрутизации на предыдущей схеме размещено после статических файлов. В таком порядке реализуются шаблоны проектов путем явного вызова app.UseRouting. Если оператор app.UseRouting не вызван, по умолчанию ПО промежуточного слоя маршрутизации запускается в начале конвейера. Дополнительные сведения см. в разделе Маршрутизация.

Конвейер фильтра ASP.NET Core

Порядок, в котором компоненты промежуточного слоя добавляются в файл Program.cs, определяет порядок их вызова при запросах и обратный порядок для откликов. Соблюдать этот порядок крайне важно для обеспечения безопасности, производительности и функциональности.

Следующий выделенный код в Program.cs добавляет связанные с безопасностью компоненты промежуточного слоя в стандартном рекомендуемом порядке:

using IndividualAccountsExample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

В приведенном выше коде:

  • ПО промежуточного слоя, которое не было добавлено при создании веб-приложения с учетными записями отдельных пользователей, деактивируется.
  • Этот порядок соблюдается не для любого ПО промежуточного слоя. Пример.
    • UseCors, UseAuthentication и UseAuthorization должны присутствовать в указанном порядке.
    • Сейчас UseCors нужно использовать перед UseResponseCaching. Это требование объясняется в вопросе GitHub по адресу dotnet/aspnetcore #23218.
    • UseRequestLocalization нужно использовать перед любым ПО промежуточного слоя, которое может проверять язык и региональные параметры запроса (например, app.UseMvcWithDefaultRoute()).

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

app.UseResponseCaching();
app.UseResponseCompression();

С помощью приведенного выше кода можно снизить загрузку ЦП путем кэширования сжатого ответа, но при этом может быть выполнено кэширование нескольких представлений ресурса с помощью разных алгоритмов сжатия, таких как Gzip или Brotli.

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

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

Следующий код Program.cs добавляет компоненты промежуточного слоя для распространенных сценариев приложений:

  1. Обработка исключений/ошибок
    • Когда приложение выполняется в среде разработки:
      • ПО промежуточного слоя страницы исключений для разработчика (UseDeveloperExceptionPage) сообщает об ошибках среды выполнения приложения.
      • ПО промежуточного слоя страницы исключений для базы данных (UseDatabaseErrorPage) сообщает об ошибках среды выполнения базы данных.
    • Когда приложение выполняется в рабочей среде:
      • ПО промежуточного слоя обработчика исключений (UseExceptionHandler) перехватывает исключения, возникшие в указанном ниже ПО промежуточного слоя.
      • ПО промежуточного слоя протокола HTTP Strict Transport Security Protocol (HSTS) (UseHsts) добавляет заголовок Strict-Transport-Security.
  2. ПО промежуточного слоя перенаправления HTTPS (UseHttpsRedirection) перенаправляет запросы с HTTP на HTTPS.
  3. ПО промежуточного слоя статических файлов (UseStaticFiles) возвращает статические файлы и сокращает дальнейшую обработку запросов.
  4. ПО промежуточного слоя политики файлов Cookie (UseCookiePolicy) обеспечивает соответствие приложения нормам Общего регламента по защите данных (GDPR) ЕС.
  5. ПО промежуточного слоя маршрутизации (UseRouting) для маршрутизации запросов.
  6. ПО промежуточного слоя проверки подлинности (UseAuthentication) пытается проверить подлинность пользователя, прежде чем предоставить ему доступ к защищенным ресурсам.
  7. ПО промежуточного слоя авторизации (UseAuthorization) разрешает пользователю доступ к защищенным ресурсам.
  8. ПО промежуточного слоя сеанса (UseSession) устанавливает и поддерживает состояние сеанса. Если в приложении используется состояние сеанса, вызовите ПО промежуточного слоя сеанса после ПО промежуточного слоя политики файлов Cookie и до ПО промежуточного слоя MVC.
  9. ПО промежуточного слоя маршрутизации конечных точек (UseEndpoints с MapRazorPages) для добавления конечных точек Razor Pages в конвейер запросов.
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseDatabaseErrorPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();

В предыдущем примере кода каждый метод расширения ПО промежуточного слоя представляется в WebApplicationBuilder с использованием пространства имен Microsoft.AspNetCore.Builder.

UseExceptionHandler — это первый компонент промежуточного слоя, добавленный в конвейер. Таким образом, обработчик исключений ПО промежуточного слоя перехватывает все исключения, возникающие в последующих вызовах.

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

Если запрос не обрабатывается компонентом промежуточного слоя для статических файлов, он передается в компонент промежуточного слоя для проверки подлинности (UseAuthentication), который выполняет проверку подлинности. Этот компонент не замыкает запросы, не прошедшие проверку подлинности. Хотя ПО промежуточного слоя для проверки подлинности проверяет подлинность запросов, авторизация (и отклонение) выполняются только после того, как MVC выберет указанную страницу Razor Pages или контроллер MVC и действие.

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

// Static files aren't compressed by Static File Middleware.
app.UseStaticFiles();

app.UseRouting();

app.UseResponseCompression();

app.MapRazorPages();

Дополнительные сведения об одностраничных приложениях см. в руководствах по шаблонам проектов React и Angular.

Порядок ПО промежуточного слоя перенаправления заголовков

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

Ветвление конвейера ПО промежуточного слоя

Расширения Map используются в качестве соглашения для ветвления конвейера. Map осуществляет ветвление конвейера запросов на основе совпадений для заданного пути запроса. Если путь запроса начинается с заданного пути, данная ветвь выполняется.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMapTest1);

app.Map("/map2", HandleMapTest2);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});

app.Run();

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

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

В таблице ниже приведены запросы и отклики http://localhost:1234 на базе предыдущего кода.

Запрос Ответ
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

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

Map поддерживает вложение, например:

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

Map также может сопоставить несколько сегментов одновременно:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1/seg1", HandleMultiSeg);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});

app.Run();

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

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

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

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

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

Ниже приведены запросы и отклики http://localhost:1234 на базе предыдущего кода.

Запрос Ответ
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=main Branch used = main

UseWhen также осуществляет ветвление конвейера запросов на основе результата заданного предиката. В отличие от MapWhen, эта ветвь снова объединяется с основным конвейером, если она не выполняется по сокращенной схеме или не содержит конечное ПО промежуточного слоя:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
    appBuilder => HandleBranchAndRejoin(appBuilder));

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});

app.Run();

void HandleBranchAndRejoin(IApplicationBuilder app)
{
    var logger = app.ApplicationServices.GetRequiredService<ILogger<Program>>(); 

    app.Use(async (context, next) =>
    {
        var branchVer = context.Request.Query["branch"];
        logger.LogInformation("Branch used = {branchVer}", branchVer);

        // Do work that doesn't write to the Response.
        await next();
        // Do other work that doesn't write to the Response.
    });
}

В предыдущем примере ответ "Hello from main pipeline." записывается для всех запросов. Если запрос включает переменную строки запроса branch, ее значение регистрируется до того, как будет выполнено повторное объединение с основным конвейером.

Встроенное ПО промежуточного слоя

ASP.NET Core содержит следующие компоненты промежуточного слоя. В столбце Порядок указаны сведения о размещении ПО промежуточного слоя в конвейере обработки запросов и условия, в соответствии с которыми ПО промежуточного слоя может прервать обработку запроса. Если промежуточный слой замыкает конвейер обработки запроса и препятствует обработке запроса дальнейшими компонентами промежуточного слоя, он называется терминальным промежуточным слоем. Дополнительные сведения о замыкании конвейера см. в разделе Создание конвейера ПО промежуточного слоя с помощью IApplicationBuilder.

ПО промежуточного слоя Описание Номер
Authentication Обеспечивает поддержку проверки подлинности. Ставится перед тем, как потребуется HttpContext.User. Является конечным для обратных вызовов OAuth.
Авторизация Обеспечивает поддержку авторизации. Непосредственно после ПО промежуточного слоя для проверки подлинности.
Политика Cookie Позволяет отслеживать согласие пользователей на хранение личных сведений и применять минимальные стандарты для полей файлов cookie, таких как secure и SameSite. Перед ПО промежуточного слоя, которое использует файлы cookie. Примеры Authentication, Session, MVC (TempData).
CORS Настраивает общий доступ к ресурсам независимо от источника. Ставится перед компонентами, использующими CORS. UseCors сейчас нужно использовать перед UseResponseCaching из-за этой ошибки.
DeveloperExceptionPage Создает страницу со сведениями об ошибках, предназначенными для использования только в среде разработки. Ставится перед компонентами, выдающими ошибки. Шаблоны проектов автоматически регистрируют это ПО промежуточного слоя в качестве первого в конвейере в среде разработки.
Error Handling Отдельное ПО промежуточного слоя, которое обеспечивает обработку исключений, предоставляет страницу исключений для разработчика, страницы состояния кода, веб-страницу по умолчанию для новых приложений. Ставится перед компонентами, выдающими ошибки. Является конечным для исключений или обслуживания веб-страницы по умолчанию для новых приложений.
Forwarded Headers Пересылает заголовки, переданные через прокси-сервер, в текущий запрос. Перед компонентами, использующими обновленные поля. Например: схема, узел, IP-адрес клиента, метод.
Проверка работоспособности Проверяет работоспособность приложения ASP.NET Core и его зависимостей, таких как проверка доступности базы данных. Является конечным, если запрос соответствует конечной точке проверки работоспособности.
Распространение заголовков Распространяет заголовки HTTP из входящего запроса на исходящие запросы HTTP-клиентов.
Ведение журнала HTTP Регистрирует запросы и отклики HTTP. В начале конвейера ПО промежуточного слоя.
HTTP Method Override Разрешает входящий запрос POST для переопределения этого метода. Ставится перед компонентами, использующими обновленный метод.
HTTPS Redirection Перенаправляет все запросы HTTP на HTTPS. Ставится перед компонентами, использующими URL-адрес.
HTTP Strict Transport Security (HSTS) ПО промежуточного слоя для повышения безопасности, которое добавляет специальный заголовок ответа. Перед отправкой ответов и после компонентов, изменяющих запросы. Примеры Forwarded Headers, URL Rewriting.
MVC Обрабатывает запросы с помощью MVC либо Razor Pages. Является конечным, если запрос соответствует маршруту.
OWIN Взаимодействие с приложениями, серверами и ПО промежуточного слоя на основе OWIN. Является конечным, если ПО промежуточного слоя OWIN полностью обрабатывает запрос.
Кэширование ответов Обеспечивает поддержку для кэширования откликов. Ставится перед компонентами, требующими кэширование. UseCORS нужно использовать перед UseResponseCaching.
Сжатие откликов Обеспечивает поддержку для сжатия откликов. Ставится перед компонентами, требующими сжатие.
Localization Обеспечивает поддержку локализации. Ставится перед компонентами, для которых важна локализация. Требуется отображение после ПО промежуточного слоя маршрутизации при использовании RouteDataRequestCultureProvider.
Маршрутизация конечных точек Определяет и ограничивает маршруты запросов. Является конечным для совпадающих маршрутов.
Безопасная проверка пароля Обрабатывает все запросы от этой точки в цепочке ПО промежуточного слоя, возвращая страницу по умолчанию для одностраничного приложения (SPA) В конце цепочки, чтобы другое ПО промежуточного слоя для обслуживания статических файлов, действий MVC и т. д. имело приоритет.
Session Обеспечивает поддержку для управления пользовательскими сеансами. Ставится перед компонентами, требующими сеанс.
Static Files Обеспечивает поддержку для обработки статических файлов и просмотра каталогов. Является конечным, если запрос соответствует файлу.
Переопределение URL-адресов Обеспечивает поддержку для переопределения URL-адресов и перенаправления запросов. Ставится перед компонентами, использующими URL-адрес.
W3CLogging Создает журналы доступа к серверу в расширенном формате файла журнала W3C. В начале конвейера ПО промежуточного слоя.
WebSockets Обеспечивает поддержку протокола WebSockets. Ставится перед компонентами, которым нужно принимать запросы WebSocket.

Дополнительные ресурсы

Авторы: Рик Андерсон (Rick Anderson) и Стив Смит (Steve Smith)

ПО промежуточного слоя — это программное обеспечение, выстраиваемое в виде конвейера приложения для обработки запросов и откликов. Каждый компонент:

  • определяет, нужно ли передать запрос следующему компоненту в конвейере;
  • может выполнять работу как до, так и после вызова следующего компонента в конвейере.

Для построения конвейера запросов используются делегаты запроса. Они обрабатывают каждый HTTP-запрос.

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

В статье миграция обработчиков и модулей HTTP в ASP.NET Core по промежуточного слоя поясняются различия между конвейерами запросов в ASP.NET Core и ASP.NET 4.x, а также приводятся дополнительные примеры ПО промежуточного слоя.

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

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

Шаблон обработки запросов, показывающий входящий запрос, обработку в трех компонентах промежуточного слоя и запрос, покидающий приложение. Каждый компонент промежуточного слоя выполняет свою логику и передает запрос следующему компоненту промежуточного слоя в операторе next(). После обработки в третьем компоненте промежуточного слоя запрос возвращается через два предыдущих компонента в обратном порядке для дополнительной обработки после их операторов next(), прежде чем он покинет приложение в качестве отклика клиенту.

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

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

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

Несколько делегатов запроса можно соединить в цепочку с помощью Use. Параметр next представляет следующий делегат в конвейере. Замыкать конвейер можно не вызывая параметр next. Обычно действия можно выполнять как до, так и после следующего делегата, как показано в этом примере.

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

Если делегат не передает запрос следующему делегату, это называется замыканием конвейера запросов. Замыкание часто является предпочтительным, так как позволяет избежать ненужной работы. Например, компонент промежуточного слоя для статических файлов может выступать в роли терминального промежуточного слоя, обрабатывая запрос статического файла и замыкая оставшуюся часть конвейера. Компоненты промежуточного слоя, предшествующие терминальному промежуточному слою, по-прежнему обрабатывают код после их инструкций next.Invoke. Но учитывайте следующее предупреждение о попытке записи в ответ, который уже был отправлен.

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

Не вызывайте next.Invoke после отправки отклика клиенту. Изменения HttpResponse после запуска отклика приведут к возникновению исключения. Таким изменением может быть задание заголовков и кода состояния. Запись в тело отклика после вызова next:

  • может вызвать нарушение протокола, например, при записи больше указанного значения Content-Length;
  • может привести к нарушению формата, например, при записи нижнего колонтитула HTML в CSS-файл.

HasStarted удобно использовать для обозначения того, были ли отправлены заголовки или выполнена запись в тело отклика.

Делегаты Run не получают параметр next. Первый делегат Run всегда является конечным и завершает конвейер. Run является соглашение. Некоторые компоненты промежуточного слоя могут предоставлять методы Run[Middleware], которые выполняются в конце конвейера:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

Если вы хотите увидеть комментарии к коду, переведенные на языки, отличные от английского, сообщите нам на странице обсуждения этой проблемы на сайте GitHub.

В предыдущем примере делегат Run записывает "Hello from 2nd delegate." в ответ и завершает конвейер. Если добавить другой делегат Use или Run после делегата Run, он не будет вызван.

Порядок ПО промежуточного слоя

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

Конвейер ПО промежуточного слоя ASP.NET Core

ПО промежуточного слоя конечной точки на предыдущей схеме выполняет конвейер фильтра для соответствующего типа приложения — MVC или Razor Pages.

Конвейер фильтра ASP.NET Core

Порядок, в котором компоненты промежуточного слоя добавляются в метод Startup.Configure, определяет порядок их вызова при запросах и обратный порядок для отклика. Соблюдать этот порядок крайне важно для обеспечения безопасности, производительности и функциональности.

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    // app.UseCookiePolicy();

    app.UseRouting();
    // app.UseRequestLocalization();
    // app.UseCors();

    app.UseAuthentication();
    app.UseAuthorization();
    // app.UseSession();
    // app.UseResponseCompression();
    // app.UseResponseCaching();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

В приведенном выше коде:

  • ПО промежуточного слоя, которое не было добавлено при создании веб-приложения с учетными записями отдельных пользователей, деактивируется.
  • Этот порядок соблюдается не для любого ПО промежуточного слоя. Пример.
    • UseCors, UseAuthentication и UseAuthorization должны присутствовать в указанном порядке.
    • Сейчас UseCors нужно использовать перед UseResponseCaching из-за этой ошибки.
    • UseRequestLocalization нужно использовать перед любым ПО промежуточного слоя, которое может проверять язык и региональные параметры запроса (например, app.UseMvcWithDefaultRoute()).

В некоторых сценариях ПО промежуточного слоя имеет другой порядок. Например, порядок кэширования и сжатия зависит от сценария, и существует несколько допустимых вариантов такого порядка. Пример:

app.UseResponseCaching();
app.UseResponseCompression();

С помощью приведенного выше кода можно экономить ресурсы ЦП путем кэширования сжатого ответа, но при этом может быть выполнено кэширование нескольких представлений ресурса с помощью разных алгоритмов сжатия, таких как Gzip или Brotli.

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

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

Метод Startup.Configure добавляет компоненты ПО промежуточного слоя для распространенных сценариев приложений:

  1. Обработка исключений/ошибок
    • Когда приложение выполняется в среде разработки:
      • ПО промежуточного слоя страницы исключений для разработчика (UseDeveloperExceptionPage) сообщает об ошибках среды выполнения приложения.
      • ПО промежуточного слоя страницы исключений для базы данных сообщает об ошибках среды выполнения базы данных.
    • Когда приложение выполняется в рабочей среде:
      • ПО промежуточного слоя обработчика исключений (UseExceptionHandler) перехватывает исключения, возникшие в указанном ниже ПО промежуточного слоя.
      • ПО промежуточного слоя протокола HTTP Strict Transport Security Protocol (HSTS) (UseHsts) добавляет заголовок Strict-Transport-Security.
  2. ПО промежуточного слоя перенаправления HTTPS (UseHttpsRedirection) перенаправляет запросы с HTTP на HTTPS.
  3. ПО промежуточного слоя статических файлов (UseStaticFiles) возвращает статические файлы и сокращает дальнейшую обработку запросов.
  4. ПО промежуточного слоя политики файлов Cookie (UseCookiePolicy) обеспечивает соответствие приложения нормам Общего регламента по защите данных (GDPR) ЕС.
  5. ПО промежуточного слоя маршрутизации (UseRouting) для маршрутизации запросов.
  6. ПО промежуточного слоя проверки подлинности (UseAuthentication) пытается проверить подлинность пользователя, прежде чем предоставить ему доступ к защищенным ресурсам.
  7. ПО промежуточного слоя авторизации (UseAuthorization) разрешает пользователю доступ к защищенным ресурсам.
  8. ПО промежуточного слоя сеанса (UseSession) устанавливает и поддерживает состояние сеанса. Если в приложении используется состояние сеанса, вызовите ПО промежуточного слоя сеанса после ПО промежуточного слоя политики файлов Cookie и до ПО промежуточного слоя MVC.
  9. ПО промежуточного слоя маршрутизации конечных точек (UseEndpoints с MapRazorPages) для добавления конечных точек Razor Pages в конвейер запросов.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseSession();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

В предыдущем примере кода каждый метод расширения ПО промежуточного слоя представляется в IApplicationBuilder с использованием пространства имен Microsoft.AspNetCore.Builder.

UseExceptionHandler — это первый компонент промежуточного слоя, добавленный в конвейер. Таким образом, обработчик исключений ПО промежуточного слоя перехватывает все исключения, возникающие в последующих вызовах.

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

Если запрос не обрабатывается компонентом промежуточного слоя для статических файлов, он передается в компонент промежуточного слоя для проверки подлинности (UseAuthentication), который выполняет проверку подлинности. Этот компонент не замыкает запросы, не прошедшие проверку подлинности. Хотя ПО промежуточного слоя для проверки подлинности проверяет подлинность запросов, авторизация (и отклонение) выполняются только после того, как MVC выберет указанную страницу Razor Pages или контроллер MVC и действие.

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

public void Configure(IApplicationBuilder app)
{
    // Static files aren't compressed by Static File Middleware.
    app.UseStaticFiles();

    app.UseRouting();

    app.UseResponseCompression();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Для одностраничных приложений (SPA) ПО промежуточного слоя SPA UseSpaStaticFiles обычно поступает в конвейер ПО промежуточного слоя последним. ПО промежуточного слоя SPA поступает последним:

  • Чтобы разрешить остальным ПО промежуточного слоя отвечать на запросы сопоставления в первую очередь.
  • Чтобы разрешить одностраничным приложениям с маршрутизацией на стороне клиента выполняться для всех маршрутов, не распознаваемых серверным приложением.

Дополнительные сведения об SPA см. в руководствах по шаблонам проектов React и Angular.

Порядок ПО промежуточного слоя перенаправления заголовков

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

Ветвление конвейера ПО промежуточного слоя

Расширения Map используются в качестве соглашения для ветвления конвейера. Map осуществляет ветвление конвейера запросов на основе совпадений для заданного пути запроса. Если путь запроса начинается с заданного пути, данная ветвь выполняется.

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

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

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

Ниже приведены запросы и отклики http://localhost:1234 на базе предыдущего кода.

Запрос Ответ
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

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

Map поддерживает вложение, например:

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

Map также может сопоставить несколько сегментов одновременно:

public class Startup
{
    private static void HandleMultiSeg(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map multiple segments.");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1/seg1", HandleMultiSeg);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate.");
        });
    }
}

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

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

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

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

Ниже приведены запросы и отклики http://localhost:1234 на базе предыдущего кода.

Запрос Ответ
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=main Branch used = main

UseWhen также осуществляет ветвление конвейера запросов на основе результата заданного предиката. В отличие от MapWhen, эта ветвь снова объединяется с основным конвейером, если она не выполняется по сокращенной схеме или не содержит конечное ПО промежуточного слоя:

public class Startup
{
    private void HandleBranchAndRejoin(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.Use(async (context, next) =>
        {
            var branchVer = context.Request.Query["branch"];
            logger.LogInformation("Branch used = {branchVer}", branchVer);

            // Do work that doesn't write to the Response.
            await next();
            // Do other work that doesn't write to the Response.
        });
    }

    public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
    {
        app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                               appBuilder => HandleBranchAndRejoin(appBuilder, logger));

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

В предыдущем примере ответ "Hello from main pipeline." записывается для всех запросов. Если запрос включает переменную строки запроса branch, ее значение регистрируется до того, как будет выполнено повторное объединение с основным конвейером.

Встроенное ПО промежуточного слоя

ASP.NET Core содержит следующие компоненты промежуточного слоя. В столбце Порядок указаны сведения о размещении ПО промежуточного слоя в конвейере обработки запросов и условия, в соответствии с которыми ПО промежуточного слоя может прервать обработку запроса. Если промежуточный слой замыкает конвейер обработки запроса и препятствует обработке запроса дальнейшими компонентами промежуточного слоя, он называется терминальным промежуточным слоем. Дополнительные сведения о замыкании конвейера см. в разделе Создание конвейера ПО промежуточного слоя с помощью IApplicationBuilder.

ПО промежуточного слоя Описание Номер
Authentication Обеспечивает поддержку проверки подлинности. Ставится перед тем, как потребуется HttpContext.User. Является конечным для обратных вызовов OAuth.
Авторизация Обеспечивает поддержку авторизации. Непосредственно после ПО промежуточного слоя для проверки подлинности.
Политика Cookie Позволяет отслеживать согласие пользователей на хранение личных сведений и применять минимальные стандарты для полей файлов cookie, таких как secure и SameSite. Перед ПО промежуточного слоя, которое использует файлы cookie. Примеры Authentication, Session, MVC (TempData).
CORS Настраивает общий доступ к ресурсам независимо от источника. Ставится перед компонентами, использующими CORS. UseCors сейчас нужно использовать перед UseResponseCaching из-за этой ошибки.
Error Handling Отдельное ПО промежуточного слоя, которое обеспечивает обработку исключений, предоставляет страницу исключений для разработчика, страницы состояния кода, веб-страницу по умолчанию для новых приложений. Ставится перед компонентами, выдающими ошибки. Является конечным для исключений или обслуживания веб-страницы по умолчанию для новых приложений.
Forwarded Headers Пересылает заголовки, переданные через прокси-сервер, в текущий запрос. Перед компонентами, использующими обновленные поля. Например: схема, узел, IP-адрес клиента, метод.
Проверка работоспособности Проверяет работоспособность приложения ASP.NET Core и его зависимостей, таких как проверка доступности базы данных. Является конечным, если запрос соответствует конечной точке проверки работоспособности.
Распространение заголовков Распространяет заголовки HTTP из входящего запроса на исходящие запросы HTTP-клиентов.
HTTP Method Override Разрешает входящий запрос POST для переопределения этого метода. Ставится перед компонентами, использующими обновленный метод.
HTTPS Redirection Перенаправляет все запросы HTTP на HTTPS. Ставится перед компонентами, использующими URL-адрес.
HTTP Strict Transport Security (HSTS) ПО промежуточного слоя для повышения безопасности, которое добавляет специальный заголовок ответа. Перед отправкой ответов и после компонентов, изменяющих запросы. Примеры Forwarded Headers, URL Rewriting.
MVC Обрабатывает запросы с помощью MVC либо Razor Pages. Является конечным, если запрос соответствует маршруту.
OWIN Взаимодействие с приложениями, серверами и ПО промежуточного слоя на основе OWIN. Является конечным, если ПО промежуточного слоя OWIN полностью обрабатывает запрос.
Кэширование ответов Обеспечивает поддержку для кэширования откликов. Ставится перед компонентами, требующими кэширование. UseCORS нужно использовать перед UseResponseCaching.
Сжатие откликов Обеспечивает поддержку для сжатия откликов. Ставится перед компонентами, требующими сжатие.
Localization Обеспечивает поддержку локализации. Ставится перед компонентами, для которых важна локализация. Требуется отображение после ПО промежуточного слоя маршрутизации при использовании RouteDataRequestCultureProvider.
Маршрутизация конечных точек Определяет и ограничивает маршруты запросов. Является конечным для совпадающих маршрутов.
Безопасная проверка пароля Обрабатывает все запросы от этой точки в цепочке ПО промежуточного слоя, возвращая страницу по умолчанию для одностраничного приложения (SPA) В конце цепочки, чтобы другое ПО промежуточного слоя для обслуживания статических файлов, действий MVC и т. д. имело приоритет.
Session Обеспечивает поддержку для управления пользовательскими сеансами. Ставится перед компонентами, требующими сеанс.
Static Files Обеспечивает поддержку для обработки статических файлов и просмотра каталогов. Является конечным, если запрос соответствует файлу.
Переопределение URL-адресов Обеспечивает поддержку для переопределения URL-адресов и перенаправления запросов. Ставится перед компонентами, использующими URL-адрес.
WebSockets Обеспечивает поддержку протокола WebSocket. Ставится перед компонентами, которым нужно принимать запросы WebSocket.

Дополнительные ресурсы