XY.Core 0.1.0

dotnet add package XY.Core --version 0.1.0
                    
NuGet\Install-Package XY.Core -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="XY.Core" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="XY.Core" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="XY.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add XY.Core --version 0.1.0
                    
#r "nuget: XY.Core, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package XY.Core@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=XY.Core&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=XY.Core&version=0.1.0
                    
Install as a Cake Tool

XY.Core

XY.Core 是 XY 后端框架的ASP.NET Core Web 宿主基础设施包。

它适用于基于 ASP.NET Core Minimal API 的 API 宿主项目,提供统一的服务注册和中间件管道入口,帮助业务项目快速接入认证、鉴权、统一响应、OpenAPI、日志、上传、限流、缓存、健康检查等常用能力。

快速接入

运行环境:.NET 10 / ASP.NET Core 10。包名与命名空间均为 XY.Core,此版本为 0.1.0。

包已发布到所配置的 NuGet 源后,安装指定版本:

dotnet add package XY.Core --version 0.1.0

在 API 项目的 Program.cs 中注册并启用:

using XY.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCore(builder.Configuration);

var app = builder.Build();

app.UseCore();

app.MapGet("/api/ping", () => new { ok = true }).AllowAnonymous();

app.Run();

也可以通过代码覆盖配置:

builder.Services.AddCore(builder.Configuration, options =>
{
    options.OpenApi.Title = "Platform API";
    options.Jwt.Issuer = "XY.Platform";
    options.CorrelationId.HeaderName = "X-Correlation-Id";
});

默认读取配置节点:

{
  "Core": {
  }
}

以上空配置会采用各模块默认值;JWT 默认开启,未标记匿名的接口默认要求登录,权限模块默认关闭。默认 JWT 密钥仅供开发占位,部署时必须通过环境变量 Core__Jwt__Secret 或安全配置提供项目独立的随机密钥。默认启用的上传模块包含 avatar、image、document、video 策略;无需上传时设置 Core:Upload:Enable=false。

配置绑定后才执行 configure 回调,因此代码设置会覆盖配置文件中的同名选项。AddCore 在服务注册期间立即校验配置。

模块能力

模块 默认 说明
ForwardedHeaders 启用 处理反向代理转发头
CorrelationId 启用 请求链路 ID,写入响应头和日志上下文
RequestLogging 启用 记录 HTTP 请求方法、路径、状态码、耗时和慢请求
SecurityHeaders 启用 添加 HSTS、X-Frame-Options、Referrer-Policy 等基础安全头
Compression 关闭 gzip/br 响应压缩
Logging 启用 Serilog 控制台、文件、错误文件日志
Telemetry 关闭 OpenTelemetry Trace、Metrics、可选 Logs
Upload 启用 多策略文件上传和静态文件映射
RateLimiting 启用 固定窗口、滑动窗口、令牌桶、并发限流
Json 启用 JSON 序列化配置和可空值转换器
Cors 启用 多 CORS 策略
Response 启用 统一响应包装和异常响应
Jwt 启用 JWT Bearer 认证
Permission 关闭 权限鉴权扩展点,需显式开启并注册业务验证器
Idempotency 启用 不安全 HTTP 方法的幂等保护
Cache 启用 内存缓存或 Redis 缓存
Injection 启用 按程序集扫描自动 DI
Validation 启用 FluentValidation 参数验证
Mapper 启用 Mapster 对象映射
Http 启用 Flurl HTTP Client 配置
SignalR 启用 自动扫描并映射 Hub
Endpoint 启用 自动扫描并映射 Minimal API Endpoint
HealthChecks 启用 /health/live 和 /health/ready
OpenApi 启用 OpenAPI JSON 和 Scalar UI

完整配置参考

以下覆盖所有模块的可配置字段。除明确标为示例或占位的密钥、日志路径与连接串外,值来自当前版本实际默认选项。JSONC 注释用于中文说明;ASP.NET Core 的 JSON 配置提供程序支持注释,其他严格 JSON 工具需要先移除注释。不要直接复制整段作为生产配置。

<details> <summary>展开完整配置(逐字段中文说明)</summary>

