Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль

Кастомные Middleware на C# в ASP.NET Core

Запись от UnmanagedCoder размещена 27.04.2025 в 12:43
Показов 2684 Комментарии 0

Нажмите на изображение для увеличения
Название: 07ddc9da-e6d0-4909-b4a6-c7838c6e007a.jpg
Просмотров: 33
Размер:	206.6 Кб
ID:	10679
Разработка веб-приложений сегодня мало напоминает монолитное программирование прошлых лет. На смену громоздким блокам кода пришла модульная архитектура, где каждый компонент выполняет строго определённую функцию. В ASP.NET Core именно middleware-компоненты стали тем самым строительным материалом, из которого складывается функциональность веб-приложения.

Принципы работы конвейера обработки HTTP-запросов



Cуть middleware можно описать простой метафорой: представьте конвейер, по которому движется HTTP-запрос от клиента к серверу и обратно. На этом пути запрос проходит через последовательность "станций обработки" — именно эти станции и являются middleware-компонентами. Каждый из них может модифицировать запрос, проверить его, обогатить дополнительными данными или даже полностью остановить дальнейшую обработку.

Технически middleware в ASP.NET Core представляет собой делегат RequestDelegate, принимающий контекст HTTP-запроса (HttpContext) и возвращающий Task. Простейший middleware выглядит так:

C#
1
2
3
4
5
6
app.Use(async (context, next) =>
{
    // Код, выполняемый до вызова следующего middleware
    await next();
    // Код, выполняемый после возврата из следующего middleware
});
Именно этот шаблон "двойной ответственности" (обработка до и после передачи управления) делает middleware таким гибким инструментом. Один компонент может запустить таймер перед передачей запроса дальше, и остановить его при возвращении, определив время выполнения. Другой — добавить заголовки к ответу до его отправки клиенту.

Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2
Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными...

ASP.NET Core. Старт - что нужно знать, чтобы стать ASP.NET Core разработчиком?
Попалось хор краткое обзорное видео 2016 года с таким названием - Что нужно знать, чтобы стать...

Какая разница между ASP .Net Core и ASP .Net Core MVC?
Какая разница между ASP .Net Core и ASP .Net Core MVC? Или я может что-то не так понял? И...

ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...


Отличия от предыдущих версий платформы



"Классический" ASP.NET опирался на систему HTTP-модулей и HTTP-обработчиков, интегрированных в конвейер обработки IIS. Эта модель имела ряд ограничений — сложное управление порядком выполнения и тесная связь с Windows-платформой. ASP.NET Core порвал с этим наследием, предложив кросс-платформенное и более модульное решение. Теперь middleware-компоненты:
  • Работают независимо от веб-сервера.
  • Имеют чётко определённый порядок выполнения.
  • Могут быть легко добавлены или удалены из конвейера.
  • Поддерживают асинхронную обработку "из коробки".

Сравнение с другими фреймворками



Концепция middleware не уникальна для ASP.NET Core — подобные механизмы встречаются во многих современных фреймворках:

Express.js (Node.js) → app.use((req, res, next) => {...})
Koa.js (Node.js) → app.use(async (ctx, next) => {...})
Ruby on Rails → Rack middleware
ASP.NET Core → app.Use(async (context, next) => {...})

Примечательно, что реализация в ASP.NET Core во многом вдохновлена именно Node.js-фреймворками, сохраняя при этом типобезопасность C# и интеграцию с системой зависимостей.

Механика асинхронных вызовов



Одно из ключевых преимуществ middleware в ASP.NET Core — "родная" поддержка асинхронных операций. Когда вызывается await next(), текущий поток не блокируется в ожидании завершения обработки, а возвращается в пул, что позволяет обрабатывать другие запросы. Внутри конвейера операции образуют нечто вроде "стопки вызовов": каждый middleware добавляет свой контекст исполнения, формируя структуру, напоминающую матрёшку. Интересно, что код, написанный после await next(), выполняется на обратном пути — когда запрос уже обработан всеми последующими компонентами.

Влияние OWIN на архитектуру ASP.NET Core



Нельзя говорить о middleware в ASP.NET Core, не упомянув OWIN (Open Web Interface for .NET) — спецификацию, которая задала вектор развития веб-стека Microsoft. OWIN определил четкий интерфейс между веб-сервером и приложением, позволяя им эволюционировать отдельно друг от друга. Хотя ASP.NET Core не является прямой реализацией OWIN, многие идеи были перенесены в новую платформу:
  • Абстрагирование от конкретного веб-сервера.
  • Модульность и композиция компонентов.
  • Передача состояния через словарь окружения (в ASP.NET Core — через HttpContext).

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

Базовая архитектура Middleware



Понимание архитектуры middleware-компонентов — фундамент для эффективной разработки приложений на ASP.NET Core. Под капотом эта система представляет собой сложный механизм, где каждая деталь играет важную роль в обработке HTTP-запросов.

Компоненты запроса и ответа



В центре архитектуры middleware находится класс HttpContext, который инкапсулирует всю информацию о текущем HTTP-запросе и ответе. Этот объект передается через всю цепочку middleware, позволяя компонентам получать доступ к:

HttpRequest — содержит данные запроса (заголовки, тело, метод, URL),
HttpResponse — представляет ответ, отправляемый клиенту,
Services — предоставляет доступ к контейнеру зависимостей,
Items — словарь для хранения временных данных запроса,
User — информация о пользователе (после аутентификации),
Features — коллекция интерфейсов, реализующих дополнительные возможности.

C#
1
2
3
4
5
6
7
8
9
10
11
12
public async Task InvokeAsync(HttpContext context)
{
    string path = context.Request.Path;
    // Доступ к строке запроса
    var queryValues = context.Request.Query;
    
    // Манипуляции с ответом
    context.Response.Headers.Add("X-Custom-Header", "CustomValue"); 
    context.Response.ContentType = "application/json";
    
    await next(context);
}
Интересная особенность: объект HttpContext проектировался с учётом возможности его переиспользования, что позволяет сократить нагрузку на сборщик мусора при обработке большого количества запросов.

Порядок выполнения и делегирование



Порядок выполнения middleware важен для корректной работы приложения. Компоненты выполняются в том порядке, в котором они добавлены в конвейер в методе Configure класса Startup:

