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 扩展。