.NET ASP.NET Core Middleware Arquitetura Performance

ASP.NET Core middleware pipeline: criando middlewares customizados do zero

Guia completo do pipeline de middleware no ASP.NET Core: ordem de execução, middleware customizado, convention-based vs factory-based.

N
Neryx Digital Architects
9 de setembro de 2025
11 min de leitura
210 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

O middleware pipeline é o coração do ASP.NET Core: toda requisição HTTP passa por uma sequência de componentes antes de chegar ao seu controller ou endpoint. Entender como funciona — e como criar middlewares customizados — é essencial para implementar cross-cutting concerns de forma limpa e reutilizável.

Como o pipeline funciona

O pipeline é uma cadeia de delegates. Cada middleware recebe a requisição, pode processá-la, e decide se passa para o próximo componente ou encerra o fluxo:

Request ──▶ [Auth] ──▶ [RateLimit] ──▶ [Logging] ──▶ [Routing] ──▶ [Endpoint]
            ◀────────────────────────────────────────────────────── Response
// Program.cs — ordem IMPORTA: os middlewares executam na ordem de registro
var app = builder.Build();

// A ordem correta para APIs em produção:
app.UseExceptionHandler("/error");   // 1. Captura exceções de tudo abaixo
app.UseHttpsRedirection();           // 2. Redireciona HTTP → HTTPS
app.UseStaticFiles();               // 3. Serve arquivos estáticos (sem auth)
app.UseRouting();                   // 4. Resolve a rota
app.UseAuthentication();            // 5. Quem é você?
app.UseAuthorization();             // 6. Você pode fazer isso?
app.UseRateLimiter();               // 7. Dentro dos limites?
// Middlewares customizados entram aqui
app.UseRequestContextEnricher();    // 8. Enriquece contexto (tenant, correlation ID)
app.MapControllers();               // 9. Executa o handler

Use, Run e Map: os três verbos do pipeline

// Use() — passa para o próximo middleware
app.Use(async (context, next) =>
{
    // Antes do próximo middleware
    Console.WriteLine($">> {context.Request.Method} {context.Request.Path}");

    await next(context); // Chama o próximo na cadeia

    // Depois do próximo middleware (resposta voltando)
    Console.WriteLine($"<< {context.Response.StatusCode}");
});

// Run() — terminal: não chama o próximo (short-circuit)
app.Run(async context =>
{
    await context.Response.WriteAsync("Fim da linha — nenhum middleware abaixo executa");
});

// Map() — ramificação condicional por path
app.Map("/health", healthApp =>
{
    healthApp.Run(async context =>
        await context.Response.WriteAsync("Healthy"));
});

// MapWhen() — ramificação condicional por predicado
app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Internal-Request"),
    internalApp =>
    {
        internalApp.UseMiddleware<InternalRequestMiddleware>();
    });

Middleware class-based (convention-based): o padrão correto

Para middlewares reutilizáveis, use a abordagem baseada em classe:

// Middleware de correlation ID — injeta/lê um ID único por requisição
public class CorrelationIdMiddleware
{
    private const string CorrelationIdHeader = "X-Correlation-ID";
    private readonly RequestDelegate _next;
    private readonly ILogger<CorrelationIdMiddleware> _logger;

    // RequestDelegate e serviços Singleton são injetados no construtor
    public CorrelationIdMiddleware(
        RequestDelegate next,
        ILogger<CorrelationIdMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // InvokeAsync recebe o contexto e serviços Scoped/Transient
    public async Task InvokeAsync(
        HttpContext context,
        ICorrelationIdService correlationIdService) // Injetado aqui — não no construtor!
    {
        // Lê do header ou gera um novo
        var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
            ?? Guid.NewGuid().ToString("N");

        // Armazena no contexto para uso downstream
        context.Items["CorrelationId"] = correlationId;
        correlationIdService.SetCurrentId(correlationId);

        // Adiciona no header de resposta para rastreabilidade
        context.Response.Headers[CorrelationIdHeader] = correlationId;

        // Enriquece o log scope
        using var logScope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["CorrelationId"] = correlationId
        });

        await _next(context);
    }
}