C#
1
2
3
4
5
6
7
8
9
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Error");
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints();
}
Каждый компонент получает ссылку на следующий в цепочке через параметр next — это и есть механизм делегирования. Когда middleware решает передать управление дальше, он вызывает этот делегат, что приводит к выполнению следующего компонента в конвейере. Важный нюанс: если middleware не вызывает next(), обработка запроса на этом завершается — ни один из последующих компонентов не будет выполнен. Это позволяет реализовать такие сценарии, как авторизация или проверка запроса на валидность, предотвращая дальнейшую обработку, если проверка не пройдена.

Жизненный цикл middleware-компонента



Жизненный цикл middleware-компонента тесно связан с жизненным циклом приложения:

1. Регистрация — происходит при запуске приложения в методе Configure.
2. Инициализация — создание экземпляра при первом запросе.
3. Выполнение — обработка каждого запроса.
4. Уничтожение — происходит при остановке приложения.

Большинство middleware-компонентов регистрируются как синглтоны, что означает: один экземпляр обрабатывает все запросы. Это накладывает определённые ограничения — в коде middleware нельзя хранить состояние, зависящее от конкретного запроса, чтобы избежать проблем с потокобезопасностью. Однако существует возможность создавать middleware с областью видимости запроса (request-scoped), когда новый экземпляр создаётся для каждого запроса — для этого используется подход с фабричными методами:

C#
1
app.UseMiddlewareFactory<MyRequestScopedMiddleware>();

Схемы ветвления и условного выполнения



ASP.NET Core предоставляет элегантный способ создания ветвлений в конвейере middleware с помощью методов Map и MapWhen:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
// Ветвление по пути запроса
app.Map("/api", apiApp => {
    apiApp.UseMiddleware<ApiAuthMiddleware>();
    apiApp.UseMiddleware<JsonFormatterMiddleware>();
});
 
// Условное ветвление
app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Special-Header"),
    specialApp => {
        specialApp.UseMiddleware<SpecialHeaderMiddleware>();
    }
);
Эти методы фактически создают вложенные конвейеры для обработки запросов, удовлетворяющих определённым условиям. Когда запрос попадает в такое ответвление, он проходит через все middleware этого ответвления, а затем продолжает путь в основном конвейере. Менее известный способ условного выполнения — использование метода UseWhen:

C#
1
2
3
4
5
6
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/secured"),
    securedApp => {
        securedApp.UseMiddleware<AdditionalSecurityMiddleware>();
    }
);
В отличие от MapWhen, который создаёт изолированный подконвейер, UseWhen выполняет дополнительные middleware и затем возвращает запрос в основной конвейер. Это полезно, когда нужно добавить дополнительную обработку для некоторых запросов, не нарушая общий поток.

Модель двунаправленной обработки запросов



Ключевая особенность middleware в ASP.NET Core — двунаправленная модель обработки, часто называемая "луковичной" (onion model). Каждый middleware фактически разделён на две фазы:
1. Входящая фаза — код до вызова await next().
2. Исходящая фаза — код после возврата из await next().
Это создаёт симметричную структуру, где middleware обрабатывает запрос "на входе" и ответ "на выходе":

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
app.Use(async (context, next) =>
{
    // ВХОДЯЩАЯ ФАЗА
    var timer = Stopwatch.StartNew();
    
    // Передача управления следующему middleware
    await next();
    
    // ИСХОДЯЩАЯ ФАЗА
    timer.Stop();
    var responseTime = timer.ElapsedMilliseconds;
    context.Response.Headers.Add("X-Response-Time", responseTime.ToString());
});
Эта модель особенно удобна для реализации кросс-каттинг функционала, такого как логирование, мониторинг производительности, обработка исключений или модификация ответа сервера перед отправкой клиенту.

Паттерны динамической компоновки middleware в рантайме



Одной из сильных сторон middleware в ASP.NET Core является возможность динамически модифицировать конвейер обработки запросов. В отличие от статической конфигурации, которая определяется при старте приложения, динамическая компоновка позволяет адаптировать поведение конвейера во время выполнения. Достичь этого можно несколькими способами. Один из наиболее элегантных — использование фабричных методов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class MiddlewareFactoryExtensions
{
    public static IApplicationBuilder UseConditionalMiddleware(
        this IApplicationBuilder builder, 
        Func<HttpContext, bool> predicate,
        Func<RequestDelegate, RequestDelegate> middleware)
    {
        return builder.Use(next => 
        {
            var branch = middleware(next);
            
            return context => 
            {
                if (predicate(context))
                    return branch(context);
                    
                return next(context);
            };
        });
    }
}
Этот подход позволяет создавать сложные правила маршрутизации запросов через различные наборы middleware. Например, можно перенаправлять трафик на основе нагрузки, версии API или характеристик клиента. Другой интересный паттерн — "ленивая загрузка" middleware-компонентов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Прокси для ленивой загрузки middleware
public class LazyLoadedMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IServiceProvider _services;
    private volatile RequestDelegate _lazyMiddleware;
    
    public LazyLoadedMiddleware(RequestDelegate next, IServiceProvider services)
    {
        _next = next;
        _services = services;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // Ленивая инициализация при первом запросе
        if (_lazyMiddleware == null)
        {
            var factory = _services.GetRequiredService<IMiddlewareFactory>();
            var middleware = factory.Create<ExpensiveMiddleware>();
            
            var builder = new ApplicationBuilder(_services);
            builder.Use(next => middleware.InvokeAsync);
            builder.Run(_next);
            
            _lazyMiddleware = builder.Build();
        }
        
        await _lazyMiddleware(context);
    }
}
Этот шаблон особенно полезен для "тяжелых" компонентов, которые не всегда нужны при обработке запросов, но требуют значительных ресурсов для инициализации.

Обмен данными между middleware



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

1. HttpContext.Items — словарь для хранения данных в рамках одного запроса:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Первый middleware сохраняет данные
public async Task InvokeAsync(HttpContext context)
{
    context.Items["StartTime"] = DateTime.UtcNow;
    await _next(context);
}
 
// Второй middleware использует эти данные
public async Task InvokeAsync(HttpContext context)
{
    if (context.Items.TryGetValue("StartTime", out var startTimeObj))
    {
        var startTime = (DateTime)startTimeObj;
        var processingTime = DateTime.UtcNow - startTime;
        // Использование данных...
    }
    
    await _next(context);
}
2. Features API — более формализованный подход через интерфейсы:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Определение интерфейса
public interface ITrackingFeature
{
    string TrackingId { get; set; }
}
 
