Приходилось ли вам писать кастомные middleware в ASP.NET Core? Приведите пример.

Ответ

Да, создание кастомного middleware — стандартная практика для сквозной функциональности (cross-cutting concerns). Например, для логирования, обработки исключений, кастомной аутентификации или добавления специфичных заголовков.

Пример: Middleware для логирования времени выполнения запроса и добавления корреляционного ID.

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Генерация уникального ID для отслеживания цепочки запроса.
        var correlationId = Guid.NewGuid().ToString();
        context.Items["CorrelationId"] = correlationId;

        var stopwatch = Stopwatch.StartNew();

        // Логируем начало обработки.
        _logger.LogInformation(
            "Starting request {Method} {Path} with CorrelationId: {CorrelationId}",
            context.Request.Method,
            context.Request.Path,
            correlationId
        );

        try
        {
            // Передаём управление следующему компоненту в конвейере.
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            // Логируем завершение обработки и время выполнения.
            _logger.LogInformation(
                "Completed request {Method} {Path} with status {StatusCode} in {ElapsedMs}ms. CorrelationId: {CorrelationId}",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds,
                correlationId
            );
        }
    }
}

// Метод расширения для удобной регистрации.
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggingMiddleware>();
    }
}

Регистрация в конвейере (Program.cs):

var app = builder.Build();

// Порядок важен! Логирование должно быть одним из первых.
app.UseRequestLogging();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Зачем это нужно? Такой middleware централизует логирование для всех запросов, добавляет полезный контекст (CorrelationId) и помогает в диагностике проблем с производительностью, не загрязняя код контроллеров.

Ответ 18+ 🔞

Ну что за вопрос, конечно можно! Это ж как раз та самая магия, когда тебе нужно запилить какую-то общую фигню на все запросы — логирование, проверку прав, обработку ошибок. Чтобы не копипастить одно и то же в каждый контроллер, а просто один раз написал и забыл, как страшный сон.

Вот смотри, например, если тебе нужно залогировать, сколько времени запрос тормозит, и прикрутить к нему уникальный айдишник, чтобы потом в логах не еб**ться, где чьи следы. Делается это просто, как три копейки:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Генерим уникальный ключ для отслеживания этого конкретного запроса по всем сервисам.
        var correlationId = Guid.NewGuid().ToString();
        context.Items["CorrelationId"] = correlationId;

        var stopwatch = Stopwatch.StartNew();

        // Пишем в лог, что вот, мол, начали работать, не мешайте.
        _logger.LogInformation(
            "Starting request {Method} {Path} with CorrelationId: {CorrelationId}",
            context.Request.Method,
            context.Request.Path,
            correlationId
        );

        try
        {
            // А теперь пускаем эту телегу дальше по конвейеру.
            await _next(context);
        }
        finally
        {
            stopwatch.Stop();
            // И когда всё закончилось (успешно или нет — неважно), пишем, сколько времени это безобразие длилось.
            _logger.LogInformation(
                "Completed request {Method} {Path} with status {StatusCode} in {ElapsedMs}ms. CorrelationId: {CorrelationId}",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds,
                correlationId
            );
        }
    }
}

// А это такой красивый метод-расширение, чтобы в основном коде всё выглядело опрятно.
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggingMiddleware>();
    }
}

А вот куда эту штуку впихнуть (Program.cs):

var app = builder.Build();

// Важно ставить это одним из первых, чтобы логировалось ВСЁ, что происходит дальше.
app.UseRequestLogging();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

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