Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Asp.Net Core

.NET Core Middleware: Building a Production-Grade Request Pipeline

- 23.02.26 | 23.02.26 - ErcanOPAK

🔗 The Request Pipeline Mystery

Your API is slow. Users complain. You add logging… but where? You need auth checks, rate limiting, error handling. They’re scattered everywhere. Sound chaotic?

Understanding the Middleware Pipeline

🎯 What Middleware Actually Does

Request comes in →
  ├─ Logging Middleware (start timer)
  │   ├─ Auth Middleware (check JWT)
  │   │   ├─ Rate Limiting Middleware (check quota)
  │   │   │   ├─ Exception Handling Middleware
  │   │   │   │   ├─ Your Controller Action
  │   │   │   │   └─ Response generated
  │   │   │   └─ Log errors if any
  │   │   └─ Check rate limit
  │   └─ Validate auth
  └─ Log request duration
Response goes out ←

Each middleware can:
1. Do something BEFORE next middleware
2. Call next middleware (or short-circuit)
3. Do something AFTER next middleware

🛠️ Custom Middleware Example: Request Timing

// RequestTimingMiddleware.cs
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public RequestTimingMiddleware(
        RequestDelegate next, 
        ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        var requestPath = context.Request.Path;

        try
        {
            // BEFORE: Log request start
            _logger.LogInformation("Request started: {Method} {Path}", 
                context.Request.Method, requestPath);

            // Call next middleware
            await _next(context);

            // AFTER: Log success
            stopwatch.Stop();
            _logger.LogInformation(
                "Request completed: {Method} {Path} - {StatusCode} in {Duration}ms",
                context.Request.Method,
                requestPath,
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex,
                "Request failed: {Method} {Path} after {Duration}ms",
                context.Request.Method,
                requestPath,
                stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}

// Extension method for clean registration
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware();
    }
}

// Program.cs
app.UseRequestTiming();

🔐 Production Middleware: API Key Validation

public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _config;

    public ApiKeyMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _config = config;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Skip auth for health checks
        if (context.Request.Path.StartsWithSegments("/health"))
        {
            await _next(context);
            return;
        }

        // Check for API key in header
        if (!context.Request.Headers.TryGetValue("X-API-Key", out var apiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new 
            { 
                error = "API Key required",
                hint = "Add X-API-Key header"
            });
            return; // Short-circuit! Don't call next middleware
        }

        // Validate API key
        var validKey = _config["ApiKey"];
        if (apiKey != validKey)
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
            return;
        }

        // API key valid, continue pipeline
        await _next(context);
    }
}

🎨 The Perfect Middleware Order (Critical!)

// Program.cs
var app = builder.Build();

// 1. FIRST: Exception handling (catch everything)
app.UseExceptionHandler("/error");

// 2. HTTPS redirect
app.UseHttpsRedirection();

// 3. Static files (before auth - public content)
app.UseStaticFiles();

// 4. Routing (determines which endpoint)
app.UseRouting();

// 5. CORS (before auth!)
app.UseCors();

// 6. Authentication (who are you?)
app.UseAuthentication();

// 7. Authorization (what can you do?)
app.UseAuthorization();

// 8. Custom middleware
app.UseRequestTiming();
app.UseMiddleware();

// 9. Rate limiting
app.UseRateLimiter();

// 10. LAST: Endpoints
app.MapControllers();

app.Run();

// Order matters! Auth before authorization, timing before everything
Middleware Purpose Order Priority
Exception Handler Catch all unhandled errors 1st (Top)
Static Files Serve JS, CSS, images Early
CORS Handle cross-origin requests Before Auth
Authentication Validate JWT, cookies Middle
Authorization Check permissions After Auth
Endpoints Your controllers Last (Bottom)

💡 Advanced Pattern: Conditional Middleware

// Only apply middleware to specific paths
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    appBuilder =>
    {
        appBuilder.UseMiddleware();
        appBuilder.UseRateLimiter();
    }
);

// Apply middleware to everything EXCEPT certain paths
app.UseWhen(
    context => !context.Request.Path.StartsWithSegments("/public"),
    appBuilder => appBuilder.UseAuthentication()
);
⚠️ Common Mistakes

  • Wrong order: Auth after endpoints = auth never runs
  • Forgetting await _next(): Pipeline stops, request hangs
  • Exception in middleware: Use try-catch or exception handler
  • Heavy operations: Middleware runs on EVERY request – keep it fast
  • Modifying response after next(): Response may be committed already

“We moved auth, logging, and rate limiting into middleware. Removed 2000+ lines of duplicate code from controllers. Request pipeline went from spaghetti to surgical.”

— Senior .NET Architect, Financial Services

🎯 The Power of Middleware

1
Place
Not 50 controllers
✓
Every Request
Consistent behavior
0
Duplication
DRY principle

Related posts:

Is Your Clean Architecture Actually a "Dirty" Mess?

IOptionsSnapshot vs IOptionsMonitor — Subtle but Critical

Async Streams in C# — Clean, Fast, Real-Time Data

Post Views: 5

Post navigation

Git Worktrees: Work on Multiple Branches Simultaneously Without Stashing
.NET Core Background Services: Running Tasks Without Blocking Your API

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

April 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930  
« Mar    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (752)
  • Add Constraint to SQL Table to ensure email contains @ (578)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (564)
  • Average of all values in a column that are not zero in SQL (531)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (489)
  • Find numbers with more than two decimal places in SQL (447)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries
  • SQL: Use Window Functions for Advanced Analytical Queries
  • .NET Core: Use Background Services for Long-Running Tasks
  • .NET Core: Use Minimal APIs for Lightweight HTTP Services
  • Git: Use Cherry-Pick to Apply Specific Commits Across Branches
  • Git: Use Interactive Rebase to Clean Up Commit History Before Merge

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (752)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com