Comprendre l’ASP.NET Core Request and Response Pipeline - Part 3: Middleware Pipeline
Introduction
Nous' avons étudié la couche d’hébergement et le serveur Kestrel dans la partie 2. Maintenant nous sommes au cœur de l’ASP . Core NET M SK3 le pipeline des logiciels intermédiaires . C’est là que vit votre application MSC5 sa logique MST6 où les demandes sont traitées M ST7 et où les réponses sont construitesM ST8 La compréhension du logiciel intermédiaire est fondamentale pour maîtriser ASP M st9 Core Net Mst10
Middleware est élégantment simple dans son concept—toute la composante est une fonction qui traite une demande HTTP —et assez puissant pour traiter tout, de l'authentification à la gestion des erreurs aux fichiers de service. Dans cette partieMSC3 nousM SK4explorerons comment fonctionne le middlewareMNK5 examinerons les composantes en placeMRK6in componentsMMK7 et apprendreons à créer nos propresMسک8
NOTE: Il s'agit d'une série d'articles expérimentaux (dont il est daté dans le passé ). C'est une combinaison de l'AI et de mon propre travail d'édition et de tweaking.
Qu'est-ce que Middleware?
Middleware est un logiciel qui, dans le cadre d’une pipeline de demandes et de réponses, est assemblée en '.
- Choisirs si la demande doit être transmise à l'élément suivant du pipeline
- Peut exécuter des travaux avant et après la prochaine composante du pipeline
Pensez à ce qu’il s’agisse d’une série de appels de fonction nested, où chaque outil méditerranéen englobe le suivant:
graph LR
A[Request] --> B[Middleware 1]
B --> C[Middleware 2]
C --> D[Middleware 3]
D --> E[Endpoint]
E --> D
D --> C
C --> B
B --> F[Response]
Chaque middleware peut:
- Traiter la demande avant de la transmettre à l'intermédiaire suivant
- Short-circuit et retourner une réponse immédiatement
- Processuer la réponse à mesure qu'elle retourne par pipeline
La signature déléguée de Middleware
À son cœur, le middleware , n’est qu’une fonction avec cette signature.
public delegate Task RequestDelegate(HttpContext context);
Chaque middleware reçoit:
HttpContext context- Contient toutes les informations relatives à la demande et aux réponsesRequestDelegate next- Le prochain outil de middleware dans le pipeline
Flux d'exécution de Middleware
Let's visualiser la façon dont une demande s’écoule par le middleware:
sequenceDiagram
participant Client
participant MW1 as Middleware 1
participant MW2 as Middleware 2
participant MW3 as Middleware 3
participant Endpoint
Client->>MW1: HTTP Request
Note over MW1: Before logic
MW1->>MW2: next()
Note over MW2: Before logic
MW2->>MW3: next()
Note over MW3: Before logic
MW3->>Endpoint: next()
Note over Endpoint: Execute endpoint
Endpoint-->>MW3: Return
Note over MW3: After logic
MW3-->>MW2: Return
Note over MW2: After logic
MW2-->>MW1: Return
Note over MW1: After logic
MW1-->>Client: HTTP Response
Points clés:
- Les demandes s’acheminent vers l’avant par le pipeline (de haut à basM SK1
- Réponse s’écoule vers l’arrière par le pipeline (bottom to topM SK1
- Chaque logiciel peut exécuter le code avant et après l'appel
next() - Middleware peut short-circuit en n'appelant pas
next()
Creating Middleware: Trois approches
1. Middleware en ligne avec Use()
L'approche la plus simple utilise les expressions lambda:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Before the next middleware
Console.WriteLine($"Request: {context.Request.Path}");
var startTime = DateTime.UtcNow;
// Call the next middleware
await next(context);
// After the next middleware
var elapsed = DateTime.UtcNow - startTime;
Console.WriteLine($"Response: {context.Response.StatusCode} ({elapsed.TotalMilliseconds}ms)");
});
app.MapGet("/", () => "Hello World!");
app.Run();
Résultats lors de l'accès /:
Request: /
Response: 200 (15.3ms)
2. Terminal Middleware avec Run()
Run() creates terminal middleware qui termine le pipeline:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("This executes");
await next(context);
});
app.Run(async context =>
{
Console.WriteLine("This is terminal - no next() to call");
await context.Response.WriteAsync("End of pipeline");
});
// This never executes because Run() terminates the pipeline
app.Use(async (context, next) =>
{
Console.WriteLine("This never executes");
await next(context);
});
app.Run();
3. Classe-Based Middleware
Pour les logiciels intermédiaires complexes,, utiliser une classe:
// Middleware class
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
// Store start time in HttpContext.Items for other middleware to access
context.Items["RequestStartTime"] = DateTime.UtcNow;
try
{
await _next(context);
}
finally
{
sw.Stop();
_logger.LogInformation(
"Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
context.Request.Method,
context.Request.Path,
sw.ElapsedMilliseconds,
context.Response.StatusCode
);
}
}
}
// Extension method for convenience
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}
// Usage
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRequestTiming();
app.MapGet("/", () => "Hello World!");
app.Run();
Output:
info: RequestTimingMiddleware[0]
Request GET / completed in 12ms with status 200
Ordre des produits de base
L’ordre dans lequel vous ajoutez le middleware au pipeline est essentiel. Voici l’ordre recommandé'
graph TD
A[Exception Handler] --> B[HSTS]
B --> C[HTTPS Redirection]
C --> D[Static Files]
D --> E[Routing]
E --> F[CORS]
F --> G[Authentication]
G --> H[Authorization]
H --> I[Custom Middleware]
I --> J[Session]
J --> K[Response Caching]
K --> L[Response Compression]
L --> M[Endpoints]
Pourquoi cette commande?
- Gestionnaire de l'exception - Il faut être le premier à prendre toutes les exceptions
- TÉU, TPAC et TPMD - Intitulé de sécurité qui devrait être établi tôt
- Redirection HTTPS - Rediriger à HTTPS avant le traitement
- Les fichiers statiques - Peut se raccourcirM SK1circuiter tôt pour le contenu statique
- Routage - Corresponde la demande à un point final
- CORS - doit être après le routage et avant l'authentification
- Authentification - Identifie qui est l'utilisateur
- Autorisation - Détermine ce que l'utilisateur peut faire
- Outils de médiation personnalisés - Logique de votre application
- Séance - Gestion de l'état des séances
- Cache des réponses - Réponses au cache
- Compression de la réponse - Réponses de compression
- Points finals - Exécuter le point final correspondant
Let's voir cela dans le code:
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddResponseCaching();
builder.Services.AddResponseCompression();
builder.Services.AddSession();
builder.Services.AddCors();
var app = builder.Build();
// 1. Exception handling (must be first)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts(); // HTTP Strict Transport Security
}
// 2. HTTPS redirection
app.UseHttpsRedirection();
// 3. Static files (can short-circuit)
app.UseStaticFiles();
// 4. Routing (matches endpoints)
app.UseRouting();
// 5. CORS (after routing, before auth)
app.UseCors();
// 6. Authentication (who are you?)
app.UseAuthentication();
// 7. Authorization (what can you do?)
app.UseAuthorization();
// 8. Custom middleware
app.UseRequestTiming();
// 9. Session
app.UseSession();
// 10. Response caching
app.UseResponseCaching();
// 11. Response compression
app.UseResponseCompression();
// 12. Endpoints
app.MapGet("/", () => "Hello World!");
app.Run();
Élaboré dans Middleware Deep Dive
Outils de gestion des exceptions
Capture les exceptions provenant de logiciels intermédiaires ultérieurs et génère des réponses d’erreur:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Global exception handler
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
await context.Response.WriteAsJsonAsync(new
{
error = new
{
message = ex.Message,
type = ex.GetType().Name,
stackTrace = app.Environment.IsDevelopment() ? ex.StackTrace : null
}
});
}
});
});
// This will be caught by the exception handler
app.MapGet("/error", () =>
{
throw new InvalidOperationException("Something went wrong!");
});
app.MapGet("/", () => "Hello World!");
app.Run();
Essais:
$ curl http://localhost:5000/error
{
"error": {
"message": "Something went wrong!",
"type": "InvalidOperationException",
"stackTrace": "..."
}
}
Middleware des fichiers statiques
Serve les fichiers statiques et courts-circuit le pipeline:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Default: serves files from wwwroot/
app.UseStaticFiles();
// Serve files from additional directory
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
RequestPath = "/StaticFiles"
});
// Custom file type (MIME mapping)
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".myapp"] = "application/x-myapp";
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
// Enable directory browsing (development only!)
if (app.Environment.IsDevelopment())
{
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
RequestPath = "/browse"
});
}
app.Run();
Diagramme de débit:
graph TD
A[Request: /css/site.css] --> B{Static Files Middleware}
B -->|File exists| C[Serve file]
C --> D[Return 200]
B -->|File not found| E[Call next middleware]
E --> F[Routing/Endpoints]
F --> G[Return 404]
style C stroke:#10b981,stroke-width:3px
style G stroke:#ef4444,stroke-width:3px
Middleware d'authentification
Authentique l'utilisateur en fonction de la demande:
var builder = WebApplication.CreateBuilder(args);
// Add authentication services
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies", options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.ExpireTimeSpan = TimeSpan.FromHours(1);
})
.AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "https://myapp.com",
ValidAudience = "https://myapp.com",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-secret-key-here"))
};
});
var app = builder.Build();
app.UseAuthentication(); // Must be before UseAuthorization()
app.MapGet("/public", () => "Anyone can access this");
app.MapGet("/protected", () => "Only authenticated users can access this")
.RequireAuthorization();
app.MapGet("/login", async (HttpContext context) =>
{
var claims = new[]
{
new Claim(ClaimTypes.Name, "testuser"),
new Claim(ClaimTypes.Email, "[email protected]")
};
var identity = new ClaimsIdentity(claims, "Cookies");
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync("Cookies", principal);
return Results.Ok("Logged in");
});
app.Run();
Middleware d'autorisation
Détermine si un utilisateur authentifié a la permission d'accéder à une ressource:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies");
// Configure authorization policies
builder.Services.AddAuthorization(options =>
{
// Policy: Requires specific claim
options.AddPolicy("AdminOnly", policy =>
policy.RequireClaim("role", "admin"));
// Policy: Requires age over 18
options.AddPolicy("Adults", policy =>
policy.RequireAssertion(context =>
{
var ageClaim = context.User.FindFirst("age");
if (ageClaim != null && int.TryParse(ageClaim.Value, out var age))
{
return age >= 18;
}
return false;
}));
// Policy: Combines requirements
options.AddPolicy("AdminOrManager", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim("role", "admin") ||
context.User.HasClaim("role", "manager")));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// No authorization required
app.MapGet("/public", () => "Public endpoint");
// Requires authentication
app.MapGet("/authenticated", () => "Authenticated endpoint")
.RequireAuthorization();
// Requires specific policy
app.MapGet("/admin", () => "Admin only endpoint")
.RequireAuthorization("AdminOnly");
// Multiple policies
app.MapGet("/restricted", () => "Adults and admins only")
.RequireAuthorization("Adults", "AdminOnly");
app.Run();
CORS Middleware
Répartition des ressources d’origine
var builder = WebApplication.CreateBuilder(args);
// Configure CORS policies
builder.Services.AddCors(options =>
{
// Policy 1: Allow all
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
// Policy 2: Specific origin
options.AddPolicy("AllowSpecificOrigin", policy =>
{
policy.WithOrigins("https://example.com", "https://app.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // Important for cookies/auth
});
// Policy 3: Specific methods and headers
options.AddPolicy("RestrictedAccess", policy =>
{
policy.WithOrigins("https://partner.com")
.WithMethods("GET", "POST")
.WithHeaders("Content-Type", "Authorization")
.WithExposedHeaders("X-Custom-Header")
.SetPreflightMaxAge(TimeSpan.FromMinutes(10));
});
// Default policy
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("https://trustedsite.com")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
app.UseRouting();
// Apply CORS globally
app.UseCors("AllowSpecificOrigin");
// Or apply per endpoint
app.MapGet("/api/data", () => new { data = "Hello" })
.RequireCors("AllowAll");
app.MapGet("/api/restricted", () => new { data = "Restricted" })
.RequireCors("RestrictedAccess");
app.Run();
Correspondance des demandes de prévol CORS:
sequenceDiagram
participant Browser
participant CORS as CORS Middleware
participant Endpoint
Browser->>CORS: OPTIONS /api/data<br/>(Preflight Request)
Note over Browser,CORS: Origin: https://example.com<br/>Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: Content-Type
Note over CORS: Check CORS policy
alt Policy allows
CORS-->>Browser: 204 No Content<br/>Access-Control-Allow-Origin: https://example.com<br/>Access-Control-Allow-Methods: POST<br/>Access-Control-Allow-Headers: Content-Type
Browser->>CORS: POST /api/data<br/>(Actual Request)
CORS->>Endpoint: Forward request
Endpoint-->>CORS: Response
CORS-->>Browser: Response + CORS headers
else Policy denies
CORS-->>Browser: 403 Forbidden
end
Réponse Caching Middleware
Réponses aux caches pour améliorer la performance:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
// Cache this endpoint
app.MapGet("/cached", (HttpContext context) =>
{
context.Response.Headers.CacheControl = "public,max-age=60"; // Cache for 60 seconds
return $"Generated at {DateTime.UtcNow:HH:mm:ss}";
});
// Don't cache this
app.MapGet("/no-cache", (HttpContext context) =>
{
context.Response.Headers.CacheControl = "no-cache";
return $"Generated at {DateTime.UtcNow:HH:mm:ss}";
});
// Conditional caching
app.MapGet("/data", (HttpContext context, string? cache) =>
{
if (cache == "yes")
{
context.Response.Headers.CacheControl = "public,max-age=30";
}
return new
{
timestamp = DateTime.UtcNow,
data = "Some data"
};
});
app.Run();
Essais:
# First request - generates response and caches it
$ curl -i http://localhost:5000/cached
Date: Mon, 15 Jan 2024 10:30:00 GMT
Cache-Control: public,max-age=60
Generated at 10:30:00
# Second request within 60 seconds - served from cache
$ curl -i http://localhost:5000/cached
Date: Mon, 15 Jan 2024 10:30:00 GMT # Same time!
Cache-Control: public,max-age=60
Age: 15 # Cache age in seconds
Generated at 10:30:00 # Same response!
Réponse Compression Middleware
Compression des réponses pour réduire la largeur de bande:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true; // Enable for HTTPS (be aware of CRIME attack)
// Providers (order matters - tried in sequence)
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
// MIME types to compress
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/json", "text/plain", "text/css", "application/javascript" });
});
// Configure compression levels
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest; // Fastest, Optimal, SmallestSize
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Optimal;
});
var app = builder.Build();
app.UseResponseCompression();
app.MapGet("/large", () =>
{
// Generate large response
var data = string.Join("", Enumerable.Repeat("Hello World! ", 1000));
return Results.Text(data, "text/plain");
});
app.Run();
Essais:
# Without compression
$ curl -H "Accept-Encoding:" http://localhost:5000/large
# Response: ~13KB
# With gzip
$ curl -H "Accept-Encoding: gzip" http://localhost:5000/large
# Response: ~100 bytes (compressed)
# Header: Content-Encoding: gzip
# With brotli (better compression)
$ curl -H "Accept-Encoding: br" http://localhost:5000/large
# Response: ~60 bytes (compressed)
# Header: Content-Encoding: br
Creating Custom Middleware: Real-World Examples
Request ID Middleware
Ajoute un identificateur unique à chaque demande de traçage:
public class RequestIdMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestIdMiddleware> _logger;
public RequestIdMiddleware(RequestDelegate next, ILogger<RequestIdMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Check if request already has an ID (from client)
var requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
// Add to response headers
context.Response.Headers["X-Request-ID"] = requestId;
// Store in HttpContext for other middleware/endpoints to access
context.Items["RequestId"] = requestId;
// Use scope for structured logging
using (_logger.BeginScope(new Dictionary<string, object>
{
["RequestId"] = requestId
}))
{
_logger.LogInformation("Processing request {RequestId}", requestId);
await _next(context);
_logger.LogInformation("Completed request {RequestId}", requestId);
}
}
}
API Key Authentication Middleware
Authentification personnalisée pour les clés API:
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string API_KEY_HEADER = "X-API-Key";
public ApiKeyMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
// Skip authentication for public endpoints
if (context.Request.Path.StartsWithSegments("/public"))
{
await _next(context);
return;
}
// Check for API key in header
if (!context.Request.Headers.TryGetValue(API_KEY_HEADER, out var providedKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new { error = "API Key is missing" });
return;
}
// Validate API key (in production, check against database)
var validApiKey = _configuration["ApiKey"];
if (providedKey != validApiKey)
{
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new { error = "Invalid API Key" });
return;
}
// Set user identity based on API key
var claims = new[] { new Claim("ApiKey", providedKey!) };
var identity = new ClaimsIdentity(claims, "ApiKey");
context.User = new ClaimsPrincipal(identity);
await _next(context);
}
}
Moyen-ware de rajustement limité
Limite les demandes par adresse IP:
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly IMemoryCache _cache;
private readonly int _requestLimit;
private readonly TimeSpan _timeWindow;
public RateLimitingMiddleware(
RequestDelegate next,
IMemoryCache cache,
int requestLimit = 100,
int timeWindowSeconds = 60)
{
_next = next;
_cache = cache;
_requestLimit = requestLimit;
_timeWindow = TimeSpan.FromSeconds(timeWindowSeconds);
}
public async Task InvokeAsync(HttpContext context)
{
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var cacheKey = $"RateLimit_{clientIp}";
// Get current request count
var requestCount = _cache.GetOrCreate(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = _timeWindow;
return 0;
});
if (requestCount >= _requestLimit)
{
context.Response.StatusCode = 429; // Too Many Requests
context.Response.Headers["Retry-After"] = _timeWindow.TotalSeconds.ToString();
await context.Response.WriteAsJsonAsync(new
{
error = "Rate limit exceeded",
retryAfter = _timeWindow.TotalSeconds
});
return;
}
// Increment request count
_cache.Set(cacheKey, requestCount + 1, _timeWindow);
// Add rate limit headers
context.Response.Headers["X-RateLimit-Limit"] = _requestLimit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] = (_requestLimit - requestCount - 1).ToString();
context.Response.Headers["X-RateLimit-Reset"] = DateTimeOffset.UtcNow.Add(_timeWindow).ToUnixTimeSeconds().ToString();
await _next(context);
}
}
// Usage
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
var app = builder.Build();
app.UseMiddleware<RateLimitingMiddleware>(100, 60); // 100 requests per 60 seconds
app.Run();
Essais:
# First request - OK
$ curl -i http://localhost:5000/api/data
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1705318860
# After 100 requests
$ curl -i http://localhost:5000/api/data
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705318860
{"error":"Rate limit exceeded","retryAfter":60}
Direction générale de l'informatique
Vous pouvez brancher le pipeline selon les conditions:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Map: Creates a branch based on path prefix
app.Map("/api", apiApp =>
{
apiApp.UseMiddleware<ApiKeyMiddleware>();
apiApp.MapGet("/data", () => new { data = "API data" });
});
// MapWhen: Creates a branch based on custom condition
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Custom-Header"),
customApp =>
{
customApp.Use(async (context, next) =>
{
context.Response.Headers["X-Custom-Response"] = "Matched!";
await next(context);
});
customApp.MapGet("/special", () => "Special endpoint");
});
// UseWhen: Rejoins the main pipeline after the branch
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/admin"),
adminApp =>
{
adminApp.Use(async (context, next) =>
{
// Log admin access
Console.WriteLine("Admin area accessed");
await next(context);
});
});
app.MapGet("/", () => "Main pipeline");
app.Run();
Visualisation par branchement:
graph TD
A[Request] --> B{Path?}
B -->|/api/*| C[API Branch]
B -->|/admin/*| D[Admin Branch UseWhen]
B -->|Other| E[Main Pipeline]
C --> F[API Key Middleware]
F --> G[API Endpoints]
G --> Z1[Response]
D --> H[Admin Logging]
H --> I[Rejoin Main Pipeline]
I --> J[Main Pipeline Continues]
J --> Z2[Response]
E --> K[Main Endpoints]
K --> Z3[Response]
Méthodes exemplaires de Middleware
1. Mettre l'accent sur Middleware
Chaque middleware devrait avoir une seule responsabilité:
// ❌ Bad: Doing too much
public class BadMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
// Authentication
// Authorization
// Logging
// Rate limiting
// Response modification
// ... too much!
}
}
// ✅ Good: Single responsibility
public class AuthenticationMiddleware { /* Only authentication */ }
public class LoggingMiddleware { /* Only logging */ }
public class RateLimitingMiddleware { /* Only rate limiting */ }
2. Gérer correctement les exceptions
public class SafeMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SafeMiddleware> _logger;
public SafeMiddleware(RequestDelegate next, ILogger<SafeMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in middleware");
// Don't swallow exceptions - let exception handler middleware handle them
throw;
}
}
}
3. Respecter la réponse
Ne modifiez pas la réponse après qu’elle ait commencé:
public class ResponseSafeMiddleware
{
private readonly RequestDelegate _next;
public ResponseSafeMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Good: Check before modifying
if (!context.Response.HasStarted)
{
context.Response.Headers["X-Custom-Header"] = "Value";
}
await _next(context);
// Bad: Response might have already started
// context.Response.Headers["X-After-Header"] = "Value"; // This might throw!
// ✅ Good: Check first
if (!context.Response.HasStarted)
{
context.Response.Headers["X-After-Header"] = "Value";
}
}
}
4. Utiliser judicieusement une injection à dépendance
public class DIAwareMiddleware
{
private readonly RequestDelegate _next;
// Singleton services injected in constructor
private readonly ILogger<DIAwareMiddleware> _logger;
public DIAwareMiddleware(RequestDelegate next, ILogger<DIAwareMiddleware> logger)
{
_next = next;
_logger = logger;
}
// Scoped/Transient services injected in InvokeAsync
public async Task InvokeAsync(HttpContext context, IMyService myService)
{
// myService is scoped to this request
var data = await myService.GetDataAsync();
await _next(context);
}
}
Key Takeaways
- Middleware est le cœur du traitement des demandes dans ASP.NET Core
- Chaque outil de middleware enveloppe l’un suivant, en créant une chaîne nested
- L'ordre de Middleware est critique—exception handlers first, endpoints last
- Middleware peut short-circuit en n'appelant pas
next() - Mise en place- dans le logiciel de middleware gère les préoccupations communes M SK1 fichiers statiques , auth, CORSMSC4 cachingMNK5 compressionMRK6
- Le logiciel de middleware personnalisé élargit le cadre pour les besoins spécifiques de l’application-
- Utilisation
app.Use()pour les classes de middleware inline, pour les classements complexes - Middleware peut brancher le pipeline avec
Map()etMapWhen() - Vérifier toujours
Response.HasStartedavant de modifier les titres - Inject singleton services in the constructor, scoped services in
InvokeAsync()
La compréhension du logiciel de middleware vous donne le contrôle complet sur la façon dont les demandes sont traitées.
Continuer à la partie 4: Routing and Endpoints to learn how requests are matched to endpoints and how the routing system works.