Public vs Private APIs in ASP.NET Core — 分支中间件管道(面向生产,带着微笑)

发布: (2026年1月20日 GMT+8 04:20)
12 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容(除代码块和 URL 之外的文本),我将把它翻译成简体中文并保持原有的格式、Markdown 语法和技术术语不变。谢谢!

目录

  1. 一句话概述问题
  2. 为何管道分支是正确的模式
  3. 按前缀使用 UseWhen(推荐)
    • A1) 仅对 Public /_api 运行中间件
    • A2) 仅对 Private /api 运行中间件
    • A3) 使用扩展方法保持 Startup 干净
  4. 选项 B – 端点元数据配置文件(企业灵活)
  5. 选项 C – 路由组(最小化 API)
  6. DI:“不注册” vs “不执行”
  7. 生产注意事项(OData + Controllers + 排序)
  8. 常见陷阱
  9. 最终建议

一句话概括的问题

您需要为访问 /api/.../_api/... 的请求使用不同的中间件链,而 在每个中间件中混入路由逻辑。

为什么管道分支是正确的模式

好处解释
关注点分离中间件保持“纯粹”——内部没有特定路由的代码。
单一真相来源所有基于路由的安全策略集中在一个位置(Startup 或扩展)。
性能不匹配分支的请求永远不会实例化/执行那些中间件。
可演进性易于从简单前缀扩展到以后更丰富的端点元数据。

按前缀使用 UseWhen(推荐)

当你的接口已经拥有明确的前缀(/_api/api)时,这是最简洁的解决方案。

A1) 仅为 Public /_api 运行中间件

// ------------------------------------------------------------
// 1️⃣  Place after UseRouting() and before endpoint mapping
// ------------------------------------------------------------
app.UseRouting();

// ✅ Branch: only /_api
app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments(
        "/_api", StringComparison.OrdinalIgnoreCase),
    branch =>
    {
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
    });

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

A2) 仅为 Private /api 运行中间件

app.UseRouting();

// ✅ Branch: only /api
app.UseWhen(
    ctx => ctx.Request.Path.StartsWithSegments(
        "/api", StringComparison.OrdinalIgnoreCase),
    branch =>
    {
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
        branch.UseMiddleware();
    });

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

A3) 使用扩展方法保持 Startup 整洁

// ──────────────────────────────────────────────────────────────
//  Extension methods that encapsulate the branching logic
// ──────────────────────────────────────────────────────────────
public static class ApiSecurityPipelineExtensions
{
    public static IApplicationBuilder UsePublicApiSecurity(this IApplicationBuilder app)
    {
        return app.UseWhen(
            ctx => ctx.Request.Path.StartsWithSegments(
                "/_api", StringComparison.OrdinalIgnoreCase),
            branch =>
            {
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
            });
    }

    public static IApplicationBuilder UsePrivateApiSecurity(this IApplicationBuilder app)
    {
        return app.UseWhen(
            ctx => ctx.Request.Path.StartsWithSegments(
                "/api", StringComparison.OrdinalIgnoreCase),
            branch =>
            {
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
                branch.UseMiddleware();
            });
    }
}

Configure 中的使用方式

app.UseRouting();

app.UsePublicApiSecurity();   // 或 app.UsePrivateApiSecurity();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

选项 B — 端点元数据配置文件(企业灵活版)

当你需要 例外(例如,某些 /_api 端点应跳过堆栈,或某些 /api 端点应选择加入)时,将决定从 路径 移到 元数据

1️⃣ 定义自定义属性

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
                AllowMultiple = false)]
public sealed class SecurityMiddlewareProfileAttribute : Attribute
{
    public SecurityMiddlewareProfileAttribute(string profile) => Profile = profile;
    public string Profile { get; }
}

2️⃣ 集中管理配置文件常量

public static class SecurityProfiles
{
    public const string Public  = "public";
    public const string Private = "private";
    public const string None    = "none";   // 跳过所有安全中间件
}

3️⃣ 为控制器 / 操作标注

[SecurityMiddlewareProfile(SecurityProfiles.Public)]
[Route("_api/report/v1/[controller]")]
public class ReportController : ControllerBase
{
    // …
}

4️⃣ 基于端点元数据进行分支(路由之后)

app.UseRouting();

app.UseWhen(ctx =>
{
    var endpoint = ctx.GetEndpoint();
    var profile  = endpoint?.Metadata
                            .GetMetadata<SecurityMiddlewareProfileAttribute>()
                            ?.Profile;

    // 仅在 “public” 配置文件时执行堆栈(根据需要调整)
    return string.Equals(profile, SecurityProfiles.Public,
                         StringComparison.OrdinalIgnoreCase);
},
branch =>
{
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
});

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

现在,你可以在不修改中间件代码的情况下,对每个端点的行为进行精细调控。

选项 C — 路由组(如果你转向 Minimal API)

var publicGroup = app.MapGroup("/_api")
    .AddEndpointFilter(async (context, next) =>
    {
        // optional per‑group filter logic
        return await next(context);
    });

publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();
publicGroup.UseMiddleware();

publicGroup.MapGet("/report/v1/{id}", (int id) => /* … */);

Minimal‑API 路由组为你提供相同的分支能力,同时拥有更流畅的语法。

DI:“不注册” 与 “不执行”

方法何时使用
不在 ConfigureServices 中注册 中间件当中间件 构造成本高,且在特定部署中永远不需要时。
不执行(分支)当中间件构造成本低,但只应在部分请求中运行时。