// Реализация
public class TrackingFeature : ITrackingFeature
{
    public string TrackingId { get; set; }
}
 
// Использование в middleware
public async Task InvokeAsync(HttpContext context)
{
    var trackingFeature = new TrackingFeature
    {
        TrackingId = Guid.NewGuid().ToString()
    };
    
    context.Features.Set<ITrackingFeature>(trackingFeature);
    await _next(context);
}
Features API предоставляет более типобезопасный механизм обмена данными, что снижает вероятность ошибок при рефакторинге кода.

Оптимизация производительности middleware



Производительность — критический фактор в веб-приложениях с высокой нагрузкой. Несколько практических советов:

1. Минимизация аллокаций памяти:
C#
1
2
3
4
5
   // Плохо: создает новый массив при каждом запросе
   private readonly byte[] _newLineBytes = new byte[] { 13, 10 };
   
   // Хорошо: использует статический массив
   private static readonly byte[] _newLineBytes = new byte[] { 13, 10 };
2. Повторное использование объектов:
C#
1
   private readonly Regex _pathRegex = new Regex(@"^/api/v\d+/", RegexOptions.Compiled);
3. Избегание блокирующих операций:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
   // Плохо: блокирует поток
   public Task InvokeAsync(HttpContext context)
   {
       Thread.Sleep(100); // Имитация работы
       return _next(context);
   }
   
   // Хорошо: асинхронная операция
   public async Task InvokeAsync(HttpContext context)
   {
       await Task.Delay(100); // Имитация работы
       await _next(context);
   }
4. Кэширование результатов вычислений:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   // Кэш результатов предыдущих запросов
   private static readonly ConcurrentDictionary<string, object> _cache 
       = new ConcurrentDictionary<string, object>();
   
   public async Task InvokeAsync(HttpContext context)
   {
       var key = context.Request.Path;
       if (!_cache.TryGetValue(key, out var result))
       {
           result = await ComputeExpensiveResult(context);
           _cache.TryAdd(key, result);
       }
       
       await WriteResultToResponse(context.Response, result);
       // Пропускаем вызов next(), так как мы уже сформировали ответ
   }

Инструменты диагностики middleware



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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Middleware для отображения текущего конвейера
public class PipelineVisualizerMiddleware
{
    private readonly RequestDelegate _next;
    
    public PipelineVisualizerMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path == "/_debug/pipeline")
        {
            // Получение информации о зарегистрированных middleware
            var services = context.RequestServices;
            var appBuilder = services.GetRequiredService<IApplicationBuilder>();
            
            context.Response.ContentType = "text/html";
            await context.Response.WriteAsync("<h1>Middleware Pipeline</h1>");
            await context.Response.WriteAsync("<ul>");
            
            // Вывод списка middleware
            foreach (var middleware in GetMiddlewareTypes(appBuilder))
            {
                await context.Response.WriteAsync($"<li>{middleware.Name}</li>");
            }
            
            await context.Response.WriteAsync("</ul>");
            return;
        }
        
        await _next(context);
    }
    
    private IEnumerable<Type> GetMiddlewareTypes(IApplicationBuilder app)
    {
        // Упрощено - в реальности требуется рефлексия
        yield break;
    }
}
Для крупных приложений со сложной структурой middleware такие инструменты помогают понять, как именно обрабатываются запросы, и выявить потенциальные проблемы.
В целом, middleware в ASP.NET Core предоставляет мощный и гибкий фреймворк для обработки HTTP-запросов. Понимание нюансов его работы и использование правильных паттернов проектирования позволяют создавать высокопроизводительные и хорошо структурированные веб-приложения.

Разработка собственных Middleware-компонентов



Простые встраиваемые обработчики



Начнём с самого простого — встраиваемых (inline) обработчиков. Это компактные блоки кода, добавляемые напрямую в метод Configure:

C#
1
2
3
4
5
6
7
8
9
10
app.Use(async (context, next) =>
{
    var userAgent = context.Request.Headers["User-Agent"].ToString();
    Console.WriteLine($"User-Agent: {userAgent}");
    
    await next();
    
    // На обратном пути добавляем заголовок к ответу
    context.Response.Headers.Add("X-Processed-By", "My Custom Middleware");
});
Такой подход хорош для простых сценариев или быстрого прототипирования. Однако, как с любой встраиваемой логикой, возникают проблемы масштабирования и переиспользования. Как только вы захотите применить тот же функционал в другом проекте — придётся копировать код.

Полноценные классы middleware



Для более серьёзных задач рекомендуется создавать полноценные классы middleware. Это обеспечивает лучшую изоляцию, тестируемость и повторное использование:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class RequestLoggerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggerMiddleware> _logger;
    
    public RequestLoggerMiddleware(RequestDelegate next, ILogger<RequestLoggerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation("Request started: {Method} {Path}", 
                              context.Request.Method, 
                              context.Request.Path);
        
        var sw = Stopwatch.StartNew();
        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();
            _logger.LogInformation("Request completed in {ElapsedMs}ms with status {StatusCode}", 
                                  sw.ElapsedMilliseconds, 
                                  context.Response.StatusCode);
        }
    }
}
Для регистрации такого middleware в приложении используется метод расширения:

C#
1
app.UseMiddleware<RequestLoggerMiddleware>();
Интересный нюанс: имя метода обработки — InvokeAsync. ASP.NET Core будет искать именно этот метод, чтобы вызвать его при прохождении запроса через middleware. Отсутствие правильного метода приведёт к ошибке во время выполнения.

Создание методов расширения для middleware



Чтобы сделать использование middleware более элегантным и соответствующим общей конвенции ASP.NET Core, стоит создать метод расширения:

C#
1
2
3
4
5
6
7
8
public static class RequestLoggerMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogger(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggerMiddleware>();
    }
}
Теперь регистрация выглядит более читабельно:

C#
1
app.UseRequestLogger();
Этот шаблон повторяется во всем фреймворке, где методы начинаются с Use, за которым следует название функциональности.

Внедрение зависимостей в middleware



