理解ASP.NET核心请求和反应管道----第六部分:高级管道钩和扩展 (中文 (Chinese Simplified))

理解ASP.NET核心请求和反应管道----第六部分:高级管道钩和扩展

Sunday, 09 November 2025

//

11 minute read

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

在整个系列中 我们探索了ASP.NET核心管道 从基础到应用模型

现在,我们将冒险进入先进领域: 推广点和钩子, 让你在深度上定制输油管。

  • 这些先进技术使你能够:
  • 在应用程序启动前修改管道
  • 添加与您的应用程序同时运行的背景服务
  • 创建自定义端点数据源
  • 与应用程序寿命期活动结合

构建与 ASP.NET 核心无缝结合的可再使用部件

理解这些扩展点可以将你从框架的消费者转变为能够扩大框架以满足独特要求的人。

备注:这是我在AI/一种花费1000美元Clude Code Web信用的实验的一部分。

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();

I startupFilter: 启动时修改管道

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实用实例:自动-HTTPS执法

组合多个启动过滤器

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();

IMiddleware: 带有 DI 的基于类的 Middware

与基于公约的中间软件不同

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;
        }
    }
}

定制 MVC 如何发现和配置控制器和动作:

行动说明提供方

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();

将元数据和配置应用到端点 :

  • 实用实例:全面推广系统让我们建立一个完整的插件系统 :
  • 密钥外出I startup 过滤器
  • 在正常配置前后修改管道住宿服务/背景服务
  • 在整个应用程序使用寿命期间能够进行背景任务自定义端点数据源
  • 创建动态、可发现端点IMiddle 软件
  • 为中件提供完整的 DI 支持应用示范供应商
  • MVC 控制/动作发现自定义功能功能提供商
  • 哪些类型的控件已被扫描为控制器

终点公约


将元数据和配置应用到端点

这些推广点共同合作,创建复杂、可插插的建筑

  1. **了解这些先进的钩子可以使你有能力在 ASP.NET Core 之上建立框架,创建插件系统,并定制管道,以满足独特的需求。**系列结论
  2. **我们历经ASP.NET核心请求与响应管道的每一个层面:**第一部分 第一部分
  3. :建立基础和心理模式第二部分 第二部分
  4. :探索 Kestreel 和主机层第三部分 第三部分
  5. :掌握的中间软件和加工管道第四部分 第四部分
  6. : 学习路线和终点匹配第5部分 第五部分

:检查了MVC、Razor Pages和最小API。

第6部分 第六部分

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.