{
  // 宿主基础设施配置
  "Core": {
    // 是否在生产环境输出组件启动日志
    "ShowStartupLogsInProduction": false,
    // 统一响应与异常处理
    "Response": {
      // 是否启用本模块
      "Enable": true,
      // 响应包装路径前缀;空列表表示不限制前缀
      "ResponseWrapApiPrefixes": ["/api"],
      // 成功业务码
      "SuccessCode": 0,
      // 失败业务码
      "ErrorCode": -1,
      // 响应内容类型
      "ContentType": "application/json;charset=utf-8",
      // 生产环境系统异常的提示文本
      "ErrorMsg": "系统出错啦,需要联系管理员",
      // 请求成功提示文本
      "SuccessMsg": "请求成功"
    },
    // Minimal API 端点扫描
    "Endpoint": {
      // 是否启用本模块
      "Enable": true,
      // 显式扫描程序集名称;优先于关键词
      "AssemblyNames": [],
      // 程序集名称关键词;仅自动扫描项目程序集
      "ScanAssemblyKeywords": ["Application","Services"]
    },
    // 自动依赖注入
    "Injection": {
      // 是否启用本模块
      "Enable": true,
      // 显式扫描程序集名称;优先于关键词
      "AssemblyNames": [],
      // 程序集名称关键词;仅自动扫描项目程序集
      "ScanAssemblyKeywords": ["Application","Service","Repository","Infrastructure","Core"],
      // 自动注入的类名后缀;也支持 InjectionAttribute
      "ScanClassSuffix": ["Service","Repository"]
    },
    // 日志输出
    "Logging": {
      // 是否启用本模块
      "Enable": true,
      // 最小日志级别
      "MinimumLevel": "Information",
      // 示例值 logs;默认是应用程序基目录下的 logs 绝对路径
      "LogFilePath": "logs",
      // 是否输出控制台日志
      "EnableConsole": true,
      // 是否输出文件日志
      "EnableFile": true,
      // 是否输出独立错误日志
      "EnableErrorFile": true,
      // 文件格式:Json 或 Text
      "FileFormat": "Json",
      // 错误日志文件名;null 表示自动选择
      "ErrorLogFileName": null,
      // 日志滚动周期:Day 或 Hour
      "RollingInterval": "Day",
      // 保留的日志文件数量
      "RetainedFileCountLimit": 7,
      // 错误日志保留数量;null 表示沿用主日志设置
      "ErrorRetainedFileCountLimit": null,
      // 控制台与文本日志输出模板
      "OutputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3}][{CorrelationId}]{Message:lj} <{SourceContextShort}> {NewLine}{Exception}"
    },
    // 缓存
    "Cache": {
      // 是否启用本模块
      "Enable": true,
      // 缓存类型:Memory 或 Redis
      "CacheType": "Memory",
      // Redis 连接串示例;切换 Redis 时替换为实际配置
      "RedisConnectionString": "127.0.0.1:6379,password=,defaultDatabase=0,connectTimeout=5000"
    },
    // JWT 认证
    "Jwt": {
      // 是否启用本模块
      "Enable": true,
      // 是否使用 Core 默认令牌校验参数
      "UseDefaultTokenValidationParameters": true,
      // 占位值留空;必须从安全配置提供独立随机签名密钥,不能直接使用本示例启动
      "Secret": "",
      // 令牌签发者,需与签发端一致
      "Issuer": "SaaS.Tenant",
      // 令牌接收者,需与签发端一致
      "Audience": "SaaS.Client",
      // 有效期,单位:分钟
      "ExpireMinutes": 720
    },
    // 业务权限验证
    "Permission": {
      // 是否启用本模块
      "Enable": false
    },
    // FluentValidation 参数验证
    "Validation": {
      // 是否启用本模块
      "Enable": true,
      // 显式扫描程序集名称;优先于关键词
      "AssemblyNames": [],
      // 程序集名称关键词;仅自动扫描项目程序集
      "ScanAssemblyKeywords": ["Application"]
    },
    // JSON 序列化
    "Json": {
      // 是否启用本模块
      "Enable": true,
      // 是否将可空值类型的空字符串转换为 null
      "EnableNullableValueConverters": true,
      // 日期时间序列化规则
      "DateTime": {
        // 日期输出模式:Preserve 保留类型语义,Utc 转换为 UTC
        "Mode": "Preserve",
        // 如何解释未指定 Kind 的 DateTime:Local 或 Utc
        "TreatUnspecifiedAs": "Local",
        // 是否输出毫秒
        "IncludeMilliseconds": true
      }
    },
    // 跨域访问
    "Cors": {
      // 是否启用本模块
      "Enable": true,
      // 默认 CORS 策略名称;null 或空字符串不应用默认策略
      "DefaultPolicy": "default",
      // 命名策略字典
      "Policies": {
        // 内置默认跨域策略
        "default": {
          // 允许的来源;null 或 ["*"] 允许任意来源且不携带凭证,指定域名时允许凭证
          "AllowedOrigins": null,
          // 是否允许任意 HTTP 方法
          "AllowAnyMethod": true,
          // 允许的方法;AllowAnyMethod=false 时生效
          "AllowedMethods": null,
          // 是否允许任意请求头
          "AllowAnyHeader": true,
          // 允许的请求头;AllowAnyHeader=false 时生效
          "AllowedHeaders": null,
          // 预检请求缓存时间,单位:秒
          "PreflightMaxAge": 3600,
          // 浏览器可读取的额外响应头
          "ExposedHeaders": null
        }
      }
    },
    // Mapster 对象映射
    "Mapper": {
      // 是否启用本模块
      "Enable": true,
      // 显式扫描程序集名称;优先于关键词
      "AssemblyNames": [],
      // 程序集名称关键词;仅自动扫描项目程序集
      "ScanAssemblyKeywords": ["Application"]
    },
    // Flurl HTTP 客户端
    "Http": {
      // 是否启用本模块
      "Enable": true,
      // HTTP 请求超时,单位:秒;命名客户端 null 时沿用全局值
      "TimeoutSeconds": 100,
      // 全局默认请求头
      "DefaultHeaders": {},
      // 命名客户端字典;默认空,下方提供单个客户端配置
      "NamedClients": {},
      // 是否记录 HTTP 调用错误日志
      "EnableErrorLogging": true
    },
    // 请求限流
    "RateLimiting": {
      // 是否启用本模块
      "Enable": true,
      // 不限流的 IP 列表
      "IpWhitelist": [],
      // 不限流的路径前缀
      "PathWhitelist": ["/health","/openapi"],
      // 全局限流策略名称;null 表示不启用全局限流
      "GlobalPolicy": null,
      // 是否允许应用全局限流策略
      "EnableGlobalPolicy": true,
      // 命名策略字典
      "Policies": {}
    },
    // OpenAPI 文档
    "OpenApi": {
      // 是否启用本模块
      "Enable": true,
      // 文档标题
      "Title": "XY API",
      // OpenAPI 文档版本
      "Version": "v1",
      // 文档描述
      "Description": "",
      // Scalar UI 路径
      "Path": "/scalar",
      // Scalar 界面主题
      "Theme": "Default",
      // 是否允许生产环境开放 API 文档
      "EnableInProduction": false
    },
    // 反向代理转发头
    "ForwardedHeaders": {
      // 是否启用本模块
      "Enable": true,
      // 可信代理 IP;与 KnownNetworks 都为空时信任所有代理,生产应显式配置
      "KnownProxies": [],
      // 可信代理网络,CIDR 格式
      "KnownNetworks": []
    },
    // 实时通信
    "SignalR": {
      // 是否启用本模块
      "Enable": true,
      // 是否自动扫描并映射 Hub
      "AutoMapHubs": true,
      // 显式扫描程序集名称;优先于关键词
      "AssemblyNames": [],
      // 程序集名称关键词;仅自动扫描项目程序集
      "ScanAssemblyKeywords": ["Application","Infrastructure"],
      // 是否向 SignalR 客户端返回详细错误
      "EnableDetailedErrors": false,
      // SignalR 心跳间隔,单位:秒
      "KeepAliveInterval": 15,
      // SignalR 客户端超时,单位:秒
      "ClientTimeoutInterval": 30,
      // 最大接收消息字节数;null 表示沿用 ASP.NET Core SignalR 默认值
      "MaximumReceiveMessageSize": null,
      // 多实例 SignalR 广播共享配置
      "Backplane": {
        // 是否启用本模块
        "Enable": false,
        // Backplane 提供者,目前支持 Redis
        "Provider": "Redis",
        // Backplane 连接串;空值时尝试复用启用的 Redis 缓存配置
        "ConnectionString": "",
        // Redis 广播频道前缀;不同应用应区分
        "ChannelPrefix": "xy"
      },
      // 是否允许指定路径从查询参数读取 JWT
      "EnableAccessTokenQueryString": true,
      // 查询参数令牌名称
      "AccessTokenQueryStringKey": "access_token",
      // 允许通过查询参数传递令牌的路径前缀
      "AccessTokenPathPrefixes": ["/hubs","/api/hubs"]
    },
    // 文件上传
    "Upload": {
      // 是否启用本模块
      "Enable": true,
      // 命名策略字典
      "Policies": {
        // 内置头像上传策略
        "avatar": {
          // 文件存储根目录
          "RootPath": "Uploads/Avatar",
          // 上传端点路由
          "Route": "/api/upload/avatar",
          // 最大文件大小,单位:字节
          "MaxFileSize": 2097152,
          // 允许的文件扩展名
          "AllowExtensions": [".jpg",".jpeg",".png"],
          // 静态文件缓存时长,单位:小时;不会自动删除文件
          "ExpireHours": 24,
          // 返回 URL 使用的站点域名;空值取当前请求域名
          "SiteDomain": "",
          // 静态文件访问路径;大文件策略不自动映射
          "RequestPath": "Uploads/Avatar",
          // 文件名模板,支持 Guid、FileName、Extension、Time 和自定义变量
          "FileNameTemplate": "avatar_{Time:yyyyMMdd}_{Guid}{Extension}",
          // 目录模板;空值不增加动态目录
          "DirectoryTemplate": "",
          // 是否标记为大文件策略;断点续传由宿主实现,不自动映射普通上传端点
          "EnableLargeFileUpload": false
        },
        // 内置图片上传策略
        "image": {
          // 文件存储根目录
          "RootPath": "Uploads/Image",
          // 上传端点路由
          "Route": "/api/upload/image",
          // 最大文件大小,单位:字节
          "MaxFileSize": 6291456,
          // 允许的文件扩展名
          "AllowExtensions": [".jpg",".jpeg",".png",".webp"],
          // 静态文件缓存时长,单位:小时;不会自动删除文件
          "ExpireHours": 24,
          // 返回 URL 使用的站点域名;空值取当前请求域名
          "SiteDomain": "",
          // 静态文件访问路径;大文件策略不自动映射
          "RequestPath": "Uploads/Image",
          // 文件名模板,支持 Guid、FileName、Extension、Time 和自定义变量
          "FileNameTemplate": "{Time:yyyy/MM/dd}/{Guid}{Extension}",
          // 目录模板;空值不增加动态目录
          "DirectoryTemplate": "",
          // 是否标记为大文件策略;断点续传由宿主实现,不自动映射普通上传端点
          "EnableLargeFileUpload": false
        },
        // 内置文档上传策略
        "document": {
          // 文件存储根目录
          "RootPath": "Uploads/Document",
          // 上传端点路由
          "Route": "/api/upload/document",
          // 最大文件大小,单位:字节
          "MaxFileSize": 10485760,
          // 允许的文件扩展名
          "AllowExtensions": [".pdf",".docx",".xlsx",".pptx",".zip",".rar",".txt"],
          // 静态文件缓存时长,单位:小时;不会自动删除文件
          "ExpireHours": 48,
          // 返回 URL 使用的站点域名;空值取当前请求域名
          "SiteDomain": "",
          // 静态文件访问路径;大文件策略不自动映射
          "RequestPath": "Uploads/Document",
          // 文件名模板,支持 Guid、FileName、Extension、Time 和自定义变量
          "FileNameTemplate": "{Time:yyyyMMddHHmmss}_{FileName}_{Guid}{Extension}",
          // 目录模板;空值不增加动态目录
          "DirectoryTemplate": "",
          // 是否标记为大文件策略;断点续传由宿主实现,不自动映射普通上传端点
          "EnableLargeFileUpload": false
        },
        // 内置视频策略;大文件传输由宿主实现
        "video": {
          // 文件存储根目录
          "RootPath": "Uploads/Video",
          // 上传端点路由
          "Route": "/api/upload/video",
          // 最大文件大小,单位:字节
          "MaxFileSize": 10737418240,
          // 允许的文件扩展名
          "AllowExtensions": [".mp4",".mov",".avi",".mkv",".flv"],
          // 静态文件缓存时长,单位:小时;不会自动删除文件
          "ExpireHours": 168,
          // 返回 URL 使用的站点域名;空值取当前请求域名
          "SiteDomain": "",
          // 静态文件访问路径;大文件策略不自动映射
          "RequestPath": "Uploads/Video",
          // 文件名模板,支持 Guid、FileName、Extension、Time 和自定义变量
          "FileNameTemplate": "video_{Time:yyyyMMddHHmmss}_{Guid}{Extension}",
          // 目录模板;空值不增加动态目录
          "DirectoryTemplate": "",
          // 是否标记为大文件策略;断点续传由宿主实现,不自动映射普通上传端点
          "EnableLargeFileUpload": true
        }
      }
    },
    // 健康检查
    "HealthChecks": {
      // 是否启用本模块
      "Enable": true,
      // 存活检查路由
      "LivePath": "/health/live",
      // 就绪检查路由
      "ReadyPath": "/health/ready"
    },
    // 请求链路 ID
    "CorrelationId": {
      // 是否启用本模块
      "Enable": true,
      // 请求头名称
      "HeaderName": "X-Correlation-Id",
      // 是否将链路 ID 写入响应头
      "IncludeInResponse": true
    },
    // 安全响应头
    "SecurityHeaders": {
      // 是否启用本模块
      "Enable": true,
      // 是否启用 HSTS;非开发环境添加该响应头,浏览器仅在 HTTPS 下生效
      "EnableHsts": true,
      // HSTS 有效期,单位:天
      "HstsMaxAgeDays": 365,
      // 嵌入框架策略,默认 DENY
      "FrameOptions": "DENY",
      // 来源信息策略
      "ReferrerPolicy": "no-referrer",
      // 内容安全策略;空值不添加 CSP 头
      "ContentSecurityPolicy": ""
    },
    // 响应压缩
    "Compression": {
      // 是否启用本模块
      "Enable": false,
      // 是否允许压缩 HTTPS 响应
      "EnableForHttps": true,
      // 压缩级别,例如 Fastest、Optimal
      "Level": "Fastest"
    },
    // 请求日志
    "RequestLogging": {
      // 是否启用本模块
      "Enable": true,
      // 慢请求阈值,单位:毫秒
      "SlowRequestThresholdMilliseconds": 1000,
      // 跳过处理的路径前缀
      "ExcludedPathPrefixes": ["/health","/favicon.ico","/scalar","/openapi"]
    },
    // OpenTelemetry 遥测
    "Telemetry": {
      // 是否启用本模块
      "Enable": false,
      // 遥测服务名称
      "ServiceName": "XY.Api",
      // 遥测服务版本
      "ServiceVersion": "1.0.0",
      // 遥测环境名称;空值时不添加 deployment.environment 属性
      "Environment": "",
      // 是否采集追踪
      "EnableTracing": true,
      // 是否采集指标
      "EnableMetrics": true,
      // 是否额外导出 ILogger 日志
      "EnableLogs": false,
      // 导出方式:None、Otlp、Console
      "Exporter": "Otlp",
      // OTLP 接收端地址
      "OtlpEndpoint": "http://localhost:4317"
    },
    // 幂等保护
    "Idempotency": {
      // 是否启用本模块
      "Enable": true,
      // 请求头名称
      "HeaderName": "Idempotency-Key",
      // 是否强制要求幂等请求头
      "RequireHeader": false,
      // 有效期,单位:分钟
      "ExpireMinutes": 10,
      // 幂等存储:Auto、Memory、Redis
      "Storage": "Auto",
      // 需要幂等处理的 HTTP 方法
      "Methods": ["POST","PUT","PATCH","DELETE"],
      // 跳过处理的路径前缀
      "ExcludedPathPrefixes": ["/api/upload","/health","/openapi","/scalar"],
      // 可缓存 JSON 响应的最大字节数
      "MaxResponseBodyBytes": 1048576
    }
  }
}