Одно из мощных преимуществ ASP.NET Core — встроенный контейнер внедрения зависимостей (Dependency Injection). Middleware-компоненты могут получать сервисы через конструктор:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class GeoLocationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IGeoService _geoService;
    private readonly GeoOptions _options;
    
    public GeoLocationMiddleware(
        RequestDelegate next, 
        IGeoService geoService, 
        IOptions<GeoOptions> options)
    {
        _next = next;
        _geoService = geoService;
        _options = options.Value;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var ipAddress = context.Connection.RemoteIpAddress;
        if (ipAddress != null && _options.EnableIpLookup)
        {
            var location = await _geoService.LookupLocationAsync(ipAddress);
            context.Items["UserLocation"] = location;
        }
        
        await _next(context);
    }
}
Кроме того, middleware может получать зависимости через метод InvokeAsync, которые будут разрешаться для каждого запроса:

C#
1
2
3
4
5
6
7
public async Task InvokeAsync(HttpContext context, IUserService userService)
{
    // userService будет создаваться для каждого запроса
    var user = await userService.GetUserAsync(context.User);
    // ...
    await _next(context);
}
Это ключевая разница: зависимости, внедренные через конструктор, существуют на протяжении всего жизненного цикла middleware (обычно — весь срок работы приложения), а зависимости в InvokeAsync создаются заново для каждого запроса.

Асинхронная обработка в middleware



Асинхронность — не просто модный термин, а необходимость для высокопроизводительных веб-приложений. ASP.NET Core проектировался с учётом асинхронности с самого начала:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task InvokeAsync(HttpContext context)
{
    // Асинхронная операция до передачи управления
    await PrepareRequestAsync(context);
    
    // Передача управления следующему middleware
    await _next(context);
    
    // Асинхронная операция после возврата
    await FinalizeResponseAsync(context);
}
 
private async Task PrepareRequestAsync(HttpContext context)
{
    // Имитация асинхронной операции
    await Task.Delay(10); // В реальности здесь может быть запрос к БД или внешнему API
}
 
private async Task FinalizeResponseAsync(HttpContext context)
{
    // Ещё одна асинхронная операция
    await Task.Delay(5);
}
Избегайте распространенных ошибок при работе с асинхронным кодом:
  1. Использование блокирующих вызовов (Task.Result, .Wait()) — это может привести к дедлокам.
  2. Забывание ключевого слова await — код будет выполняться асинхронно, но возможны непредсказуемые результаты.
  3. Возвращение Task.CompletedTask вместо await _next(context) — это нарушает правильный поток выполнения.

Маппинг middleware на определённые маршруты



Часто middleware должен применяться только к определённым URL. Для этого ASP.NET Core предлагает несколько подходов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Применение middleware только к определённому пути
app.Map("/api", apiApp =>
{
    apiApp.UseApiKeyMiddleware();
    apiApp.UseCustomHeaderMiddleware();
});
 
// Условное применение middleware
app.MapWhen(
    context => context.Request.Query.ContainsKey("debug"),
    debugApp => {
        debugApp.UseDebugMiddleware();
    }
);
Более тонкий контроль можно получить с помощью пользовательской логики в самом middleware:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public async Task InvokeAsync(HttpContext context)
{
    // Проверяем путь запроса
    if (context.Request.Path.StartsWithSegments("/secured"))
    {
        // Логика для защищённых маршрутов
        if (!IsAuthenticated(context))
        {
            context.Response.StatusCode = 401; // Unauthorized
            return; // Прерываем конвейер
        }
    }
    
    await _next(context);
}

Стратегии обработки ошибок



В middleware ошибки могут возникать как до, так и после вызова _next. Эффективная стратегия обработки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public async Task InvokeAsync(HttpContext context)
{
    try
    {
        await _next(context);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "An unhandled exception occurred");
        
        // Предотвращаем утечку деталей об ошибке в продакшн
        var isDevelopment = Environment.IsDevelopment();
        
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";
        
        var response = new { 
            error = isDevelopment ? ex.Message : "An error occurred", 
            stackTrace = isDevelopment ? ex.StackTrace : null 
        };
        
        await context.Response.WriteAsJsonAsync(response);
    }
}
Этот шаблон — основа для создания глобальных обработчиков исключений, которые могут регистрировать ошибки, отправлять уведомления или выполнять другие действия при возникновении проблем.

Оптимизация памяти при работе с большими объемами данных



Обработка больших объёмов данных — частая задача в веб-приложениях. Будь то загрузка файлов, стриминг медиа, или парсинг JSON-документов, неэффективная работа с памятью может быстро превратить ваше приложение в прожорливого монстра, пожирающего системные ресурсы. Рассмотрим middleware для обработки больших файлов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public async Task InvokeAsync(HttpContext context)
{
    if (context.Request.Path.StartsWithSegments("/upload"))
    {
        // ПЛОХО: загружает весь файл в память
        using var memoryStream = new MemoryStream();
        await context.Request.Body.CopyToAsync(memoryStream);
        var fileBytes = memoryStream.ToArray();
        // Дальнейшая обработка...
    }
    
    await _next(context);
}
Этот код работает, но легко может вызвать OutOfMemoryException при загрузке больших файлов. Вместо этого используйте потоковую обработку:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
public async Task InvokeAsync(HttpContext context)
{
    if (context.Request.Path.StartsWithSegments("/upload"))
    {
        // ХОРОШО: обрабатывает данные потоково
        using var fileStream = File.Create(Path.GetTempFileName());
        await context.Request.Body.CopyToAsync(fileStream);
        fileStream.Position = 0;
        // Дальнейшая обработка...
    }
    
    await _next(context);
}
Для ещё лучшей производительности можно использовать буферизацию с фиксированным размером буфера:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public async Task InvokeAsync(HttpContext context)
{
    if (context.Request.Path.StartsWithSegments("/upload"))
    {
        using var fileStream = File.Create(Path.GetTempFileName());
        var buffer = new byte[8192]; // 8KB буфер
        int bytesRead;
        
        while ((bytesRead = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
        {
            await fileStream.WriteAsync(buffer, 0, bytesRead);
            // Можно добавить обработку каждого чанка данных
        }
    }
    
    await _next(context);
}
Ещё одна распространённая ошибка — многократное чтение тела запроса. По умолчанию его можно прочитать только один раз:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public async Task InvokeAsync(HttpContext context)
{
    // Делаем тело запроса доступным для повторного чтения
    context.Request.EnableBuffering();
    
    // Первое чтение 
    using (var reader = new StreamReader(
        context.Request.Body,
        encoding: Encoding.UTF8,
        detectEncodingFromByteOrderMarks: false,
        leaveOpen: true))
    {
        var body = await reader.ReadToEndAsync();
        // Обработка body...
        
        // Важно: сбросить позицию потока
        context.Request.Body.Position = 0;
    }
    
    // Теперь другие middleware могут читать тело запроса
    await _next(context);
}

Использование ValueTask для оптимизации производительности



Начиная с C# 7.0, платформа .NET предлагает ValueTask<T> — структуру, которая может повысить производительность асинхронных операций, особенно если они часто завершаются синхронно:

C#
1
2
3
4
5
6
7
8
9
10
11
public ValueTask<string> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out var cachedValue))
    {
        // Значение в кэше — возвращаем немедленно без создания Task
        return new ValueTask<string>((string)cachedValue);
    }
    
    // Значения нет — делаем "тяжёлую" асинхронную операцию
    return new ValueTask<string>(FetchValueFromDatabaseAsync(key));
}
В middleware это может выглядеть так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class OptimizedMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;
    
    public OptimizedMiddleware(RequestDelegate next, IMemoryCache cache)
    {
        _next = next;
        _cache = cache;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var cacheKey = context.Request.Path;
        
        // Пытаемся получить данные из кэша
        var cachedValue = await GetCachedDataAsync(cacheKey);
        if (cachedValue != null)
        {
            await context.Response.WriteAsync(cachedValue);
            return;
        }
        
        await _next(context);
    }
    
    private ValueTask<string> GetCachedDataAsync(string key)
    {
        if (_cache.TryGetValue(key, out string value))
        {
            return new ValueTask<string>(value);
        }
        
        return new ValueTask<string>(FetchAndCacheDataAsync(key));
    }
    
    private async Task<string> FetchAndCacheDataAsync(string key)
    {
        // Имитация тяжелой операции
        await Task.Delay(100);
        var result = $"Data for {key} at {DateTime.UtcNow}";
        
        _cache.Set(key, result, TimeSpan.FromMinutes(10));
        return result;
    }
}

