Table of Contents

Web 开发

接入

builder.AddLedonWeb(o => o.UseOpenApi = true);   // Web 开关
app.UseLedonWeb();                               // 管线装配

端点

public class HelloEndpoint : IEndpoint
{
    public void Map(IEndpointRouteBuilder builder) =>
        builder.MapGet("/hello", () => "Hello, Ledon!");
}

控制器

[ApiController]
[Route("api/todos")]
public class TodosController(TodoDbContext db) : ControllerBase { /* ... */ }

所在程序集自动挂为 MVC ApplicationPart。

API 规范化开关

开关 默认 行为
UseProblemDetails ✅ 未处理异常 → RFC 7807(含关联 ID/TraceId),状态码按异常类型映射
模型验证 ✅ 失败统一 400 ValidationProblemDetails
UseCorrelationId ✅ X-Correlation-Id 贯穿请求/响应头/日志作用域
UseUnifiedResponse ❌ 成功响应包 {"code":0,"data":...} 信封
UseApiVersioning ❌ URL 段 api/v{version}/...
UseCors ❌ 按 CorsOrigins 白名单放行跨域
UseScalarUi ❌ Scalar 文档 UI(需 UseOpenApi)
UseRateLimiting ❌ 固定窗口限流(分区键 X-Api-Key 或客户端 IP)
UseResponseCompression ❌ Brotli/Gzip 响应压缩(HTTPS 也启用)
UseRequestTimeouts ❌ 请求超时(RequestTimeoutSeconds,超时 504)
UseIdempotency ❌ Idempotency-Key 幂等(需 IDistributedCache)

限流

builder.AddLedonWeb(o =>
{
    o.UseRateLimiting = true;
    o.RateLimitPermitLimit = 100;    // 窗口内请求数
    o.RateLimitWindowSeconds = 60;   // 窗口长度
});
  • 分区键优先取 X-Api-Key 请求头(服务间调用按调用方限流),否则按客户端 IP;
  • 超限返回 429 ProblemDetails(带 retryAfter);
  • 管线位置在路由之后、认证之前——超限请求尽早拒绝,不消耗认证/业务资源。

响应压缩 / 请求超时

builder.AddLedonWeb(o =>
{
    o.UseResponseCompression = true;   // Brotli 优先、Gzip 兜底
    o.UseRequestTimeouts = true;
    o.RequestTimeoutSeconds = 30;      // 超时 504
});

启动探针

app.MapLedonStartup();   // /health/startup:宿主启动完成(含模块初始化)后 200,之前 503

供容器/编排系统区分"还在启动"与"启动失败"——启动慢的应用不会被探针误杀。

跨域(CORS)

builder.AddLedonWeb(o =>
{
    o.UseCors = true;
    o.CorsOrigins = ["https://app.example.com"];   // 空数组 = 允许任意来源(仅建议开发环境)
});

管线顺序:路由 → CORS → 认证授权 → 端点。预检(OPTIONS)自动应答。

多实例:Data Protection 码环

cookie/防伪令牌的加密码环持久化到 Redis(不配则各实例本地码环,负载均衡后间歇 400/登录态丢失):

builder.AddLedonWeb(o => o.DataProtectionRedisConnectionString = "localhost:6379");

惰性连接 + AbortOnConnectFail=false:Redis 短暂不在线不阻塞启动。

幂等请求

带 Idempotency-Key 请求头的变更请求(POST/PUT/PATCH)自动幂等——重放返回缓存响应、并发同 Key 只执行一次(需 IDistributedCache 在场):

builder.AddLedonWeb(o => o.UseIdempotency = true);
builder.Services.AddLedonDistributedCaching(o => o.RedisConnectionString = "localhost:6379");
  • 缓存键 = (方法 + 路径 + Key),响应带 Idempotency-Replayed: true 头表示重放;
  • 只缓存 2xx——失败不占键,客户端可带同 Key 重试;
  • 进程内单飞;跨实例去重靠共享 IDistributedCache(Redis 时多实例生效)。

API 文档 UI

OpenAPI 只有 JSON 文档;要可视化调试界面开 Scalar UI:

builder.AddLedonWeb(o =>
{
    o.UseOpenApi = true;
    o.UseScalarUi = true;   // 打开 /scalar(自动指向 OpenAPI 文档)
});

文档元数据可配(OpenApiTitle / OpenApiDescription)。检测到 Ledon.Security 在场时,文档自动带上 Bearer 安全方案,且带 [Authorize] 的端点自动带安全要求——Scalar/Swagger UI 的 Authorize 按钮直接可用。

SignalR 多实例:Redis backplane

builder.Services.AddLedonSignalR(o => o.RedisConnectionString = "localhost:6379");
app.MapLedonSignalRHubs();   // ChatHub → /hubs/chat

配置后自动挂 StackExchangeRedis backplane:发往任一实例的消息经 Redis 广播给所有实例的客户端(不配则只有本实例的连接能收到)。连接惰性建立,Redis 短暂不在线不阻塞启动。

认证授权

builder.Services.AddLedonSecurity(o => o.Jwt.SigningKey = Configuration["Jwt:Key"]!);
// [Authorize]                        → JWT 或 API Key 任一通过
// [Authorize(Policy = "users.read")]  → 权限策略

可观测性

builder.Services.AddLedonObservability(o => o.ServiceName = "my-app");
builder.Services.AddLedonHealthChecks();
app.MapLedonLiveness(); app.MapLedonReadiness();

异常 → 状态码映射

未处理异常统一转 RFC 7807 ProblemDetails,不再一律 500:

异常 状态码 说明
ArgumentException 400 调用方能改
UnauthorizedAccessException 403
KeyNotFoundException / FileNotFoundException 404
OperationCanceledException 499 客户端已断开,不是服务器错误
其余 500 detail 只给通用文案,真实原因只进日志

5xx 不把 exception.Message 返给客户端——它可能含 SQL 片段、连接串、文件路径。 真实原因按 Error 级别写日志,调用方拿 correlationId / traceId 来对。

业务包要认领自己的异常类型(例如 Ledon.Application.ValidationException → 400), 实现 IExceptionMapping 并登记,不需要让 Ledon.Web 反向依赖业务包:

public sealed class ValidationExceptionMapping : IExceptionMapping
{
    public bool TryMap(Exception exception, out int statusCode, out string title, out string? detail)
    {
        if (exception is not ValidationException validation)
        {
            statusCode = 0; title = ""; detail = null;
            return false;
        }

        statusCode = StatusCodes.Status400BadRequest;
        title = "请求校验失败";
        detail = string.Join("; ", validation.Errors);
        return true;
    }
}

builder.Services.AddLedonExceptionMapping<ValidationExceptionMapping>();

带 Errors 属性的异常(校验类)会自动把逐条原因写进 ProblemDetails 的 errors 扩展。