Writing Custom Middleware
Create inline middleware with Use/Run/Map and class-based middleware with InvokeAsync for reusable components.
Writing Custom Middleware is a free C# Academy lesson on CoddyKit. This is lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.
Why Custom Middleware?
Custom middleware handles cross-cutting concerns that apply to many requests: logging, timing, error handling, authentication, compression, caching. Writing it once means the logic runs automatically for every matching request.
Inline Middleware with Use
The quickest way to add middleware: pass a delegate to app.Use(). Good for prototyping or truly simple logic.
app.Use(async (context, next) =>
{
var start = DateTimeOffset.UtcNow;
await next(context);
var elapsed = DateTimeOffset.UtcNow - start;
context.Response.Headers.Append(
"X-Elapsed", elapsed.TotalMilliseconds.ToString("F0") + "ms");
});Class-Based Middleware (Convention)
A conventional middleware class has a constructor receiving RequestDelegate and a public InvokeAsync method. Additional constructor params can be injected from DI.
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
public SecurityHeadersMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var headers = context.Response.Headers;
headers.Append("X-Content-Type-Options", "nosniff");
headers.Append("X-Frame-Options", "DENY");
headers.Append("X-XSS-Protection", "1; mode=block");
headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
await _next(context);
}
}
// Extension method for clean registration:
public static class SecurityHeadersExtensions
{
public static IApplicationBuilder UseSecurityHeaders(
this IApplicationBuilder app) =>
app.UseMiddleware<SecurityHeadersMiddleware>();
}IMiddleware for DI-Scoped Dependencies
If your middleware needs scoped services (like DbContext), implement IMiddleware. Register it as Scoped, and the framework resolves it fresh per request.
public class AuditMiddleware : IMiddleware
{
private readonly AuditDbContext _db;
private readonly IHttpContextAccessor _http;
public AuditMiddleware(AuditDbContext db, IHttpContextAccessor http)
{
_db = db;
_http = http;
}
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
await next(ctx);
if (ctx.Request.Method != "GET")
{
_db.AuditLogs.Add(new AuditLog
{
User = ctx.User.Identity?.Name,
Path = ctx.Request.Path,
Status = ctx.Response.StatusCode,
At = DateTime.UtcNow
});
await _db.SaveChangesAsync();
}
}
}
builder.Services.AddScoped<AuditMiddleware>();
app.UseMiddleware<AuditMiddleware>();Middleware with Options
Pass configuration to middleware via an options object in the constructor. Register options with the DI container or pass inline when registering the middleware.
public class ThrottleOptions
{
public int MaxRequestsPerSecond { get; set; } = 100;
public string[] ExcludedPaths { get; set; } = Array.Empty<string>();
}
public class ThrottleMiddleware
{
private readonly RequestDelegate _next;
private readonly ThrottleOptions _options;
public ThrottleMiddleware(RequestDelegate next, ThrottleOptions options)
{
_next = next;
_options = options;
}
// ...
}
// Registration with options:
app.UseMiddleware<ThrottleMiddleware>(new ThrottleOptions
{
MaxRequestsPerSecond = 50
});Exception Handling Middleware
A custom exception handler middleware catches all unhandled exceptions and returns structured error responses.
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> l)
{
_next = next;
_logger = l;
}
public async Task InvokeAsync(HttpContext ctx)
{
try { await _next(ctx); }
catch (NotFoundException ex)
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "Internal server error" });
}
}
}Request Logging Middleware
A structured request/response logging middleware records method, path, status code, and elapsed time for every request.
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _log;
public RequestLoggingMiddleware(RequestDelegate n, ILogger<RequestLoggingMiddleware> l)
{ _next = n; _log = l; }
public async Task InvokeAsync(HttpContext ctx)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
await _next(ctx);
sw.Stop();
_log.LogInformation(
"{Method} {Path} -> {Status} in {Ms}ms",
ctx.Request.Method,
ctx.Request.Path,
ctx.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}Conditional Middleware with UseWhen
UseWhen branches the pipeline conditionally based on a predicate, without permanently splitting it. The branch merges back into the main pipeline after.
// Apply authentication middleware only to /api/* routes
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments("/api"),
apiApp =>
{
apiApp.UseAuthentication();
apiApp.UseAuthorization();
});
// Public routes (/public/*) bypass authentication
app.MapGet("/public/health", () => "OK");
app.MapGet("/api/data", [Authorize] async (DataService s) => await s.GetAsync());Response Caching Middleware
The built-in response caching middleware stores full responses in memory. Configure cache policies per endpoint using headers or attributes.
builder.Services.AddResponseCaching();
app.UseResponseCaching();
app.MapGet("/products", async (AppDbContext db) =>
{
var products = await db.Products.AsNoTracking().ToListAsync();
return Results.Ok(products);
})
.CacheOutput(p => p.Expire(TimeSpan.FromMinutes(2)).Tag("products"));Real-World: Maintenance Mode Middleware
A maintenance mode middleware that reads a flag from configuration (supporting hot-reload via IOptionsMonitor) and rejects non-health-check requests during maintenance.
public class MaintenanceMiddleware
{
private readonly RequestDelegate _next;
private readonly IOptionsMonitor<MaintenanceOptions> _options;
public MaintenanceMiddleware(RequestDelegate next,
IOptionsMonitor<MaintenanceOptions> opts)
{ _next = next; _options = opts; }
public async Task InvokeAsync(HttpContext ctx)
{
if (_options.CurrentValue.IsEnabled
&& !ctx.Request.Path.StartsWithSegments("/health"))
{
ctx.Response.StatusCode = 503;
ctx.Response.Headers.Append("Retry-After", "300");
await ctx.Response.WriteAsJsonAsync(
new { message = "Service under maintenance" });
return;
}
await _next(ctx);
}
}Quick Check
Why should you implement IMiddleware instead of the convention-based approach when you need a scoped service (like DbContext)?
Recap: Writing Custom Middleware
Key takeaways:
- Inline (Use delegate): quick, one-liners; class-based: reusable, testable
- Conventional class: constructor receives RequestDelegate; InvokeAsync(HttpContext)
- IMiddleware: per-request DI resolution — required for Scoped dependencies
- UseWhen: conditional branching that rejoins the main pipeline
- Write exception handling middleware first (outermost) to catch all errors
- Create extension methods (UseXxx) for clean, discoverable registration
Learn C# with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 93
- Lessons
- 346
Frequently Asked Questions
Is the “Writing Custom Middleware” lesson free?
Yes — the full text of “Writing Custom Middleware” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO. The C# Academy course includes 4 lessons in total.
What will I learn in “Writing Custom Middleware”?
Create inline middleware with Use/Run/Map and class-based middleware with InvokeAsync for reusable components. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
How long does the “Writing Custom Middleware” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- ASP.NET Core Pipeline Overview
- Writing Custom Middleware
- Short-Circuiting & Branching
- Middleware Ordering & Built-in Middleware