Создание конвейерных middleware-компонентов



Для сложных сценариев можно создавать middleware, которые сами формируют мини-конвейеры обработки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CompositeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly RequestDelegate _pipeline;
    
    public CompositeMiddleware(RequestDelegate next, IServiceProvider services)
    {
        _next = next;
        
        // Создаём внутренний конвейер из нескольких middleware
        var builder = new ApplicationBuilder(services);
        builder.UseMiddleware<FirstComponentMiddleware>();
        builder.UseMiddleware<SecondComponentMiddleware>();
        builder.UseMiddleware<ThirdComponentMiddleware>();
        builder.Run(ctx => next(ctx)); // Подключаем внешний конвейер
        
        _pipeline = builder.Build();
    }
    
    public Task InvokeAsync(HttpContext context)
    {
        // Запускаем внутренний конвейер, который в конце вызовет _next
        return _pipeline(context);
    }
}
Этот подход позволяет инкапсулировать сложную логику обработки, представив её как единый компонент для основного конвейера приложения.

Параллельная обработка в middleware



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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public async Task InvokeAsync(HttpContext context)
{
    // Запускаем несколько задач параллельно
    var userDataTask = FetchUserDataAsync(context.User);
    var preferencesTask = FetchPreferencesAsync(context.User);
    var analyticsTask = TrackRequestAsync(context);
    
    // Дожидаемся завершения всех задач
    await Task.WhenAll(userDataTask, preferencesTask, analyticsTask);
    
    // Используем результаты
    context.Items["UserData"] = userDataTask.Result;
    context.Items["Preferences"] = preferencesTask.Result;
    
    await _next(context);
}
Однако помните о потенциальных проблемах:
  • Параллельное выполнение увеличивает потребление ресурсов.
  • Возможны гонки данных при доступе к общим ресурсам.
  • Необходимо правилно обрабатывать ошибки во всех задачах.

Сигналы отмены и тайм-ауты



Обработка запроса может быть прервана клиентом, или может потребоваться установить тайм-аут для определённых операций:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public async Task InvokeAsync(HttpContext context)
{
    // Получаем токен отмены из контекста запроса
    var requestAborted = context.RequestAborted;
    
    // Создаём токен с тайм-аутом 5 секунд
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    using var linkedCts = CancellationTokenSource
        .CreateLinkedTokenSource(requestAborted, timeoutCts.Token);
    
    try
    {
        // Передаём токен в асинхронную операцию
        var result = await ExpensiveOperationAsync(linkedCts.Token);
        context.Items["OperationResult"] = result;
    }
    catch (OperationCanceledException)
    {
        if (timeoutCts.IsCancellationRequested)
        {
            context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
            await context.Response.WriteAsync("Operation timed out");
            return;
        }
        
        // Клиент прервал запрос
        context.Abort();
        return;
    }
    
    await _next(context);
}
Такой подход позволяет избежать блокировки ресурсов сервера при долгих операциях или при разрыве соединения с клиентом.

Практические сценарии применения



Теория middleware восхитительна своей элегантностью, но настоящая магия начинается при создании практических решений. Рассмотрим наиболее распространённые сценарии применения middleware в боевых приложениях ASP.NET Core.

Логирование и мониторинг



Логирование — краеугольный камень отладки и диагностики проблем. С помощью middleware можно реализовать централизованное логирование всех запросов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
 
    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString();
        
        // Записываем информацию о начале запроса
        _logger.LogInformation("Запрос {RequestId}: {Method} {Url} начат", 
            requestId, context.Request.Method, context.Request.Path);
 
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Запрос {RequestId} завершился ошибкой", requestId);
            throw; // Пробрасываем исключение дальше
        }
        finally
        {
            stopwatch.Stop();
            // Записываем информацию о завершении запроса
            _logger.LogInformation("Запрос {RequestId}: завершён со статусом {StatusCode} за {ElapsedMs}мс", 
                requestId, context.Response.StatusCode, stopwatch.ElapsedMilliseconds);
        }
    }
}
Этот middleware не только логирует запросы, но и измеряет время их выполнения, что помогает выявлять "узкие места" в производительности.

Межсервисное взаимодействие и API-шлюзы