// Extensão para registro limpo no Program.cs
public static class CorrelationIdMiddlewareExtensions
{
    public static IApplicationBuilder UseCorrelationId(
        this IApplicationBuilder app)
        => app.UseMiddleware<CorrelationIdMiddleware>();
}

// Program.cs
app.UseCorrelationId();

Factory-based middleware: para middlewares que precisam de DI Scoped

// IMiddleware — abordagem factory-based
// Diferença: a instância é criada por requisição (suporta Scoped no construtor)
public class TenantResolutionMiddleware : IMiddleware
{
    private readonly ITenantRepository _tenantRepository; // Scoped — OK aqui!
    private readonly ITenantContext _tenantContext;

    public TenantResolutionMiddleware(
        ITenantRepository tenantRepository,
        ITenantContext tenantContext)
    {
        _tenantRepository = tenantRepository;
        _tenantContext = tenantContext;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Resolve tenant a partir do subdomínio
        var host = context.Request.Host.Host; // ex: "acme.neryx.com.br"
        var subdomain = host.Split('.').FirstOrDefault();

        if (subdomain is not null and not "www" and not "app")
        {
            var tenant = await _tenantRepository.GetBySubdomainAsync(subdomain);
            if (tenant is null)
            {
                context.Response.StatusCode = 404;
                await context.Response.WriteAsJsonAsync(new
                {
                    Error = "Tenant não encontrado",
                    Subdomain = subdomain
                });
                return; // Short-circuit — não chama o próximo
            }

            _tenantContext.SetCurrent(tenant);
        }

        await next(context);
    }
}

// Factory-based EXIGE registro explícito no DI
builder.Services.AddScoped<TenantResolutionMiddleware>();

// E uso via UseMiddleware
app.UseMiddleware<TenantResolutionMiddleware>();

Middleware de logging de requisições estruturado

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

    // Paths a não logar (health checks, métricas)
    private static readonly HashSet<string> _ignoredPaths = new(StringComparer.OrdinalIgnoreCase)
    {
        "/health", "/health/live", "/health/ready", "/metrics"
    };

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

    public async Task InvokeAsync(HttpContext context)
    {
        if (_ignoredPaths.Contains(context.Request.Path))
        {
            await _next(context);
            return;
        }

        var sw = Stopwatch.StartNew();

        try
        {
            await _next(context);
            sw.Stop();

            var level = context.Response.StatusCode >= 500
                ? LogLevel.Error
                : context.Response.StatusCode >= 400
                    ? LogLevel.Warning
                    : LogLevel.Information;

            _logger.Log(level,
                "HTTP {Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                sw.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            sw.Stop();
            _logger.LogError(ex,
                "HTTP {Method} {Path} threw exception after {ElapsedMs}ms",
                context.Request.Method,
                context.Request.Path,
                sw.ElapsedMilliseconds);
            throw;
        }
    }
}

Middleware de tratamento global de exceções

// .NET 8+: IExceptionHandler (preferível ao middleware manual)
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        => _logger = logger;

    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var (statusCode, title) = exception switch
        {
            ValidationException => (StatusCodes.Status422UnprocessableEntity, "Erro de validação"),
            NotFoundException => (StatusCodes.Status404NotFound, "Recurso não encontrado"),
            UnauthorizedException => (StatusCodes.Status401Unauthorized, "Não autenticado"),
            ForbiddenException => (StatusCodes.Status403Forbidden, "Acesso negado"),
            ConflictException => (StatusCodes.Status409Conflict, "Conflito de dados"),
            _ => (StatusCodes.Status500InternalServerError, "Erro interno do servidor")
        };

        if (statusCode == 500)
            _logger.LogError(exception, "Exceção não tratada: {Message}", exception.Message);
        else
            _logger.LogWarning("Exceção de negócio: {Type} — {Message}",
                exception.GetType().Name, exception.Message);

        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = exception.Message,
            Instance = context.Request.Path
        };

        // Adiciona correlation ID para rastreabilidade
        if (context.Items.TryGetValue("CorrelationId", out var correlationId))
            problemDetails.Extensions["correlationId"] = correlationId;

        context.Response.StatusCode = statusCode;
        await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // true = exceção foi tratada, não propaga
    }
}

// Registrar no Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

app.UseExceptionHandler(); // Usa o IExceptionHandler registrado

