Comprendre l’ASP.NET Core Request and Response Pipeline - Part 6: Advanced Pipeline Hooks and Extensibility
Introduction
Tout au long de cette série, nous avons étudié le pipeline ASP, . et NET Core depuis ses fondements jusqu’aux modèles d’application.
Ces techniques avancées vous permettent de:
- Modifier la pipeline avant le début de l'application
- Ajouter des services de fond qui fonctionnent à côté de votre application
- Créer des sources de données sur un point final personnalisé
- Inscrire les événements de vie de l'application
- Élaborer des composants réutilisables qui s'intègrent sans fil avec ASP.NET Core
La compréhension de ces points d’extension vous transforme d’un consommateur du cadre à une personne qui peut l’étendre pour répondre à des exigences uniques.
NOTE: Il s'agit d'une partie de mes expériences avec l'AI / une façon d'épargner $1000 Code Calude Crédits WebM SK3 J'ai alimenté cet article par un BUNCH de documentsMSC5 ma compréhensionMST6 des questions que j'ai eues pour générer cet articleMst7 C'est fun et remplit une lacune que je n'ai jamais vu remplie ailleursM st10
Aperçu des points d'extension
graph TB
subgraph "Application Startup"
A1[IStartupFilter]
A2[Configure Services]
A3[Configure Pipeline]
end
subgraph "Background Processing"
B1[IHostedService]
B2[BackgroundService]
B3[IHostApplicationLifetime]
end
subgraph "Endpoint Discovery"
C1[EndpointDataSource]
C2[IEndpointConventionBuilder]
C3[IEndpointRouteBuilder]
end
subgraph "Request Processing"
D1[IMiddleware]
D2[IApplicationModelProvider]
D3[IActionDescriptorProvider]
end
Start[Application Start] --> A1
A1 --> A2
A2 --> A3
A3 --> B1
B1 --> C1
C1 --> D1
IStartupFilter: Modifier le pipeline au démarrage
IStartupFilter vous permet de configurer le middleware avant ou après l'application's configuration normale.
Usage de base
public class RequestTimingStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
// This middleware runs BEFORE your normal pipeline configuration
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
context.Items["RequestStartTime"] = DateTime.UtcNow;
await next(context);
sw.Stop();
context.Response.Headers["X-Response-Time-Ms"] = sw.ElapsedMilliseconds.ToString();
});
// Call the next startup filter
next(app);
// This middleware runs AFTER your normal pipeline configuration
app.Use(async (context, next) =>
{
context.Response.Headers["X-Pipeline-End"] = "true";
await next(context);
});
};
}
}
// Register the startup filter
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IStartupFilter, RequestTimingStartupFilter>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Ordonnance d'exécution:
sequenceDiagram
participant Request
participant SF1 as Startup Filter 1 (Before)
participant App as Application Middleware
participant SF1A as Startup Filter 1 (After)
participant Endpoint
Request->>SF1: Enter
Note over SF1: Added before app.Build()
SF1->>App: Enter
Note over App: Your middleware (app.Use...)
App->>Endpoint: Execute endpoint
Endpoint-->>App: Return
App-->>SF1A: Return
Note over SF1A: Added after next(app)
SF1A-->>Request: Response
Exemple pratique: Auto-HTTPS Enforcement
public class HttpsEnforcementStartupFilter : IStartupFilter
{
private readonly IWebHostEnvironment _env;
public HttpsEnforcementStartupFilter(IWebHostEnvironment env)
{
_env = env;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
// Only enforce HTTPS in production
if (_env.IsProduction())
{
app.Use(async (context, next) =>
{
if (!context.Request.IsHttps)
{
var httpsUrl = $"https://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.Response.Redirect(httpsUrl, permanent: true);
return;
}
await next(context);
});
// Add HSTS header
app.UseHsts();
}
next(app);
};
}
}
Composer des filtres de démarrage multiples
// Filter 1: Logging
public class LoggingStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.Use(async (context, next) =>
{
Console.WriteLine($"[LoggingFilter] Before: {context.Request.Path}");
await next(context);
Console.WriteLine($"[LoggingFilter] After: {context.Response.StatusCode}");
});
next(app);
};
}
}
// Filter 2: Security headers
public class SecurityHeadersStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
await next(context);
});
next(app);
};
}
}
// Register both filters
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IStartupFilter, LoggingStartupFilter>();
builder.Services.AddSingleton<IStartupFilter, SecurityHeadersStartupFilter>();
var app = builder.Build();
// Filters execute in registration order
IHostedService: tâches de fond
IHostedService permet d'effectuer des tâches de fond qui se déroulent pendant la durée de vie de l'application.
Service d'accueil de base
public class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger<TimedHostedService> _logger;
private Timer? _timer;
private int _executionCount = 0;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service is starting");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
var count = Interlocked.Increment(ref _executionCount);
_logger.LogInformation(
"Timed Hosted Service is working. Count: {Count}",
count);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service is stopping");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
// Register
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TimedHostedService>();
var app = builder.Build();
BackgroundService: Tasks simplifiés de fond
public class QueueProcessorService : BackgroundService
{
private readonly ILogger<QueueProcessorService> _logger;
private readonly IServiceProvider _serviceProvider;
public QueueProcessorService(
ILogger<QueueProcessorService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Processor Service is starting");
// Run until cancellation is requested
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessQueueAsync(stoppingToken);
// Wait before next iteration
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
catch (OperationCanceledException)
{
// Expected when stopping
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing queue");
// Wait before retrying
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
_logger.LogInformation("Queue Processor Service is stopping");
}
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
{
// Create a scope for scoped services
using var scope = _serviceProvider.CreateScope();
var queueService = scope.ServiceProvider.GetRequiredService<IQueueService>();
var item = await queueService.DequeueAsync(cancellationToken);
if (item != null)
{
_logger.LogInformation("Processing item: {Item}", item);
await queueService.ProcessAsync(item, cancellationToken);
}
}
}
Coordination avec la durée de vie de l'application
public class StartupTasksService : BackgroundService
{
private readonly ILogger<StartupTasksService> _logger;
private readonly IHostApplicationLifetime _lifetime;
private readonly IServiceProvider _serviceProvider;
public StartupTasksService(
ILogger<StartupTasksService> logger,
IHostApplicationLifetime lifetime,
IServiceProvider serviceProvider)
{
_logger = logger;
_lifetime = lifetime;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Wait for application to fully start
await Task.Delay(100, stoppingToken);
try
{
using var scope = _serviceProvider.CreateScope();
// Run startup tasks
await WarmUpCacheAsync(scope, stoppingToken);
await ValidateDatabaseAsync(scope, stoppingToken);
await LoadConfigurationAsync(scope, stoppingToken);
_logger.LogInformation("All startup tasks completed successfully");
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Startup tasks failed");
// Stop the application if startup tasks fail
_lifetime.StopApplication();
return;
}
// Continue running for the lifetime of the application
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private async Task WarmUpCacheAsync(IServiceScopeScope, CancellationToken cancellationToken)
{
_logger.LogInformation("Warming up cache...");
await Task.Delay(500, cancellationToken);
_logger.LogInformation("Cache warmed up");
}
private async Task ValidateDatabaseAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("Validating database...");
await Task.Delay(300, cancellationToken);
_logger.LogInformation("Database validated");
}
private async Task LoadConfigurationAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("Loading configuration...");
await Task.Delay(200, cancellationToken);
_logger.LogInformation("Configuration loaded");
}
}
Visualisation du cycle de vie:
sequenceDiagram
participant App as Application
participant Lifetime as IHostApplicationLifetime
participant Service as BackgroundService
App->>Lifetime: Application starting
Lifetime->>Service: StartAsync()
Service->>Service: ExecuteAsync() begins
Note over App,Service: Application running
Lifetime->>Lifetime: ApplicationStarted event
Note over Service: Background work continues
App->>Lifetime: Shutdown requested
Lifetime->>Lifetime: ApplicationStopping event
Lifetime->>Service: StopAsync()
Service->>Service: Cancel ExecuteAsync()
Service-->>Lifetime: Stopped
Lifetime->>Lifetime: ApplicationStopped event
App->>App: Exit
Sources de données sur le point final personnalisées
Créer des points de fin dynamiques qui sont découverts au moment du démarrage:
public class PluginEndpointDataSource : EndpointDataSource
{
private readonly List<Endpoint> _endpoints = new();
private readonly IChangeToken _changeToken = NullChangeToken.Singleton;
public PluginEndpointDataSource()
{
// Discover plugins and create endpoints
DiscoverPlugins();
}
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
public override IChangeToken GetChangeToken() => _changeToken;
private void DiscoverPlugins()
{
// Example: Create endpoints for each discovered plugin
var plugins = new[]
{
new { Name = "Plugin1", Path = "/plugins/plugin1" },
new { Name = "Plugin2", Path = "/plugins/plugin2" }
};
foreach (var plugin in plugins)
{
var requestDelegate = CreatePluginDelegate(plugin.Name);
var routeEndpoint = new RouteEndpoint(
requestDelegate,
RoutePatternFactory.Parse(plugin.Path),
order: 0,
new EndpointMetadataCollection(
new DisplayNameMetadata(plugin.Name)),
displayName: plugin.Name);
_endpoints.Add(routeEndpoint);
}
}
private RequestDelegate CreatePluginDelegate(string pluginName)
{
return async context =>
{
await context.Response.WriteAsJsonAsync(new
{
plugin = pluginName,
message = $"Response from {pluginName}",
timestamp = DateTime.UtcNow
});
};
}
}
// Register the endpoint data source
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<EndpointDataSource, PluginEndpointDataSource>();
var app = builder.Build();
// Endpoints from PluginEndpointDataSource are automatically available
app.Run();
Production d'un point final dynamique
public class DatabaseEndpointDataSource : EndpointDataSource
{
private readonly List<Endpoint> _endpoints = new();
private CancellationTokenSource _cts = new();
private IChangeToken _changeToken;
public DatabaseEndpointDataSource(IServiceProvider serviceProvider)
{
_changeToken = new CancellationChangeToken(_cts.Token);
// Load endpoints from database
LoadEndpointsFromDatabase(serviceProvider);
// Set up periodic refresh
Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(5));
ReloadEndpoints(serviceProvider);
}
});
}
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
public override IChangeToken GetChangeToken() => _changeToken;
private void LoadEndpointsFromDatabase(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
// Simulate loading from database
var routes = new[]
{
new { Path = "/dynamic/users", Handler = "GetUsers" },
new { Path = "/dynamic/products", Handler = "GetProducts" }
};
_endpoints.Clear();
foreach (var route in routes)
{
var requestDelegate = CreateDynamicDelegate(route.Handler);
var endpoint = new RouteEndpoint(
requestDelegate,
RoutePatternFactory.Parse(route.Path),
order: 0,
new EndpointMetadataCollection(),
displayName: route.Handler);
_endpoints.Add(endpoint);
}
}
private void ReloadEndpoints(IServiceProvider serviceProvider)
{
LoadEndpointsFromDatabase(serviceProvider);
// Notify of changes
var oldCts = _cts;
_cts = new CancellationTokenSource();
_changeToken = new CancellationChangeToken(_cts.Token);
oldCts.Cancel();
}
private RequestDelegate CreateDynamicDelegate(string handler)
{
return async context =>
{
await context.Response.WriteAsJsonAsync(new
{
handler,
message = $"Dynamic endpoint: {handler}",
path = context.Request.Path.Value
});
};
}
}
IMiddleware: Classe-Based Middleware with DI
Contrairement à la convention, le logiciel de middleware fondé sur - et , IMiddleware appuie pleinement l'injection de dépendance:
public class DatabaseHealthCheckMiddleware : IMiddleware
{
private readonly ILogger<DatabaseHealthCheckMiddleware> _logger;
// Can inject scoped services
public DatabaseHealthCheckMiddleware(ILogger<DatabaseHealthCheckMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Path == "/health/db")
{
// Can resolve scoped services from context
var dbContext = context.RequestServices.GetRequiredService<MyDbContext>();
try
{
await dbContext.Database.CanConnectAsync();
context.Response.StatusCode = 200;
await context.Response.WriteAsJsonAsync(new
{
status = "healthy",
database = "connected",
timestamp = DateTime.UtcNow
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Database health check failed");
context.Response.StatusCode = 503;
await context.Response.WriteAsJsonAsync(new
{
status = "unhealthy",
database = "disconnected",
error = ex.Message
});
}
return;
}
await next(context);
}
}
// Register
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<DatabaseHealthCheckMiddleware>();
var app = builder.Build();
app.UseMiddleware<DatabaseHealthCheckMiddleware>();
app.Run();
Fournisseurs de modèles d'application
Customiser la façon dont MVC découvre et configure les contrôleurs et les actions:
public class CustomApplicationModelProvider : IApplicationModelProvider
{
public int Order => -1000 + 10; // Run after default provider
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
// Runs before the default provider
foreach (var controller in context.Result.Controllers)
{
// Add custom route prefix to all controllers
controller.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = "api/v2/[controller]"
}
});
}
}
public void OnProvidersExecuted(ApplicationModelProviderContext context)
{
// Runs after the default provider
foreach (var controller in context.Result.Controllers)
{
// Add custom metadata to all actions
foreach (var action in controller.Actions)
{
action.Properties["CustomProperty"] = "CustomValue";
}
}
}
}
// Register
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.ModelMetadataDetailsProviders.Add(
new CustomApplicationModelProvider());
});
Fournisseurs de déscripteurs d'action
Ajouter ou modifier dynamiquement les descripteurs d'action:
public class DynamicActionDescriptorProvider : IActionDescriptorProvider
{
public int Order => -1000; // Run early
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
{
// Add dynamic actions
context.Results.Add(new ActionDescriptor
{
RouteValues = new Dictionary<string, string>
{
["controller"] = "Dynamic",
["action"] = "Generated"
},
DisplayName = "Dynamic Generated Action",
AttributeRouteInfo = new AttributeRouteInfo
{
Template = "dynamic/generated"
}
});
}
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
{
// Modify existing actions
foreach (var action in context.Results)
{
// Add custom metadata
action.Properties["Timestamp"] = DateTime.UtcNow;
}
}
}
Fournisseurs de services
Contrôle des assemblages et types d'appareils à contrôler:
public class PluginFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
// Load plugin assemblies
var pluginPath = Path.Combine(AppContext.BaseDirectory, "Plugins");
if (!Directory.Exists(pluginPath))
return;
var pluginAssemblies = Directory.GetFiles(pluginPath, "*.dll")
.Select(Assembly.LoadFrom);
foreach (var assembly in pluginAssemblies)
{
var controllers = assembly.GetTypes()
.Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var controller in controllers)
{
feature.Controllers.Add(controller.GetTypeInfo());
}
}
}
}
// Register
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews()
.ConfigureApplicationPartManager(manager =>
{
manager.FeatureProviders.Add(new PluginFeatureProvider());
});
Conventions de fin
Appliquer les métadonnées et la configuration aux points finals:
public class RequireApiKeyConvention : IEndpointConventionBuilder
{
private readonly List<Action<EndpointBuilder>> _conventions = new();
public void Add(Action<EndpointBuilder> convention)
{
_conventions.Add(convention);
}
public void ApplyConventions(EndpointBuilder builder)
{
foreach (var convention in _conventions)
{
convention(builder);
}
}
}
// Usage
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var apiGroup = app.MapGroup("/api")
.WithMetadata(new RequiresApiKeyMetadata())
.AddEndpointFilter(async (context, next) =>
{
var apiKey = context.HttpContext.Request.Headers["X-API-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey))
{
return Results.Unauthorized();
}
return await next(context);
});
apiGroup.MapGet("/data", () => new { data = "Protected data" });
app.Run();
class RequiresApiKeyMetadata { }
Exemple pratique: Système complet d'extension
Let's construire un système de plugin complet:
// Plugin interface
public interface IPlugin
{
string Name { get; }
string Version { get; }
void ConfigureServices(IServiceCollection services);
void ConfigureRoutes(IEndpointRouteBuilder endpoints);
}
// Plugin discovery service
public class PluginLoader : BackgroundService
{
private readonly ILogger<PluginLoader> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly List<IPlugin> _plugins = new();
public PluginLoader(ILogger<PluginLoader> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Loading plugins...");
var pluginPath = Path.Combine(AppContext.BaseDirectory, "Plugins");
if (Directory.Exists(pluginPath))
{
var pluginAssemblies = Directory.GetFiles(pluginPath, "*.dll")
.Select(Assembly.LoadFrom);
foreach (var assembly in pluginAssemblies)
{
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in pluginTypes)
{
var plugin = (IPlugin)Activator.CreateInstance(type)!;
_plugins.Add(plugin);
_logger.LogInformation(
"Loaded plugin: {Name} v{Version}",
plugin.Name,
plugin.Version);
}
}
}
_logger.LogInformation("Loaded {Count} plugins", _plugins.Count);
await Task.CompletedTask;
}
public IReadOnlyList<IPlugin> GetPlugins() => _plugins;
}
// Startup filter that configures plugins
public class PluginStartupFilter : IStartupFilter
{
private readonly PluginLoader _pluginLoader;
public PluginStartupFilter(PluginLoader pluginLoader)
{
_pluginLoader = pluginLoader;
}
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
next(app);
// Configure plugin routes after main application
var plugins = _pluginLoader.GetPlugins();
foreach (var plugin in plugins)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
plugin.ConfigureRoutes(endpoints);
});
}
};
}
}
// Register everything
var builder = WebApplication.CreateBuilder(args);
// Register plugin system
var pluginLoader = new PluginLoader(
builder.Services.BuildServiceProvider().GetRequiredService<ILogger<PluginLoader>>(),
builder.Services.BuildServiceProvider());
builder.Services.AddSingleton(pluginLoader);
builder.Services.AddHostedService(sp => sp.GetRequiredService<PluginLoader>());
builder.Services.AddSingleton<IStartupFilter, PluginStartupFilter>();
var app = builder.Build();
app.MapGet("/plugins", (PluginLoader loader) =>
{
var plugins = loader.GetPlugins();
return plugins.Select(p => new
{
p.Name,
p.Version
});
});
app.Run();
Key Takeaways
- IStartupFilter modifie le pipeline avant et après la configuration normale
- IHostedService/BackgroundService permet d'effectuer des tâches de fond pendant toute la durée de vie de l'application
- Source de données de point final personnalisée crée des points finals détectables dynamiques,
- Logiciels internationaux fournit un soutien DI complet pour le middleware
- Fournisseurs de modèles d'application adaptation du contrôleur MVC/détection de l'action
- Fournisseurs de services contrôler quels types sont scannés en tant que contrôleurs
- Conventions de fin appliquer les métadonnées et la configuration aux points finals
- Ces points d’extension travaillent ensemble pour créer des architectures , pluggables sophistiquées
La compréhension de ces crochets avancés vous permet d’élaborer des cadres sur ASP.NET CoreM SK1 créer des systèmes plugins, et de personnaliser le pipeline pour répondre à des exigences uniques.
Conclusion de la série
Tout au long de cette série de six parties, nous avons parcouru toutes les couches de l’ASP.
- Partie 1: Établissement de la base et du modèle mental
- Partie 2: Exploré Kestrel et la couche d'hébergement
- Partie 3: Matériel de middleware et pipeline de traitement
- Partie 4: Connaissance de l'itinéraire et du parallèle
- Partie 5: MVC examiné, Pages de revêtementM SK2 et API minimales
- Partie 6: Points d’extension avancés découverts
Vous avez maintenant une compréhension complète de la façon dont les demandes HTTP s’acheminent par l’ASP.Applications de base NET,Comment sont les réponses générées ,et comment adapter chaque aspect de ce processusM SK3 Cette connaissance vous permet d’élaborer des applications Web performantesMSC4Supportables à entretenir~, et sophistiquées.
Le pipeline est élégant dans sa simplicité mais infiniment étendu.