【ASP.NET CORE】15.HQServer框架架构更新与使用说明

前言

前面的专题已经分别介绍了 HQServer 的 JWT、ORM、Quartz、RabbitMQ、SignalR 和基础架构整理。随着这些能力逐步加入,框架已经从“能跑起来的 Web API 项目”继续向“可以直接作为业务项目起点的基础架构”演进。

这次更新没有引入复杂的 CQRS、MediatR 或大型领域框架,而是把企业 API 项目中经常重复、容易遗漏的能力继续收敛到 HQ.Common:统一响应和异常、参数校验、TraceId、健康检查、跨仓储事务、HTTP 客户端、缓存、文件存储,以及更完整的 Demo 和测试。

本文以当前 HQServer 源码为准,重点说明这次框架修改解决了什么问题,以及新项目应该怎样使用。

一、这次更新包含哪些内容

  • 统一 API 成功和失败响应,增加错误码与 TraceId。
  • 增加参数校验过滤器和业务异常处理。
  • 补充 HTTPS 重定向、CORS、存活检查和数据库就绪检查。
  • 增加 IHQUnitOfWork,支持多个仓储共用一个事务边界。
  • HttpClientHelper 接入 IHttpClientFactory
  • 增加轻量内存缓存抽象和本地文件存储抽象。
  • 优化 Quartz 错失触发策略,补强 RabbitMQ 消费者生命周期和 QoS。
  • 增加可直接调试的待办 Demo、Docker Compose、环境变量示例和基础测试。

二、统一响应、错误码和 TraceId

以前的接口只返回 SuccessMessageData,前端无法稳定区分参数错误、未授权、资源不存在和业务冲突。这次给 ApiResult 增加了稳定的错误码和 TraceId。

public sealed class ApiResult<T>
{
    public bool Success { get; init; }
    public int Code { get; init; }
    public string? Message { get; init; }
    public string? TraceId { get; init; }
    public T? Data { get; init; }

    public static ApiResult<T> Ok(
        T? data,
        string? message = null,
        string? traceId = null)
        => new()
        {
            Success = true,
            Code = ApiResultCodes.Success,
            Message = message,
            TraceId = traceId,
            Data = data
        };
}

常用错误码集中在 ApiResultCodes 中,例如参数错误、未授权、无权限、资源不存在和服务器错误。业务层可以抛出 BusinessException,全局异常中间件会转换成统一响应,不需要每个 Controller 重复写 try-catch。

if (order is null)
    throw new BusinessException(
        "订单不存在",
        ApiResultCodes.NotFound);

请求 TraceId 会写入响应体和 X-Trace-Id 响应头。线上排查时,前端只要提供 TraceId,后端就可以在日志中定位对应请求。

三、参数校验和 Web 生产基线

注册 Controller 时可以加入全局参数校验过滤器:

builder.Services.AddControllers(options =>
    options.Filters.Add<ApiValidationFilter>());

模型绑定失败后,接口会直接返回统一的参数错误结构。业务规则校验仍然放在 Service 中,避免把业务逻辑堆进过滤器。

Web 默认管道还补齐了 HTTPS 重定向、CORS 和健康检查:

  • /health:只检查应用是否能够响应,适合存活探针。
  • /health/ready:额外检查数据库连接,适合就绪探针。
  • CORS 从 Cors:AllowedOrigins 读取允许的前端来源。
  • 生产环境建议使用 HTTPS,并明确配置允许的域名,不要长期使用通配来源。

四、跨仓储事务使用 IHQUnitOfWork

单个仓储的新增、修改和删除默认会使用事务。但订单创建通常还要同时写订单明细、库存记录和操作日志,这时必须由业务层明确建立一个外层事务。

public sealed class OrderService(
    IHQUnitOfWork unitOfWork,
    IBaseRepository<Order, long> orderRepository,
    IBaseRepository<OrderItem, long> itemRepository)
{
    public async Task CreateAsync(
        Order order,
        List<OrderItem> items,
        CancellationToken cancellationToken = default)
    {
        await unitOfWork.ExecuteAsync(async token =>
        {
            // 关闭仓储自身事务,加入 UnitOfWork 外层事务。
            var orders = orderRepository.WithoutTransaction();
            var orderItems = itemRepository.WithoutTransaction();

            await orders.AddAsync(order);
            await orderItems.AddRangeAsync(items);
        }, cancellationToken: cancellationToken);
    }
}

执行边界变成:

BeginTranAsync
    OrderRepository.AddAsync
    OrderItemRepository.AddRangeAsync
CommitTranAsync

任意一个仓储操作抛出异常,UnitOfWork 都会回滚整个事务。事务内部不要再次调用 WithTransaction(),应使用 WithoutTransaction(),否则容易形成嵌套事务或不一致的提交边界。

IHQUnitOfWork 同时支持返回值和指定隔离级别:

