Imizuki.UniversalApi 1.2.3

Imizuki.UniversalApi

面向 .NET 10 与 ASP.NET Core 的通用 API 组件。

  • 包名:Imizuki.UniversalApi
  • 版本:1.2.3
  • 作者:Imizuki
  • Copyright © 2026 Imizuki

更新日志

1.2.3

  • 生成端点具有 IFormFile 或 IFormFileCollection 参数时,AddGeneratedEndpointServices() 自动注册 AddAntiforgery()。
  • 在应用上调用 MapGeneratedEndpoints() 时自动添加 UseAntiforgery();检测 .NET 10 已有中间件标记,避免重复添加。
  • 无文件参数时不添加防伪配置;补充文件上传端点、认证授权顺序和客户端令牌要求。

1.2.2

  • 生成的 MapGroup 后追加 WithTags,内容为 EndpointGroup 所标记的实现类名称。
  • 按实现类和路由前缀分组,避免多个类共享前缀时混用标签;方法独立分组也使用所属类名称。

1.2.1

  • Refit 注册改用 AddRefitGeneratedClient<TApi>(),使用 Refit 源生成客户端,不回退到反射实现。
  • 生成文件提示名称移除 Refit/ 路径前缀,与端点生成器采用一致的平铺命名方式,避免 IDE 显示额外的 Refit 文件夹分组。
  • Refit 源码仍保留在各项目的 Refit/ 目录,注册入口和配置方式不变。

1.2.0

  • 新增 RefitConfig 和独立 Refit 注册生成器,统一生成 MapCustomRefitClient()。
  • 通过 ConfigureHttpClient 配置固定基地址与超时,默认总超时 30 秒。
  • 新增 IResilienceApi 静态虚配置入口,支持派生接口完全覆盖默认弹性策略。
  • 默认仅 GET/HEAD 暂时性故障重试 2 次,基础延迟 2 秒,指数退避加抖动,单次超时 10 秒;不配置熔断。
  • 引入 Refit.HttpClientFactory 15.2.0 与 Microsoft.Extensions.Http.Resilience 10.10.0,新增 IUA004 配置诊断。
  • 弹性管道 Name 默认使用接口名称;仅继承 IResilienceApi 的接口启用弹性策略。
  • Timeout = -1 取消 HttpClient 总超时,但默认单次超时仍生效;partial 接口自动去重注册。

1.1.1

  • 在生成的路由调用链末尾添加成功响应 Produces 声明,按最终 NonUnify 配置选择业务类型或 ApiResult<T>。
  • 新增方法级 ApiResponse<T>,用于覆盖业务响应文档类型,不改变运行时返回值和序列化行为。
  • 按接口返回类型识别 Task、Task<T>、ValueTask、ValueTask<T>,显式生成 async/await 调用。
  • 无返回值端点默认声明 ApiResult<object>;禁用包装时仅声明 HTTP 200,不声明响应体。
  • 重复或无效的响应类型覆盖通过 IUA002 定位到对应源码特性。

1.1.0

  • 将端点分组改为 EndpointGroup<TService>,通过接口注入调用实例方法。
  • 新增 AddGeneratedEndpointServices(),按特性配置注册瞬态、作用域或单例服务,默认瞬态。
  • HTTP 方法特性支持独立分组及方法优先的 NonUnify 响应开关。
  • 移除非泛型分组和两套独立的 NonUnify 特性,属于破坏性 API 变更。
  • 新增接口契约、方法签名及重复服务绑定的编译期诊断(IUA001–IUA003)。
  • 保留实现方法的参数顺序、绑定特性和默认值,支持重载、显式接口实现及异步返回值。
  • 旧静态端点需迁移为接口实例方法,并调用 AddGeneratedEndpointServices();迁移步骤见下文。

1.0.2

  • 修复代码源生成时二次生成造成引用不明的BUG。

功能与命名空间

命名空间 功能
Imizuki.UniversalApi.Attributes 端点分组、HTTP 方法、统一响应开关与业务响应类型覆盖特性
Imizuki.UniversalApi.Core 应用配置与服务访问
Imizuki.UniversalApi.Unify 统一响应、业务异常与 MVC / Minimal API 过滤器
Imizuki.UniversalApi.Utils JSON 配置、日期转换与 ID 工具
Imizuki.UniversalApi.Refit Refit 客户端配置及可覆盖的默认弹性策略
Imizuki.UniversalApi.Generator 端点映射源生成器实现

生成器独立编译为 netstandard2.0 程序集,通过 NuGet 包的 analyzers/dotnet/cs 目录加载。运行时库不执行生成器,生成的扩展方法使用 Imizuki.UniversalApi.Generated 命名空间。