</details>

命名策略与客户端字段

RateLimiting.Policies 默认是空字典。添加命名策略时,每个策略可配置以下字段;设置 GlobalPolicy 为策略名称,或在端点调用 RequireRateLimiting 应用策略:

{
  // 限流类型:FixedWindow、SlidingWindow、TokenBucket、Concurrency
  "PolicyType": "SlidingWindow",
  // 限流窗口,单位:秒
  "WindowSeconds": 60,
  // 窗口内请求上限或并发上限
  "PermitLimit": 100,
  // 排队请求上限,0 表示不排队
  "QueueLimit": 0,
  // 限流分区:ip 或 global
  "PartitionBy": "ip"
}

Http.NamedClients 默认是空字典。每个命名客户端包含以下字段,BaseUrl 为示例,其他值为默认值:

{
  // HTTP 客户端基础 URL 示例
  "BaseUrl": "https://api.example.com",
  // HTTP 请求超时,单位:秒;命名客户端 null 时沿用全局值
  "TimeoutSeconds": null,
  // 命名客户端的默认请求头
  "Headers": {}
}

仅通过 C# 配置的扩展项

以下包含委托或框架对象,不能完整地通过 JSON 表达:

配置项 中文说明
Response.SuccessFactory / FailFactory 自定义成功或失败的响应结构,默认输出 code、message、data
Mapper.GlobalSettings 配置 Mapster 映射规则,默认未设置
Http.JsonSettings 自定义 HTTP 客户端 JSON 序列化,默认未设置
Http.ErrorFactory 自定义调用失败处理,默认记录错误日志;EnableErrorLogging 开启时执行
RateLimiting.Policies[名称].PartitionKeyResolver 自定义分区键,优先于 PartitionBy;默认未设置
Json.JsonSerializerOptions 默认使用驼峰属性名、非缩进输出、保留 null 字段、UnsafeRelaxedJsonEscaping;转换器及命名策略等对象需通过 C# 设置
builder.Services.AddCore(builder.Configuration, options =>
{
    // 自定义成功响应,失败响应使用 FailFactory 配套设置。
    options.Response.SuccessFactory = context => new
    {
        code = context.Code,
        message = context.Message,
        result = context.Data
    };
    // 是否缩进输出 JSON。
    options.Json.JsonSerializerOptions.WriteIndented = true;
    // 设置全局 HTTP 客户端序列化选项。
    options.Http.JsonSettings = json => json.WriteIndented = false;
});