var result = await unitOfWork.ExecuteAsync(
    async token =>
    {
        var orders = orderRepository.WithoutTransaction();
        await orders.AddAsync(order);
        return new OrderDto(order.Id, order.Amount);
    },
    isolationLevel: IsolationLevel.ReadCommitted,
    cancellationToken: cancellationToken);

数据库事务和 RabbitMQ 消息不是同一个事务。需要保证“数据库成功后消息最终送达”时,应进一步设计 Outbox,而不能把消息发布代码放进数据库事务就认为两者已经具备原子性。

五、HttpClientHelper 和 IHttpClientFactory

框架保留了原有的 HttpClientHelper,并增加统一注册入口:

builder.Services.AddHQHttpClient(builder.Configuration);

底层交给 IHttpClientFactory 管理 HttpClient 生命周期,同时保留超时、最大响应体、并发连接数、请求体日志和 QueryString 日志控制。

var result = await httpClient.GetJsonAsync<ProductDto>(
    "/products/1",
    headers: new Dictionary<string, string>
    {
        ["Authorization"] = $"Bearer {token}"
    },
    ct: cancellationToken);

默认响应体上限为 10 MB,请求体和 QueryString 默认不写入日志。若目标 URL 来自用户输入,仍然需要增加目标域名白名单和内网地址拦截,避免 SSRF。

六、缓存和文件存储抽象

本次增加了轻量的 IHQCacheIFileStorage。默认缓存使用内存实现,默认文件存储使用本地目录。这样开发环境不需要强制依赖 Redis、MinIO 或 OSS,后续也可以直接替换 Provider。

var cached = await cache.GetAsync<List<ProductDto>>(
    "products:list",
    cancellationToken);

if (cached is null)
{
    cached = await service.LoadFromDatabaseAsync(cancellationToken);
    await cache.SetAsync(
        "products:list",
        cached,
        TimeSpan.FromMinutes(1),
        cancellationToken);
}

文件存储使用相对路径,并在存储层校验路径不能越过根目录。上传文件时不要直接使用用户提供的原始文件名作为物理路径,应该限制大小、校验扩展名并使用服务端生成的文件名。

七、Quartz 和 RabbitMQ 的稳定性增强

Quartz Cron 任务现在默认使用错失触发后跳过的策略,应用停机期间错过的任务不会在恢复时瞬间集中补跑。需要补偿执行的任务,应单独设计补偿机制。

RabbitMQ 消费端增加了 QoS 预取、消费者注册记录和停止时的资源释放。消息处理成功后 ACK,失败后根据 RequeueOnConsumerError 决定 NACK 是否重新入队。失败消息建议配合死信队列,不要让坏消息无限重试。

await consumers.SubscribeAsync<OrderCreated>(
    new RabbitMQQueueDeclareOptions
    {
        QueueName = "order.created",
        EnableDeadLetter = true
    },
    async (message, ct) =>
    {
        await HandleAsync(message, ct);
    },
    cancellationToken);

八、可运行 Demo 和项目结构

项目内置待办 Demo,用来串联 JWT、权限、SqlSugar、UnitOfWork、缓存、文件存储、SignalR、Quartz 和 RabbitMQ。主要目录如下:

HQ.Application/Application/Demo
├── DemoAuthController.cs
├── DemoTodoController.cs
├── DemoFileController.cs
├── DemoMessageController.cs
└── DemoRealtimeController.cs

HQ.Service/Service/DemoTodoService.cs
HQ.Entity/Model/DemoTodo.cs
HQ.Common/ORM/SQLSugar/HQUnitOfWork.cs

本地可以复制 .env.example.env,使用 Docker Compose 启动 SQL Server 和 RabbitMQ,再运行项目:

cp .env.example .env
docker compose up -d
dotnet restore
dotnet build HQserver.sln -c Release
dotnet test HQserver.sln -c Release
dotnet run --project HQ.Application -c Release

开发环境可以通过 Swagger 获取 Demo JWT,再调用待办接口。生产环境的数据库密码、RabbitMQ 密码和 JWT 签名密钥必须通过环境变量或 Secret 注入,不能写进提交文件。

九、测试和后续扩展

项目增加了基础测试项目,覆盖统一响应和 Quartz 任务定义等核心行为。提交前建议至少执行:

dotnet build HQserver.sln -c Release
dotnet test HQserver.sln -c Release
dotnet list HQserver.sln package --vulnerable --include-transitive

这次更新的重点不是增加更多层次,而是让通用能力有统一入口、默认行为更安全、业务层更容易组合。后续可以在当前结构上继续接入 Redis、MinIO/OSS、Outbox、审计字段和软删除,而不需要推翻现有分层。

总结

HQServer 当前的推荐开发路径是:Entity 定义数据结构,Service 组织业务,Controller 负责 HTTP,Common 提供基础设施。单仓储写入直接使用仓储,跨仓储一致性使用 IHQUnitOfWork,实时通知使用 SignalR,异步解耦使用 RabbitMQ,定时执行使用 Quartz。

完整架构说明和各模块示例已经同步到项目 README,本文则作为本次框架更新的专题记录。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享