接口注入与自动注册

using Imizuki.UniversalApi.Attributes;
using Imizuki.UniversalApi.Generated;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGeneratedEndpointServices();

var app = builder.Build();
app.MapGeneratedEndpoints();
app.Run();

public interface ITestService
{
    string Get(int id);
}

[EndpointGroup<ITestService>("/test", ServiceLifetime.Scoped, NonUnify = true)]
public class TestService : ITestService
{
    [Get("/{id}", Group = "/other", NonUnify = false)]
    public string Get(int id) => id.ToString();
}

上述声明生成 services.AddScoped<ITestService, TestService>(),并将端点映射为:

var group0 = endpoints.MapGroup("/other").WithTags("TestService");
group0.MapGet(
    "/{id}",
    ([Microsoft.AspNetCore.Mvc.FromServices] ITestService service, int id) => service.Get(id))
    .WithMetadata(new Imizuki.UniversalApi.Unify.EndpointResponseMetadata(false))
    .AddEndpointFilter<Imizuki.UniversalApi.Unify.UnifyEndpointFilter>()
    .Produces<Imizuki.UniversalApi.Unify.ApiResult<string>>();
  • EndpointGroup<TService> 的泛型参数必须是标记类实现的接口;类须可访问、非抽象、非开放泛型并有公共构造函数。
  • 生命周期使用 ServiceLifetime:默认 Transient,也支持 Scoped 和 Singleton。每个标记类注册一次,同一接口不能绑定多个端点实现类。
  • 标记的端点必须属于指定接口或其父接口契约。支持重载、显式接口实现及普通和异步返回值;不支持静态方法、泛型方法、async void 和按引用参数或返回值。
  • Lambda 第一参数始终注入服务接口,其余参数按被标记实现方法的顺序保留名称、类型、可空性、默认值和参数绑定特性。绑定信息不从接口参数继承。
  • Get、Post、Put、Delete 都支持 Group:不设置或为 null 时继承类前缀,设置时完全替换前缀,空字符串表示根分组。
  • 类级 NonUnify 默认 false。方法未设置时继承类值,显式 false 可覆盖类上的 true。独立分组只改变路由,不改变响应继承关系。
  • NonUnify 仅跳过成功响应包装,异常处理保持启用。MVC 结果过滤器同样按方法配置优先、类配置次之读取这些特性。

文件上传与自动防伪配置

直接声明 IFormFile 或 IFormFileCollection 参数时,生成器按框架类型符号识别上传端点(包括可空参数),自动在 AddGeneratedEndpointServices() 中注册防伪服务,并在 app.MapGeneratedEndpoints() 中添加 app.UseAntiforgery()。存在多个上传端点时只生成一次配置;没有这两种参数时不添加配置。

using Imizuki.UniversalApi.Attributes;
using Imizuki.UniversalApi.Generated;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGeneratedEndpointServices();

var app = builder.Build();
// 使用认证、授权时,将 UseAuthentication() 和 UseAuthorization() 放在映射入口之前。
app.MapGeneratedEndpoints();
app.Run();

public interface IUploadService
{
    string Upload(IFormFile file);
}

[EndpointGroup<IUploadService>("/files")]
public class UploadService : IUploadService
{
    [Post("/upload")]
    public string Upload([FromForm] IFormFile file)
    {
        // 此处仅返回上传文件名,实际保存逻辑由业务实现。
        return file.FileName;
    }
}

上传请求使用 multipart/form-data,字段名为 file,同时需要提交有效的防伪 Cookie 和请求令牌。自动配置不会替客户端获取、提交令牌;宿主可通过 IAntiforgery.GetAndStoreTokens(HttpContext) 提供令牌,并通过 AddAntiforgery(options => ...) 自定义表单字段或请求头名称。

自动中间件配置要求映射入口的接收对象同时实现 IApplicationBuilder,例如 WebApplication。在 RouteGroupBuilder 或传统 UseEndpoints 回调中映射时,宿主仍须在应用管道中手动调用 UseAntiforgery(),位于认证、授权之后及端点执行之前。直接参数以外的 DTO 文件属性和仅含普通字段的 [FromForm] 参数不在本次自动检测范围内,需要宿主手动配置。

响应类型声明与异步调用

生成器从所映射的接口方法推导响应类型,使用方法显式配置优先、类配置次之的最终 NonUnify 值:

接口返回类型 启用包装 禁用包装
T Produces<ApiResult<T>>() Produces<T>()
Task<T> / ValueTask<T> Produces<ApiResult<T>>() Produces<T>()
void / Task / ValueTask Produces<ApiResult<object>>() Produces(200)