在大多数情况下,分支UseWhen)已经足够,因为中间件管道在启动时只构建一次;分支仅会跳过不匹配请求的执行。

Production Notes (OData + Controllers + Ordering)

  1. UseWhen 放在 UseRouting 之后 – 在路由之前端点元数据(包括 OData 路由)不可用。
  2. UseAuthentication / UseAuthorization 之前 如果这些中间件必须在分支之外运行(例如,你仍希望在私有端点上进行身份验证)。
    • 如果你只想在公共分支上进行身份验证,请将身份验证调用 移到 分支内部。
  3. OData 路由注册 (app.UseEndpoints(endpoints => endpoints.MapODataRoute(...))) 应该放在 分支之后,这样分支才能看到最终的端点。
  4. 安全中间件的顺序 很重要 – 在每个分支内部保持与线性管道相同的顺序。

常见陷阱

陷阱症状解决方案
分支放置在 UseRouting 之前ctx.GetEndpoint()null;前缀检查可能仍然有效,但后续基于路由的元数据将失效。将分支移动到 app.UseRouting() 之后
使用 StartsWith 而不是 StartsWithSegments/api2/... 错误地匹配了 /api使用 StartsWithSegments 进行基于段的匹配。
在需要时忘记在分支内部调用 app.UseAuthentication()分支内部的请求未经过身份验证。在分支内部添加身份验证中间件,或如果应适用于所有请求则全局保留。
注册了相同的中间件 两次(一次全局,一次在分支中)工作重复,可能产生副作用。仅在需要的地方注册。
在 URL 重写后(例如 UsePathBase)依赖 HttpContext.Request.Path路径可能已被修改,导致不匹配。使用 ctx.Request.PathBase + Path,或相应调整谓词。

最终建议

  1. 首先使用基于前缀的 UseWhen 方法——它最简单、性能最佳,并且能够保持中间件的纯粹性。
  2. 将分支逻辑封装在扩展方法中,以保持 Startup.Configure 的整洁。
  3. 当需要对单个端点进行例外处理时,切换到 端点元数据配置文件(选项 B)。
  4. 如果采用 Minimal API,考虑使用 路由组(选项 C)以获得更流畅的使用体验。

通过对管道进行分支,你可以实现关注点的清晰分离,避免不必要的中间件执行,并在 API 范围扩展时保持安全模型的灵活性。

Source:

在 ASP.NET Core 中按端点控制中间件

以下是原始讨论的精简版,保持相同的结构和内容。

使用 UseWhen 进行分支

app.UseWhen(context => context.Request.Path.StartsWithSegments("/_api"), branch =>
{
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
    branch.UseMiddleware();
});

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

结果: 你可以以声明式方式为每个端点控制中间件行为。

Minimal‑API 替代方案 – 路由组

如果以后迁移到 Minimal API,路由组可以提供“完美”的语法:

var publicApi = app.MapGroup("/_api");
publicApi.UseMiddleware();
publicApi.UseMiddleware();

var privateApi = app.MapGroup("/api");
// privateApi.UseMiddleware();

注意: 控制器不以同样的一等公民方式支持路由组,所以目前 UseWhen 仍是最安全的做法。

“不执行” vs. “不注册”

你说:“当是私有的时,我既不想执行它们,也不想在依赖中注册它们。”

在 ASP.NET Core 中,你无需注销中间件来阻止其执行:

场景行为
位于 UseWhen(...) 内且路由 不匹配• 中间件 不会执行
• 通常 不会解析(DI 不会创建实例)。
位于任何条件分支之外中间件会对每个请求运行。

因此真正的目标是 “不执行”;“不注册”往往会增加不必要的复杂度。

如果你仍希望在某些部署中避免注册,可将其视为 环境特定的配置(例如,仅在生产环境注册某些中间件)。

顺序很重要

UseRouting() 必须在检查路由元数据的任何分支之前运行。

放置位置效果
UseRouting() 之后(甚至之前) – 前缀检查简洁且适用于大多数场景。
UseRouting() 之后 – 端点元数据检查必须,因为元数据在此之前不可用。
在端点映射之后(MapControllers()MapHub() 等)该分支 不会影响 请求。
UseRouting() 之前 – 元数据检查GetEndpoint() 将为 null

典型的管道布局

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();               // ("/callshub");

常见陷阱

  • UseWhen 放得太晚(在端点映射之后) → 永远不会运行。
  • UseRouting() 之前检查端点元数据GetEndpoint()null
  • 混合前缀检查和元数据检查且没有明确规则 → 可能导致中间件运行两次。
  • 将中间件逻辑耦合到 “公开/私有” → 保持中间件 纯粹,让管道决定 何时 运行它们。

推荐方案

  1. 先使用方案 A – 基于前缀的 UseWhen
    它匹配你当前的 API 表面(/_api/api),并能快速上线。

  2. 当需要更细粒度的端点控制时,演进为 方案 B – 元数据配置文件(例如自定义端点元数据属性)。

  3. 保持中间件可组合且纯粹;将 “何时运行” 的逻辑放在管道配置中。

后续步骤

如果你贴出控制器的路由方式(包括任何 OData 路由),我可以生成精确的 UseWhen 块以及最安全的排序,以确保不会与 OData 路由或 UseEndpoints() 冲突。

祝发布顺利! 🚀

Back to Blog

相关文章

阅读更多 »

NuGet 中的 WebForms Core

概述:WebForms Core 是由 Elanat 开发的服务器驱动 UI 技术,现已在 NuGet 上正式提供,包名为 WFC。该包允许…