В микросервисной архитектуре middleware отлично работает в качестве API-шлюза, маршрутизируя запросы между сервисами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ApiGatewayMiddleware
{
    private readonly RequestDelegate _next;
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _config;
 
    public ApiGatewayMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _config = config;
        _httpClient = new HttpClient();
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;
        
        // Определяем, к какому сервису направляется запрос
        if (path.StartsWith("/users"))
        {
            await ProxyRequest(context, "user-service");
            return;
        }
        
        if (path.StartsWith("/orders"))
        {
            await ProxyRequest(context, "order-service");
            return;
        }
        
        // Если нет подходящего сервиса, продолжаем стандартную обработку
        await _next(context);
    }
 
    private async Task ProxyRequest(HttpContext context, string serviceName)
    {
        var serviceUrl = _config[$"Services:{serviceName}:Url"];
        var targetUrl = $"{serviceUrl}{context.Request.Path}{context.Request.QueryString}";
        
        // ... логика проксирования запроса ...
    }
}

Обработка исключений



Вместо разбросанных по коду try/catch блоков, middleware позволяет централизованно обрабатывать все исключения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly IWebHostEnvironment _env;
 
    public ExceptionHandlingMiddleware(
        RequestDelegate next, 
        ILogger<ExceptionHandlingMiddleware> logger,
        IWebHostEnvironment env)
    {
        _next = next;
        _logger = logger;
        _env = env;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Необработаное исключение");
            
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = "application/json";
            
            var response = _env.IsDevelopment()
                ? new { error = ex.Message, stackTrace = ex.StackTrace }
                : new { error = "Произошла внутренняя ошибка сервера" };
            
            await context.Response.WriteAsJsonAsync(response);
        }
    }
}

Кэширование и сжатие ответов



Повышение производительности — ещё одно применение middleware. Например, простая реализация кэширования ответов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class SimpleCachingMiddleware
{
    private readonly RequestDelegate _next;
    private static readonly ConcurrentDictionary<string, CachedResponse> _cache 
        = new ConcurrentDictionary<string, CachedResponse>();
 
    public SimpleCachingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        // Кэшируем только GET-запросы
        if (!HttpMethods.IsGet(context.Request.Method))
        {
            await _next(context);
            return;
        }
 
        var cacheKey = context.Request.Path.Value;
        
        if (_cache.TryGetValue(cacheKey, out var cachedResponse) && !cachedResponse.Expired)
        {
            // Возвращаем кэшированный ответ
            context.Response.StatusCode = cachedResponse.StatusCode;
            context.Response.ContentType = cachedResponse.ContentType;
            await context.Response.WriteAsync(cachedResponse.Body);
            return;
        }
 
        // Сохраняем оригинальный поток ответа
        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;
 
        try
        {
            await _next(context);
            
            if (context.Response.StatusCode == 200)
            {
                // Сохраняем ответ в кэш
                responseBody.Seek(0, SeekOrigin.Begin);
                var responseBodyText = await new StreamReader(responseBody).ReadToEndAsync();
                
                var response = new CachedResponse
                {
                    Body = responseBodyText,
                    ContentType = context.Response.ContentType,
                    StatusCode = context.Response.StatusCode,
                    Cached = DateTime.UtcNow,
                    Expired = false
                };
                
                _cache.TryAdd(cacheKey, response);
                
                // Сбрасываем позицию потока для дальнейшего чтения
                responseBody.Seek(0, SeekOrigin.Begin);
            }
        }
        finally
        {
            // Копируем измененный ответ в оригинальный поток
            await responseBody.CopyToAsync(originalBodyStream);
            context.Response.Body = originalBodyStream;
        }
    }
 
    private class CachedResponse
    {
        public string Body { get; set; }
        public string ContentType { get; set; }
        public int StatusCode { get; set; }
        public DateTime Cached { get; set; }
        public bool Expired { get; set; }
    }
}

Аутентификация и авторизация



Проверка JWT-токенов — ещё один идеальный сценарий для middleware:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class JwtMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _secret;
 
    public JwtMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _secret = config["Jwt:Key"];
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var token = context.Request.Headers["Authorization"]
            .FirstOrDefault()?.Split(" ").Last();
 
        if (token != null)
            AttachUserToContext(context, token);
 
        await _next(context);
    }
 
    private void AttachUserToContext(HttpContext context, string token)
    {
        try
        {
            var key = Encoding.ASCII.GetBytes(_secret);
            var handler = new JwtSecurityTokenHandler();
            
            // Валидация токена...
            
            // При успехе добавляем информацию о пользователе в контекст
            var userId = "extracted-user-id";
            context.Items["User"] = userId;
        }
        catch
        {
            // При ошибке валидации просто продолжаем без аутентификации
        }
    }
}

Реализация rate-limiting



Защита API от перегрузки — критически важная задача для публичных сервисов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;
    private readonly int _maxRequests;
    private readonly TimeSpan _window;
 
    public RateLimitingMiddleware(
        RequestDelegate next,
        IMemoryCache cache,
        IConfiguration config)
    {
        _next = next;
        _cache = cache;
        _maxRequests = config.GetValue<int>("RateLimit:MaxRequests", 100);
        _window = TimeSpan.FromSeconds(config.GetValue<int>("RateLimit:WindowSeconds", 60));
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var cacheKey = $"rate_limit_{clientIp}";
        
        var counter = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _window;
            return 0;
        });
        
        // Увеличиваем счётчик
        counter++;
        _cache.Set(cacheKey, counter, _window);
        
        // Добавляем заголовки с информацией о лимитах
        context.Response.Headers.Add("X-RateLimit-Limit", _maxRequests.ToString());
        context.Response.Headers.Add("X-RateLimit-Remaining", Math.Max(0, _maxRequests - counter).ToString());
        
        if (counter > _maxRequests)
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsync("Too many requests. Please try again later.");
            return;
        }
        
        await _next(context);
    }
}

Многоуровневая валидация входящих данных



Валидация запросов на разных уровнях помогает обеспечить целостность данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IEnumerable<IValidator> _validators;
 
    public ValidationMiddleware(RequestDelegate next, IEnumerable<IValidator> validators)
    {
        _next = next;
        _validators = validators;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        foreach (var validator in _validators)
        {
            if (validator.CanValidate(context))
            {
                var result = await validator.ValidateAsync(context);
                if (!result.IsValid)
                {
                    context.Response.StatusCode = StatusCodes.Status400BadRequest;
                    await context.Response.WriteAsJsonAsync(new { errors = result.Errors });
                    return;
                }
            }
        }
        
        await _next(context);
    }
}
 
// Интерфейс для валидаторов
public interface IValidator
{
    bool CanValidate(HttpContext context);
    Task<ValidationResult> ValidateAsync(HttpContext context);
}
Эти примеры демонстрируют универсальность middleware как архитектурного паттерна. От простого логирования до сложной обработки запросов — middleware обеспечивает гибкий и модульный подход к решению различных задач в веб-приложениях ASP.NET Core.