Middleware de rate limiting por IP customizado

// Para casos onde o RateLimiter nativo não é suficiente
public class IpRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;
    private readonly ILogger<IpRateLimitMiddleware> _logger;

    private const int MaxRequests = 100;
    private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);

    public IpRateLimitMiddleware(
        RequestDelegate next,
        IMemoryCache cache,
        ILogger<IpRateLimitMiddleware> logger)
    {
        _next = next;
        _cache = cache;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Respeita X-Forwarded-For (quando atrás de reverse proxy)
        var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault()
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "unknown";

        var cacheKey = $"ratelimit:{ipAddress}";

        var count = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = Window;
            return 0;
        });

        if (count >= MaxRequests)
        {
            _logger.LogWarning("Rate limit atingido para IP {IpAddress}", ipAddress);

            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers["Retry-After"] = "60";
            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = 429,
                Title = "Too Many Requests",
                Detail = $"Limite de {MaxRequests} requisições por minuto atingido."
            });
            return;
        }

        _cache.Set(cacheKey, count + 1, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = DateTimeOffset.UtcNow.Add(Window)
        });

        // Adiciona headers informativos
        context.Response.Headers["X-RateLimit-Limit"] = MaxRequests.ToString();
        context.Response.Headers["X-RateLimit-Remaining"] = (MaxRequests - count - 1).ToString();

        await _next(context);
    }
}

Testando middlewares

// Teste unitário de middleware sem stack HTTP completo
public class CorrelationIdMiddlewareTests
{
    [Fact]
    public async Task Invoke_NoCorrelationIdInRequest_GeneratesNewId()
    {
        // Arrange
        var context = new DefaultHttpContext();
        var correlationIdService = new Mock<ICorrelationIdService>();
        string? capturedId = null;
        correlationIdService
            .Setup(s => s.SetCurrentId(It.IsAny<string>()))
            .Callback<string>(id => capturedId = id);

        var logger = NullLogger<CorrelationIdMiddleware>.Instance;
        var nextCalled = false;
        RequestDelegate next = ctx => { nextCalled = true; return Task.CompletedTask; };

        var middleware = new CorrelationIdMiddleware(next, logger);

        // Act
        await middleware.InvokeAsync(context, correlationIdService.Object);

        // Assert
        nextCalled.Should().BeTrue();
        capturedId.Should().NotBeNullOrEmpty();
        context.Response.Headers["X-Correlation-ID"].ToString().Should().Be(capturedId);
    }

    [Fact]
    public async Task Invoke_CorrelationIdInRequest_UsesExistingId()
    {
        var expectedId = "existing-correlation-id-123";
        var context = new DefaultHttpContext();
        context.Request.Headers["X-Correlation-ID"] = expectedId;

        var correlationIdService = new Mock<ICorrelationIdService>();
        string? capturedId = null;
        correlationIdService
            .Setup(s => s.SetCurrentId(It.IsAny<string>()))
            .Callback<string>(id => capturedId = id);

        var middleware = new CorrelationIdMiddleware(
            _ => Task.CompletedTask,
            NullLogger<CorrelationIdMiddleware>.Instance);

        await middleware.InvokeAsync(context, correlationIdService.Object);

        capturedId.Should().Be(expectedId);
    }
}

Ordem correta do pipeline — regra de ouro

Posição Middleware Por quê aqui?
ExceptionHandler Deve capturar exceções de tudo abaixo
HttpsRedirection Antes de qualquer processamento real
StaticFiles Sem auth — resposta rápida sem passar pelo pipeline todo
Routing Resolve o endpoint antes da autenticação
CorrelationId Antes de qualquer log — enriches context
Authentication Depois do routing (sabe qual endpoint será chamado)
Authorization Após autenticação
RateLimiter Após auth — rate limit pode ser por usuário autenticado
TenantResolution Após auth — tenant pode depender do usuário
10º RequestLogging Com contexto completo (usuário, tenant, correlation ID)
Último MapControllers/Endpoints Handler final

Middlewares bem projetados eliminam código repetitivo de controllers e garantem comportamento consistente em toda a API. Se você quer uma arquitetura ASP.NET Core mais limpa e manutenível, fale com a Neryx.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.