权限验证器、JWT 会话验证器和 Endpoint/Hub 是服务或类型扩展,通过 DI 注册或程序集扫描接入,下文提供示例。

中间件顺序

UseCore() 内部已经按推荐顺序组织管道:

  1. 转发头
  2. CorrelationId
  3. 请求日志
  4. 安全响应头
  5. 响应压缩
  6. 上传静态文件、限流、JSON、CORS
  7. 统一响应和异常处理
  8. JWT 认证
  9. 权限鉴权
  10. 幂等保护
  11. Endpoint、SignalR、HealthChecks、OpenAPI 映射

应用通常不需要手动调整这些组件顺序。若需要额外中间件,建议放在 app.UseCore() 前后明确控制范围。

统一响应

本节的 ApiResults、SkipResponseWrap 和响应配置类型位于 XY.Core.Response;代码示例需引入 using XY.Core.Response;。BusinessException 位于 XY.Core。

默认情况下,命中 Response.ResponseWrapApiPrefixes 的接口会被包装为:

{
  "code": 0,
  "message": "请求成功",
  "data": {}
}

业务失败推荐在应用服务或领域流程中抛出 BusinessException,统一异常处理中间件会将它转换为失败响应:

throw new BusinessException("业务校验失败");
throw new BusinessException("余额不足", 10001);
throw new BusinessException("提交失败", new { Field = "Name" });
throw new BusinessException("提交失败", 10002, new { Field = "Name" });