Сжатие ответов



Сокращение объёма передаваемых данных — важный фактор оптимизации веб-приложений. Middleware для сжатия ответов может значительно уменьшить трафик:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CompressionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly CompressionLevel _compressionLevel;
 
    public CompressionMiddleware(RequestDelegate next, CompressionLevel compressionLevel = CompressionLevel.Fastest)
    {
        _next = next;
        _compressionLevel = compressionLevel;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var acceptEncoding = context.Request.Headers["Accept-Encoding"].ToString().ToLowerInvariant();
        
        // Проверяем, поддерживает ли клиент сжатие
        if (acceptEncoding.Contains("gzip"))
        {
            // Сохраняем оригинальный поток ответа
            var originalBody = context.Response.Body;
            using var compressedBody = new MemoryStream();
            
            // Заменяем поток ответа на сжимающий
            context.Response.Body = compressedBody;
            context.Response.Headers.Add("Content-Encoding", "gzip");
            
            await _next(context);
            
            // После обработки запроса сжимаем результат
            compressedBody.Seek(0, SeekOrigin.Begin);
            using var gzipStream = new GZipStream(originalBody, _compressionLevel, true);
            await compressedBody.CopyToAsync(gzipStream);
        }
        else
        {
            await _next(context);
        }
    }
}
Хотя ASP.NET Core имеет встроенное middleware для сжатия, написание собственного варианта даёт больше контроля над процессом и помогает понять внутренние механизмы.

Локализация



Поддержка мультиязычности критически важна для глобальных приложений. Middleware может автоматически определять предпочтительный язык пользователя:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class LocalizationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IStringLocalizer _localizer;
 
    public LocalizationMiddleware(RequestDelegate next, IStringLocalizer<LocalizationMiddleware> localizer)
    {
        _next = next;
        _localizer = localizer;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        // Извлекаем предпочтительный язык из заголовков или куки
        var culture = GetPreferredCulture(context);
        
        // Устанавливаем культуру для текущего запроса
        var cultureInfo = new CultureInfo(culture);
        CultureInfo.CurrentCulture = cultureInfo;
        CultureInfo.CurrentUICulture = cultureInfo;
        
        // Сохраняем выбор пользователя
        context.Response.Cookies.Append("LANG-PREF", culture,
            new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
        
        await _next(context);
    }
 
    private string GetPreferredCulture(HttpContext context)
    {
        // Сначала проверяем куки
        if (context.Request.Cookies.TryGetValue("LANG-PREF", out var cookieCulture))
            return cookieCulture;
            
        // Затем смотрим заголовок Accept-Language
        var acceptLang = context.Request.Headers["Accept-Language"].ToString();
        var langHeader = acceptLang.Split(',').FirstOrDefault();
        if (!string.IsNullOrEmpty(langHeader))
            return langHeader.Split(';').First().Trim();
            
        return "en-US"; // Язык по умолчанию
    }
}

Мониторинг состояния приложения



Для обеспечения высокой доступности приложения необходим мониторинг его "здоровья":

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class HealthCheckMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<HealthCheckMiddleware> _logger;
    private readonly IHealthCheckService _healthService;
 
    public HealthCheckMiddleware(
        RequestDelegate next, 
        ILogger<HealthCheckMiddleware> logger,
        IHealthCheckService healthService)
    {
        _next = next;
        _logger = logger;
        _healthService = healthService;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path.Equals("/health"))
        {
            var result = await _healthService.CheckHealthAsync();
            context.Response.ContentType = "application/json";
            
            if (result.Status == HealthStatus.Healthy)
            {
                context.Response.StatusCode = StatusCodes.Status200OK;
                await context.Response.WriteAsJsonAsync(new { status = "healthy" });
            }
            else
            {
                _logger.LogWarning("Система сообщила о нездоровом состоянии");
                context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
                await context.Response.WriteAsJsonAsync(new { 
                    status = "unhealthy",
                    issues = result.Issues
                });
            }
            
            return;
        }
        
        await _next(context);
    }
}
Эти дополнительные примеры расширяют арсенал техник, используемых в middleware-компонентах. Красота middleware заключается в модульности и многократном использовании — написав компонент один раз, его можно применять в различных проектах, обеспечивая консистентность поведения приложений.

Техники тестирования и отладки Middleware



Любой код, написанный без должного тестирования, рано или поздно станет источником проблем. Middleware-компоненты не исключение — их тестирование имеет ряд особенностей, связанных с зависимостью от конвейера обработки запросов и асинхронной природой.

Профилирование производительности middleware-компонентов



Производительность — одна из ключевых метрик для middleware, ведь каждый запрос проходит через всю цепочку компонентов. Даже небольшая задержка в одном middleware многократно умножается при высокой нагрузке.
Простейший подход к профилированию — встраивание замеров времени:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public async Task InvokeAsync(HttpContext context)
{
    var sw = Stopwatch.StartNew();
    
    await _next(context);
    
    sw.Stop();
    _logger.LogDebug(
        "Middleware {Name} выполнялся {ElapsedMs}мс",
        GetType().Name,
        sw.ElapsedMilliseconds);
}
Для более глубокого анализа можно использовать инструменты трассировки, например, Activity API из System.Diagnostics:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public async Task InvokeAsync(HttpContext context)
{
    using var activity = new Activity("MyMiddleware.Execute")
        .SetTag("http.method", context.Request.Method)
        .SetTag("http.url", context.Request.Path)
        .Start();
    
    try
    {
        await _next(context);
        activity.SetTag("status", "success");
    }
    catch (Exception ex)
    {
        activity.SetTag("status", "error");
        activity.SetTag("error.type", ex.GetType().Name);
        activity.SetTag("error.message", ex.Message);
        throw;
    }
}
Такой подход позволяет не только измерять время выполнения, но и собирать детальную телеметрию о работе middleware, особенно в распределённых системах.

Техники построения моков для unit-тестирования middleware