所有声明使用默认成功状态码 200。IResult、Results<...> 等类型不会被进一步拆解为实际响应体;需要精确描述时,在实现方法上使用 ApiResponse<T>。声明类型需要与实际响应相符,生成器不会替换、转换或验证实际业务数据。

public interface IOrderService
{
    object Get(int id);
    Task<object> GetAsync(int id);
}

public record OrderDto(int Id);

[EndpointGroup<IOrderService>("/orders")]
public class OrderService : IOrderService
{
    [Get("/{id}")]
    [ApiResponse<OrderDto>]
    public object Get(int id) => new OrderDto(id);

    [Get("/{id}/raw", NonUnify = true)]
    [ApiResponse<OrderDto>]
    public Task<object> GetAsync(int id) => Task.FromResult<object>(new OrderDto(id));
}

同步方法生成 service.Get(id) 和 .Produces<ApiResult<OrderDto>>();异步方法即使实现没有 async 关键字,也会生成以下调用:

group0.MapGet(
        "/{id}/raw",
        async ([Microsoft.AspNetCore.Mvc.FromServices] IOrderService service, int id)
            => await service.GetAsync(id))
    .WithMetadata(new Imizuki.UniversalApi.Unify.EndpointResponseMetadata(true))
    .AddEndpointFilter<Imizuki.UniversalApi.Unify.UnifyEndpointFilter>()
    .Produces<OrderDto>();

非泛型 Task / ValueTask 生成 async (...) => { await service.ExecuteAsync(...); },void 生成 (...) => { service.Execute(...); }。其他同步返回值直接转发。本次仅识别四种标准任务类型,不为自定义 awaitable 自动生成 await。

以下示例省略路由调用链,但保留实际 Lambda 的显式参数类型和服务注入特性(Task<T> 与 ValueTask<T> 使用同一种生成形式):

// 同步返回 T
([Microsoft.AspNetCore.Mvc.FromServices] ITestService service, int id) => service.Get(id)

// Task<T> / ValueTask<T>
async ([Microsoft.AspNetCore.Mvc.FromServices] ITestService service, int id) => await service.GetAsync(id)

// Task / ValueTask
async ([Microsoft.AspNetCore.Mvc.FromServices] ITestService service, int id) => { await service.ExecuteAsync(id); }

// void
([Microsoft.AspNetCore.Mvc.FromServices] ITestService service, int id) => { service.Execute(id); }

ApiResponse<T> 只读取被标记的实现方法,不读取接口方法,也不影响同步或异步调用的判断。覆盖类型也要经过包装规则:[ApiResponse<OrderDto>] 在启用包装时声明 ApiResult<OrderDto>,禁用时声明 OrderDto;无返回值方法同样允许覆盖。重复声明(包括泛型实参不同的声明)、类型不可访问或不能用作普通泛型参数时,生成器在对应实现方法特性处报告 IUA002;推导类型无效时定位到实现方法。

迁移旧代码

将 [EndpointGroup("/test")] 改为 [EndpointGroup<ITestService>("/test")],让服务类实现接口,将静态端点改为接口实例方法,并在构建应用前调用 AddGeneratedEndpointServices()。

删除独立的 [NonUnify] / [NonUnifyAttribute],改为分组或 HTTP 方法特性上的 NonUnify = true。类上禁用后,方法可设置 NonUnify = false 恢复包装。

生成器会报告源码位置明确的错误:IUA001 表示分组或生命周期无效,IUA002 表示方法、参数或响应配置无法映射,IUA003 表示服务接口绑定冲突。

Refit 客户端自动注册

运行时相关代码集中在 Imizuki.UniversalApi/Refit/,注册生成器位于 Imizuki.UniversalApi.Generator/Refit/。两者与现有端点映射独立,Refit 自身负责 HTTP 接口代理生成。

using Imizuki.UniversalApi.Generated;
using Imizuki.UniversalApi.Refit;
using Microsoft.AspNetCore.Builder;

var builder = WebApplication.CreateBuilder(args);
builder.Services.MapCustomRefitClient();

var app = builder.Build();
app.Run();

public record ItemDto(int Id);

[RefitConfig("https://api.example.com", Timeout = 30, Name = "ExampleApi")]
public interface IExampleApi : IResilienceApi
{
    [global::Refit.Get("/items/{id}")]
    Task<ItemDto> GetAsync(int id);
}

MapCustomRefitClient() 只注册当前编译中显式标记的接口,应在启动时调用一次。partial 声明不会重复生成注册;派生接口需要独立注册时应再次声明 RefitConfig。没有有效标记接口时不生成注册入口。