ApiResults.Fail(...) 适合在 Minimal API Endpoint 委托中直接返回失败结果。它返回的是 IResult,主要用于端点层,不建议在普通 Service 中使用:

app.MapPost("/api/orders", () =>
{
    return ApiResults.Fail("业务校验失败");
});

某个 Endpoint 不需要统一包装时:

app.MapGet("/api/raw", () => new { ok = true })
   .SkipResponseWrap();

如需自定义响应结构,可以通过 SuccessFactory 和 FailFactory 自定义统一响应格式:

builder.Services.AddCore(builder.Configuration, options =>
{
    options.Response.SuccessFactory = context => new
    {
        success = true,
        code = context.Code,
        message = context.Message,
        result = context.Data
    };

    options.Response.FailFactory = context => new
    {
        success = false,
        code = context.Code,
        message = context.Message,
        error = context.Data
    };
});

自定义响应格式后,运行时响应会按工厂输出。OpenAPI 响应包装会通过工厂推导 code、message 和数据字段名;如果使用非常规字段语义,建议同步检查 /openapi/v1.json 的展示结果。

OpenAPI 与 Bearer 认证

默认地址:

  • Scalar UI:/scalar
  • OpenAPI JSON:/openapi/v1.json

当 Jwt.Enable = true 时,OpenAPI 会自动注册 Bearer/JWT 安全方案,并为非匿名接口添加 Bearer 安全要求。

生产环境默认不启用 OpenAPI。确需启用时:

{
  "Core": {
    "OpenApi": {
      "EnableInProduction": true
    }
  }
}

Minimal API Endpoint

实现 IEndpoint 后,Endpoint 模块会按程序集扫描并自动映射。