Unit-тестирование middleware требует имитации HttpContext и RequestDelegate. Вот базовый шаблон для тестов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Fact]
public async Task MyMiddleware_ShouldAddResponseHeader()
{
    // Arrange
    var context = new DefaultHttpContext();
    var nextCalled = false;
    
    RequestDelegate next = (HttpContext ctx) =>
    {
        nextCalled = true;
        return Task.CompletedTask;
    };
    
    var middleware = new MyMiddleware(next);
    
    // Act
    await middleware.InvokeAsync(context);
    
    // Assert
    Assert.True(nextCalled);
    Assert.Equal("ExpectedValue", context.Response.Headers["X-Custom-Header"]);
}
Для более сложных сценариев полезно использовать библиотеки для моков, например, Moq:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[Fact]
public async Task AuthMiddleware_ShouldBlockUnauthorizedRequests()
{
    // Arrange
    var mockContext = new DefaultHttpContext();
    mockContext.Request.Headers["Authorization"] = "InvalidToken";
    
    var mockLogger = new Mock<ILogger<AuthMiddleware>>();
    
    bool nextCalled = false;
    RequestDelegate next = (HttpContext ctx) => {
        nextCalled = true;
        return Task.CompletedTask;
    };
    
    var middleware = new AuthMiddleware(next, mockLogger.Object);
    
    // Act
    await middleware.InvokeAsync(mockContext);
    
    // Assert
    Assert.False(nextCalled); // Следующий middleware не должен вызываться
    Assert.Equal(401, mockContext.Response.StatusCode); // Должен вернуть 401 Unauthorized
    
    // Проверяем, что было вызвано логирование с нужным уровнем и сообщением
    mockLogger.Verify(
        x => x.Log(
            LogLevel.Warning,
            It.IsAny<EventId>(),
            It.Is<It.IsAnyType>((o, t) => o.ToString().Contains("Invalid token")),
            It.IsAny<Exception>(),
            It.IsAny<Func<It.IsAnyType, Exception, string>>()),
        Times.Once);
}

Интеграционное тестирование цепочек middleware-компонентов



Хотя unit-тесты полезны, они не проверяют взаимодействие middleware в реальном конвейере. Для этого нужны интеграционные тесты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MiddlewareIntegrationTests
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    
    public MiddlewareIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }
    
    [Fact]
    public async Task RequestPipeline_ShouldProcessRequestCorrectly()
    {
        // Arrange
        var client = _factory.CreateClient();
        
        // Act
        var response = await client.GetAsync("/api/test");
        
        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
        Assert.Contains("X-Response-Time", response.Headers.Select(h => h.Key));
    }
    
    [Fact]
    public async Task AuthenticationMiddleware_ShouldBlockUnauthenticatedRequests()
    {
        // Arrange
        var client = _factory.CreateClient();
        
        // Act
        var response = await client.GetAsync("/secured/resource");
        
        // Assert
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }
}
Такие тесты позволяют проверить работу всего конвейера middleware в условиях, близких к реальным, но с возможностью настройки тестового окружения.

Отладка и диагностика middleware в режиме разработки



ASP.NET Core имеет встроенные средства для отладки, особенно полезные в режиме разработки. Например, middleware для отображения исключений:

C#
1
2
3
4
5
6
7
8
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
}
Можно создать и свой диагностический middleware, который активируется только в режиме разработки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class DiagnosticMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IWebHostEnvironment _env;
    
    public DiagnosticMiddleware(RequestDelegate next, IWebHostEnvironment env)
    {
        _next = next;
        _env = env;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (_env.IsDevelopment() && context.Request.Query.ContainsKey("_debug"))
        {
            await WriteDiagnosticInfo(context);
            return;
        }
        
        await _next(context);
    }
    
    private async Task WriteDiagnosticInfo(HttpContext context)
    {
        context.Response.ContentType = "text/html";
        await context.Response.WriteAsync("<h1>Diagnostic Info</h1>");
        await context.Response.WriteAsync("<h2>Request Headers</h2>");
        await context.Response.WriteAsync("<ul>");
        
        foreach (var header in context.Request.Headers)
        {
            await context.Response.WriteAsync($"<li>{header.Key}: {header.Value}</li>");
        }
        
        await context.Response.WriteAsync("</ul>");
        
        // Можно добавить и другую диагностическую информацию
    }
}
Эффективное тестирование и отладка middleware требуют комбинации подходов — от простых unit-тестов до полномасштабных интеграционных проверок и специализированных инструментов профилирования. Инвестиции в качественное тестирование окупаются многократно, особенно когда речь идет о критически важных компонентах веб-приложения, которыми и являются middleware.

Есть ли возможность сделать Middleware в WebAPI, сделанном на ASP.NET Framework?
При написании проекта столкнулся с необходимостью предворительной проверки запросов. Для этого...

ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS
Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует...

ASP.NET MVC или ASP.NET Core
Добрый вечер, подскажите что лучшие изучать ASP.NET MVC или ASP.NET Core ? Как я понимаю ASP.NET...

Что выбрать ASP.NET или ASP.NET Core ?
Добрый день форумчане, хотелось бы услышать ваше мнение, какой из перечисленных фреймворков лучше...

ASP.NET Core или ASP.NET MVC
Здравствуйте После изучение основ c# я решил выбрать направление веб разработки. Подскажите какие...

Стоит ли учить asp.net, если скоро станет asp.net core?
Всем привет Если я правильно понимаю, лучше учить Core ?

ASP.NET или ASP.NET Core
Добрый вечер, подскажите новичку в чем разница между asp.net и asp.net core, нужно ли знать оба...

Почему скрипт из ASP.NET MVC 5 не работает в ASP.NET Core?
В представлении в версии ASP.NET MVC 5 был скрипт: @model RallyeAnmeldung.Cars ...

Кастомные атрибуты валидации в ASP.NET MVC
Добрый день. Вот такой вопрос по пользовательским атрибутам валидации. Написан кастомный атрибут...

Кто может подсказать, как правильно создавать кастомные хелперы в asp.net mvc?
Здравствуйте. Кто может подсказать новичку в asp.net mvc как правильно создавать кастомные хелперы?...

Asp.net core rc 2 и Entity Framework core
Добрый день, кто-нибудь уже перешел на новую версию фреймверка? Хотелось бы получить пример. ...

ASP.NET Core + EF Core: ошибка при обновлении БД после создания миграции
Всем привет! Начал осваивать ASP.NET Core: создал проект &quot;Веб-приложение&quot; без Identity. Сразу же...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
Паттерны проектирования GoF на C#
UnmanagedCoder 13.05.2025
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .
Создаем CLI приложение на Python с Prompt Toolkit
py-thonny 13.05.2025
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru