This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Sunday, 09 November 2025
Nous avons exploré la couche d'hébergement et le serveur Kestrel dans la partie 2. Maintenant, nous arrivons au cœur d'ASP.NET Core: le pipeline du middleware. C'est là que vit la logique de votre application, où les requêtes sont traitées, et où les réponses sont construites.
Middleware est élégamment simple dans le concept – chaque composant est une fonction qui traite une requête HTTP – assez puissante pour tout gérer, de l'authentification à la gestion des erreurs aux fichiers de service. Dans cette partie, nous allons explorer comment le middleware fonctionne, examiner les composants intégrés, et apprendre à créer notre propre.
NOTE: Ceci fait partie d'une série d'articles expérimentaux (d'où la date dans le passé). C'est une combinaison de l'IA et de mon propre montage et retouche.
Middleware est un logiciel qui est assemblé dans un pipeline d'applications pour traiter les demandes et les réponses.
Pensez-y comme une série d'appels de fonction imbriquée, où chaque intergiciel enveloppe le prochain:
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 intergiciel peut :
À son cœur, le middleware n'est qu'une fonction avec cette signature :
public delegate Task RequestDelegate(HttpContext context);
Chaque intergiciel reçoit :
HttpContext context - Contient toutes les demandes et réponsesRequestDelegate next - Le prochain middleware dans le pipelineVisualisons comment une requête circule à travers 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 :
next()next()Use()L'approche la plus simple utilise des 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();
Produit lors de l'accès /:
Request: /
Response: 200 (15.3ms)
Run()Run() crée des intergiciels terminaux qui terminent 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();
Pour les intergiciels complexes, utilisez 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();
Produit :
info: RequestTimingMiddleware[0]
Request GET / completed in 12ms with status 200
L'ordre dans lequel vous ajoutez le middleware au pipeline est critique. 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 cet ordre ?
Voyons ça en 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();
Attrape des exceptions à partir de l'intergiciel ultérieur 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();
Essai:
$ curl http://localhost:5000/error
{
"error": {
"message": "Something went wrong!",
"type": "InvalidOperationException",
"stackTrace": "..."
}
}
Servit les fichiers statiques et court-circuite 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 flux:
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
Authentifie 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();
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();
Poignées Partage des ressources entre les pays 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();
Débit de la demande de CORS avant le vol:
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
Cache les réponses pour améliorer le rendement :
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();
Essai:
# 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!
Compresse les réponses pour réduire la bande passante :
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();
Essai:
# 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
Ajoute un identifiant 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);
}
}
}
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);
}
}
Limite les requêtes 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();
Essai:
# 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}
Vous pouvez brancher le pipeline selon les conditions suivantes :
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 de la branche :
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]
Chaque intergiciel devrait avoir une responsabilité unique:
// ❌ 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 */ }
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;
}
}
}
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";
}
}
}
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);
}
}
next()app.Use() pour intergiciel en ligne, classes pour intergiciel complexeMap() et MapWhen()Response.HasStarted avant de modifier les en-têtesInvokeAsync()Comprendre l'intergiciel vous donne un contrôle complet sur la façon dont les requêtes sont traitées. Chaque préoccupation transversale – de la connexion à l'authentification à la logique commerciale personnalisée – peut être traitée avec élégance comme intergiciel.
Continuer à la partie 4: Routage et points d'extrémité pour apprendre comment les requêtes sont appariées aux points d'extrémité et comment le système de routage fonctionne.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.