using Microsoft.AspNetCore.Routing;
using XY.Core.Endpoint;

public sealed class UserEndpoint : IEndpoint
{
    public int Order => 0;

    public void MapEndpoints(IEndpointRouteBuilder app)
    {
        app.MapGet("/api/users", () => new[] { "admin" });
    }
}

默认扫描关键字来自 Endpoint.ScanAssemblyKeywords,可通过配置或代码指定程序集。

JWT 与权限

权限扩展方法及接口位于 XY.Core.Permission,使用 SkipPermission 或注册验证器时需引入该命名空间。

Jwt 负责认证:校验 Bearer Token,解析当前用户。

Permission 负责授权:注册 ASP.NET Core Authorization 策略,并通过 IPermissionValidator 执行业务权限验证。它不是 JWT 的附属模块,也可以用于 API Key、网关鉴权、内部服务鉴权等非 JWT 场景。

两个模块可以独立开关,组合后决定未显式标记 Endpoint 的默认访问行为:

Jwt Permission 默认行为 适用场景
开启 开启 需要登录 + 权限验证 后台管理、租户系统、企业内部系统
开启 关闭 只需要登录 只区分登录/未登录,不做菜单、按钮、资源权限
关闭 开启 不校验 JWT,但仍执行权限验证器 公开 API、API Key、网关鉴权、内部服务鉴权
关闭 关闭 完全公开 Demo、健康探测、纯公开服务

当前实现规则:

  • Jwt.Enable = true 时会注册 Bearer 认证,并启用 UseAuthentication()。
  • Permission.Enable = true 时会设置全局授权策略,并执行 IPermissionValidator.ValidateAsync()。
  • Jwt.Enable = true 且 Permission.Enable = true 时,默认策略为“需要登录 + 业务权限验证”。
  • Jwt.Enable = true 且 Permission.Enable = false 时,默认策略为“只要求登录”,框架仍会执行 UseAuthorization(),但不会执行 IPermissionValidator。
  • Jwt.Enable = false 且 Permission.Enable = true 时,权限验证器仍会执行,但不会检查 JWT 登录态;此时 IPermissionValidator 需要自行基于 API Key、网关 Header、内部签名、来源 IP、服务身份等信息判断是否允许访问。
  • Jwt.Enable = false 且 Permission.Enable = false 时,不启用认证授权中间件,接口默认公开。

单个 Endpoint 可以覆盖默认行为:

标记 行为 常见用途
.AllowAnonymous() 跳过 ASP.NET Core 授权流程,通常等价于公开接口 登录、注册、公钥、健康检查
.SkipPermission() 跳过 IPermissionValidator 权限验证 已登录即可访问的个人资料、退出登录

当 Jwt 和 Permission 都开启时,常见写法:

app.MapPost("/api/auth/login", Login)
   .AllowAnonymous();

app.MapGet("/api/auth/profile", GetProfile)
   .SkipPermission();

app.MapGet("/api/admin/menus", GetMenus);

含义分别是:

  • /api/auth/login:公开接口,不要求登录,不做权限验证。
  • /api/auth/profile:要求登录,但不做业务权限验证。
  • /api/admin/menus:要求登录,并执行业务权限验证。

典型权限验证流程:

请求进入
  -> 如果 Jwt.Enable = true,JWT 认证中间件解析 Token
  -> Authorization 执行全局策略
  -> 如果 Endpoint 标记 AllowAnonymous,则跳过授权
  -> 如果 Permission.Enable = true 且未 SkipPermission,则执行 IPermissionValidator
  -> 根据验证结果返回 200 / 401 / 403

业务项目可以实现自己的权限校验器:

Permission.Enable 默认是 false。开启后,默认验证器 DefaultPermissionValidator 始终返回 true,不会校验业务权限。需要显式注册自己的实现;仅定义接口实现类不保证会被自动扫描。

using XY.Core.Permission;

public sealed class MyPermissionValidator : IPermissionValidator
{
    public Task<bool> ValidateAsync()
    {
        return Task.FromResult(true);
    }
}

在 AddCore 之前注册业务验证器,避免被默认实现占位:

builder.Services.AddScoped<IPermissionValidator, MyPermissionValidator>();
builder.Services.AddCore(builder.Configuration, options =>
{
    options.Permission.Enable = true;
});

示例验证器仅演示接口结构,实际实现应基于当前用户和 Endpoint 权限元数据决定允许或拒绝访问。全局规则通过 FallbackPolicy 实现;显式使用 RequireAuthorization 的端点或自定义策略,应确认对应策略包含所需的权限要求。

需要跳过权限校验的 Endpoint 可以使用:

app.MapGet("/api/user/profile", () => "ok")
   .SkipPermission();

完全公开的接口使用:

app.MapGet("/api/auth/publicKey", () => "public-key")
   .AllowAnonymous();

日志与链路 ID

CorrelationId 会从请求头读取或自动生成链路 ID,并写入:

  • HttpContext.Items
  • 响应头,例如 X-Correlation-Id
  • Serilog LogContext 属性 CorrelationId

因此请求链路内的框架日志、请求日志、业务日志会自动带上同一个 CorrelationId。

控制台日志默认面向人读,文件日志默认使用 JSON Lines,便于后续检索和日志平台采集。错误日志文件默认开启,只记录 Error/Fatal,主日志仍保留完整事件。