每个接口生成 AddRefitGeneratedClient<TApi>(),并在 ConfigureHttpClient 中设置 BaseAddress 和 Timeout;仅当接口直接或间接继承 IResilienceApi 时,追加 AddResilienceHandler。客户端依赖 Refit 自身的源生成结果,不回退到反射实现;消费项目必须启用 Refit 源生成器。

配置项 规则
BaseUrl 必填构造参数,只接受绝对 HTTP/HTTPS 地址
Timeout 秒数,默认 30;支持 HttpClient 范围内的有限正数(至少一个 tick)或 -1
Name 弹性管道名;未填写或空白时使用接口简单名称,不改变 Refit 的 HttpClient 名称

无效地址、超时、重复特性、不可访问接口或开放泛型接口通过 IUA004 定位到源码。Refit HTTP 方法使用 global::Refit.Get 等特性,与本库的服务端端点特性不同。

默认弹性行为

IResilienceApi 默认按“重试 → 单次超时”配置管道:

  • 仅 GET、HEAD 允许重试;未知请求方法和其他方法均不重试。
  • 暂时性故障判断沿用 HttpRetryStrategyOptions 默认条件,包括请求异常、策略超时、HTTP 408、429 和 5xx。
  • 最多重试 2 次,包含首次请求最多执行 3 次;基础延迟 2 秒,指数退避并启用抖动,支持 Retry-After。
  • 单次尝试超时 10 秒,总调用时间由 HttpClient.Timeout 控制,默认 30 秒。
  • 默认不添加熔断、限流或额外总超时策略。

Timeout = -1 仅取消 HttpClient 总超时,默认管道中的 10 秒单次超时仍然生效。不继承 IResilienceApi 的接口只配置 HttpClient,不应用以上策略。

覆盖弹性策略

派生接口使用显式接口实现覆盖配置方法。以下示例完全替换默认策略,仅保留自定义的 5 秒单次超时:

using Imizuki.UniversalApi.Refit;
using Microsoft.Extensions.Http.Resilience;
using Polly;

[RefitConfig("https://api.example.com")]
public interface ICustomApi : IResilienceApi
{
    [global::Refit.Get("/status")]
    Task<string> GetStatusAsync();

    static void IResilienceApi.ConfigureResilience(
        ResiliencePipelineBuilder<HttpResponseMessage> builder,
        ResilienceHandlerContext context)
    {
        builder.AddTimeout(TimeSpan.FromSeconds(5));
    }
}

生成代码通过约束为 IResilienceApi 的类型参数调用 TApi.ConfigureResilience(builder, context),以采用派生接口最具体的实现。不要使用 new static ConfigureResilience 隐藏基接口方法;覆盖后默认重试和超时不会再被追加。上下文可用于获取服务和配置。

MVC 配置示例

using Imizuki.UniversalApi.Core;
using Imizuki.UniversalApi.Unify;
using Imizuki.UniversalApi.Utils;

var builder = WebApplication.CreateBuilder(args);
builder.AddApplicationCore();
builder.Services.AddControllers()
    .AddNewtonsoftJson(options => options.UseMvcJsonOptions())
    .AddInjectWithUnifyResult();

var app = builder.Build();
app.UseApplicationCore();
app.MapControllers();
app.Run();

构建与打包

dotnet build Imizuki.UniversalApi.slnx -c Release
dotnet pack Imizuki.UniversalApi/Imizuki.UniversalApi.csproj -c Release -o artifacts

代码规范与验证

项目通过 .editorconfig 统一代码格式,类、方法和关键处理步骤使用中文注释。

dotnet format whitespace Imizuki.UniversalApi.slnx --verify-no-changes
dotnet format style Imizuki.UniversalApi.slnx --verify-no-changes

本次改造不新增测试用例或临时测试程序,使用 Release 构建、格式检查和生成逻辑静态核查验证。构建检查不会替代消费者端点的运行时验证。

尚未指定开源许可证、项目主页或源码仓库地址;发布前应按实际情况补充相应 NuGet 元数据。

No packages depend on Imizuki.UniversalApi.

检测生成端点中的 IFormFile 和 IFormFileCollection 参数,自动注册防伪服务,并在应用端点映射入口添加 UseAntiforgery;已有防伪中间件时避免重复添加;补充文件上传、调用顺序和客户端防伪令牌说明。

Version Downloads Last updated
1.2.3 0 11/05/2026
1.2.2 1 11/05/2026
1.2.1 0 11/05/2026
1.2.0 0 11/05/2026
1.1.1 0 11/05/2026
1.1.0 0 11/05/2026
1.0.2 0 11/05/2026
1.0.1 0 11/05/2026
1.0.0 0 11/05/2026