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
Упродовж цієї серії ми дослідили трубопровод АСPNET з його фундаменту до моделей застосування.
Тепер ми надамо перевагу більш прогресивним територіям: точкам розширення і гачкам, які дають змогу налаштувати трубопровод на глибокому рівні.
Збирання придатних для зміни компонентів, які безперешкодно інтегровані з ядром ASP. NET
Розуміння цих пунктів перетворює вас від споживача системи на того, хто може розширити її, щоб задовольнити унікальні вимоги.
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Это весело и заполняет прорыв, который я больше не видел.
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();
Фільтр IStartup: Зміна трубопроводу під час запуску
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
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);
};
}
}
// 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Практичний приклад.
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();
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);
}
}
}
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");
}
}
Базова служба
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
Координація часу існування програми
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();
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Створення динамічної точки закінчення
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();
На відміну від програмного забезпечення, заснованого на конвенціях,
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());
});
Постачальники моделей програм
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;
}
}
}
Провайдери дескриптора дій
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());
});
Постачальники можливостей
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 { }
Кінцеві конгреси
// 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();
Кінцеві конгреси
Цей суфікс вказує на те, що працювати разом для створення складних, зручних архітектур
: Розглянуті MVC, Pages Razor і Мінімальні API
Частина 6
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.