启用 Telemetry 后,日志会同时带上 TraceId 和 SpanId,便于从日志跳转到 OpenTelemetry Trace。

业务代码如需读取链路 ID:

using XY.Core.CorrelationId;

public sealed class OrderService(ICorrelationIdAccessor accessor)
{
    public string? GetTraceId() => accessor.CorrelationId;
}

非 HTTP 场景可以手动建立日志范围:

using XY.Core.CorrelationId;

using var scope = CorrelationIdScope.BeginCorrelationScope("job-001");

OpenTelemetry

Telemetry 默认关闭。开启后会采集 ASP.NET Core 请求链路、HTTP Client 调用和 Runtime 指标,并可通过 OTLP 发往 OpenTelemetry Collector。

{
  "Core": {
    "Telemetry": {
      "Enable": true,
      "ServiceName": "XY.Platform.Api",
      "ServiceVersion": "1.0.0",
      "EnableTracing": true,
      "EnableMetrics": true,
      "EnableLogs": true,
      "Exporter": "Otlp",
      "OtlpEndpoint": "http://localhost:4317"
    }
  }
}

日志仍由 Serilog 负责本地控制台、文件和错误文件。EnableLogs=true 表示额外把 ILogger 日志导出到 OTLP,不替代现有文件日志。

本地想直接查看采集内容,可以使用 Console exporter,不需要 Collector:

{
  "Core": {
    "Telemetry": {
      "Enable": true,
      "EnableTracing": true,
      "EnableMetrics": true,
      "EnableLogs": false,
      "Exporter": "Console"
    }
  }
}

生产环境建议使用 Exporter: "Otlp",将数据发送到 OpenTelemetry Collector、Grafana Alloy、Aspire Dashboard 或云平台 Agent。

幂等

端点扩展方法位于 XY.Core.Idempotency,使用 RequireIdempotency 或 SkipIdempotency 时需引入该命名空间。

Idempotency 用来防止同一个业务动作因为重复点击、客户端重试、网关重放而重复执行。

默认只处理:

POST / PUT / PATCH / DELETE

GET 查询默认不参与幂等。因此用户重复查询时仍会重新读取数据库,能看到其他用户新增的数据。不要把查询接口加入 Idempotency.Methods,除非你明确想缓存查询响应。

客户端传入:

Idempotency-Key: submit-order-001

前端接入时,只需要在关键写操作请求中增加这个请求头:

const idempotencyKey = crypto.randomUUID();

await fetch("/api/orders", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey
  },
  body: JSON.stringify({
    productId: "p001",
    quantity: 1
  })
});

前端使用规则:

  • GET 查询不需要传 Idempotency-Key。
  • POST/PUT/PATCH/DELETE 等写操作建议传。
  • 同一次业务动作的重试必须复用同一个 Key,例如提交订单超时后重试。
  • 新的业务动作必须生成新的 Key,例如重新创建另一笔订单。
  • 不要把用户 ID、Token、页面固定值当作 Key,否则会把不同业务动作误判为重复请求。

如果前端有自动重试,需要把 Key 保存在本次提交动作的上下文中:

const idempotencyKey = crypto.randomUUID();

async function submitOrder() {
  return fetch("/api/orders", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey
    },
    body: JSON.stringify(orderForm)
  });
}

同一用户、同一方法、同一路径、同一个 Idempotency-Key、同一个请求体:

  • 第一次请求会执行业务;仅缓存 HTTP 2xx、JSON 类型且不超过 MaxResponseBodyBytes 的响应,其他响应会移除幂等记录。
  • 在幂等记录有效期内,满足缓存条件的后续重复请求不再执行业务,直接返回第一次缓存响应。
  • 如果同一个 Key 被用于不同请求体,返回 409 Conflict。
  • 如果第一次请求还在处理中,重复请求返回 409 Conflict 和 Retry-After: 1。

未传 Idempotency-Key 时默认跳过幂等,不影响现有接口。需要强制某个接口必须传幂等 Key:

app.MapPost("/api/orders", CreateOrder)
   .RequireIdempotency();

需要跳过幂等:

app.MapPost("/api/callback", Callback)
   .SkipIdempotency();

默认存储为 Auto:Redis 缓存启用时使用 Redis,否则使用内存。生产多实例建议使用 Redis。

上传

上传通过策略配置,每个策略对应一个上传接口、存储目录、大小限制和扩展名白名单。

{
  "Core": {
    "Upload": {
      "Enable": true,
      "Policies": {
        "avatar": {
          "RootPath": "Uploads/Avatar",
          "Route": "/api/upload/avatar",
          "MaxFileSize": 2097152,
          "AllowExtensions": [ ".jpg", ".jpeg", ".png" ],
          "RequestPath": "Uploads/Avatar",
          "FileNameTemplate": "avatar_{Time:yyyyMMdd}_{Guid}{Extension}"
        }
      }
    }
  }
}

文件名模板支持常用变量:

  • {Guid}
  • {FileName}
  • {Extension}
  • {Time:yyyyMMddHHmmss}
  • 调用方传入的自定义变量

限流

限流支持全局策略和命名策略。

{
  "Core": {
    "RateLimiting": {
      "Enable": true,
      "GlobalPolicy": "api",
      "EnableGlobalPolicy": true,
      "PathWhitelist": [ "/health", "/openapi", "/scalar" ],
      "Policies": {
        "api": {
          "PolicyType": "SlidingWindow",
          "WindowSeconds": 60,
          "PermitLimit": 100,
          "QueueLimit": 0,
          "PartitionBy": "ip"
        }
      }
    }
  }
}

接口级命名策略可以配合 ASP.NET Core 原生 RequireRateLimiting("policy-name") 使用。

缓存

默认使用内存缓存:

{
  "Core": {
    "Cache": {
      "Enable": true,
      "CacheType": "Memory"
    }
  }
}

使用 Redis:

{
  "Core": {
    "Cache": {
      "Enable": true,
      "CacheType": "Redis",
      "RedisConnectionString": "127.0.0.1:6379,password=,defaultDatabase=0,connectTimeout=5000"
    }
  }
}

启用 Redis 时必须配置连接串,否则应用启动会失败。

SignalR

Hub 增加 SignalRHubAttribute 后可自动映射:

using Microsoft.AspNetCore.SignalR;
using XY.Core.SignalR;

[SignalRHub("/hubs/notify")]
public sealed class NotifyHub : Hub
{
}

可通过 SignalR.AssemblyNames 或 SignalR.ScanAssemblyKeywords 控制扫描范围。

SignalR.Enable 和 AutoMapHubs 默认开启。JWT 默认允许 /hubs、/api/hubs 路径从 access_token 查询参数读取令牌,可通过 EnableAccessTokenQueryString 关闭。

多实例需要广播共享时,可启用 Redis Backplane:

{
  "Core": {
    "SignalR": {
      "Backplane": {
        "Enable": true,
        "Provider": "Redis",
        "ConnectionString": "127.0.0.1:6379",
        "ChannelPrefix": "my-app"
      }
    }
  }
}

Backplane 默认关闭。未单独提供连接串时可复用已启用的 Redis 缓存连接串,否则启用 Backplane 会启动失败。

程序集扫描

Endpoint、Injection、Validation、Mapper、SignalR 都支持 AssemblyNames 和 ScanAssemblyKeywords,前者优先。默认关键词为:

模块 关键词
Endpoint Application、Services
Injection Application、Service、Repository、Infrastructure、Core
Validation / Mapper Application
SignalR Application、Infrastructure

关键词自动扫描只匹配当前应用的项目程序集,不扫描 NuGet 包程序集。宿主或业务程序集名称不匹配时,应显式配置 AssemblyNames;业务能力也打成包时同样需要显式指定。Injection 默认扫描 Service、Repository 后缀的类,或带 InjectionAttribute 的类;默认生命周期为 Scoped。

JSON 日期时间

Core:Json:DateTime 默认 Mode=Preserve、TreatUnspecifiedAs=Local、IncludeMilliseconds=true。需要统一输出 UTC 时配置:

{
  "Core": {
    "Json": {
      "DateTime": {
        "Mode": "Utc",
        "TreatUnspecifiedAs": "Utc",
        "IncludeMilliseconds": true
      }
    }
  }
}

健康检查

默认映射:

  • /health/live
  • /health/ready

可通过 Core:HealthChecks 修改路径。路径必须以 / 开头,否则应用启动会失败。

启动校验

AddCore() 会在启动阶段校验关键配置,尽量提前暴露问题:

  • 启用 Redis 缓存时,必须配置 Core:Cache:RedisConnectionString
  • 启用 JWT 且使用默认 TokenValidationParameters 时,必须配置 Core:Jwt:Secret
  • 启用上传时,至少需要一个上传策略
  • 启用全局限流策略时,GlobalPolicy 必须存在于 Policies
  • 健康检查路径必须以 / 开头
  • 启用 CorrelationId 时,必须配置 HeaderName
  • 启用幂等时,HeaderName 不能为空,ExpireMinutes 必须大于 0
  • 启用 OpenTelemetry OTLP 导出时,OtlpEndpoint 不能为空

JWT 验证扩展

默认情况下,Core 使用 Core:Jwt 中的 Secret、Issuer、Audience 配置 JWT 基础校验。宿主项目需要配置自己的密钥,并让签发方与验证方使用匹配的 Issuer、Audience 和密钥。

如需追加业务级会话校验,例如用户禁用、租户停用、密码版本变化等,实现 IJwtSessionValidator 即可。该校验发生在 JWT 基础校验成功之后;任一校验器调用 Fail() 都会让当前令牌认证失败。

接口位于 XY.Core.JwtIdentity,方法签名为 Task ValidateAsync(JwtSessionValidationContext context)。需将实现注册为 IJwtSessionValidator 服务,例如 builder.Services.AddScoped<IJwtSessionValidator, MySessionValidator>();可以注册多个验证器。

如需替换 JWT 基础校验参数,例如使用外部密钥源、非默认签名算法或自定义 Issuer/Audience 规则,设置:

{
  "Core": {
    "Jwt": {
      "Enable": true,
      "UseDefaultTokenValidationParameters": false
    }
  }
}

然后在宿主项目中接管 TokenValidationParameters:

services.PostConfigure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // 宿主项目自定义基础 JWT 校验参数
        };
    });

不建议外部直接整体覆盖 options.Events,否则会覆盖 Core 内置的 SignalR query token 读取和 IJwtSessionValidator 执行链。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 107 10/13/2026