ASP.NET Core Results<T1, T2>深度解析

一、从痛点说起

ASP.NET Core Minimal API 出现之前,Controller 时代的返回类型写法早已让人习惯了模糊性:

bash 复制代码
// Controller 时代 ------ 返回类型完全不透明
public IActionResult GetUser(int id)
{
    if (id <= 0) return BadRequest("Invalid ID");
    var user = _repo.Find(id);
    if (user == null) return NotFound();
    return Ok(user);
}

这段代码的问题在于:Swagger/OpenAPI 不知道这个接口到底会返回什么 ,必须靠 [ProducesResponseType] 手动标注。

Minimal API 引入后,问题依然存在:

bash 复制代码
// 早期 Minimal API ------ 同样不透明
app.MapGet("/user/{id}", (int id) =>
{
    if (id <= 0) return Results.BadRequest("Invalid ID");
    // ...
});

Results<T1, T2> 就是为解决这个问题而生的 联合类型(Union Type)


二、Results<T1, T2> 是什么?

一句话Results<T1, T2> 是一个 值类型联合体,它携带了类型信息,使框架能在编译期 / 反射期自动推断出接口所有可能的响应类型,从而自动生成 OpenAPI 文档。

bash 复制代码
app.MapGet("/user/{id}", (int id) =>
{
    if (id <= 0) return TypedResults.BadRequest("Invalid ID");
    var user = _repo.Find(id);
    if (user == null) return TypedResults.NotFound();
    return TypedResults.Ok(user);
})
// 🎉 无需任何 Attribute,Swagger 自动识别 200 / 400 / 404

返回类型签名:

bash 复制代码
Results<Ok<User>, BadRequest<string>, NotFound>

三、核心类型体系

3.1 整体类图

bash 复制代码
IResult
  └── IValueHttpResult           (有返回值)
  └── IStatusCodeHttpResult      (有状态码)
  └── IEndpointMetadataProvider  (提供 OpenAPI 元数据)

TypedResults.Ok<T>     ──实现──▶ IResult, IValueHttpResult<T>, IStatusCodeHttpResult
TypedResults.NotFound  ──实现──▶ IResult, IStatusCodeHttpResult
TypedResults.BadRequest<T>  ──▶ IResult, IValueHttpResult<T>, IStatusCodeHttpResult

Results<T1, T2>        ──持有──▶ IResult(实际执行时委托给内部 IResult)

3.2 关键接口

bash 复制代码
// 所有响应结果的根接口
public interface IResult
{
    Task ExecuteAsync(HttpContext httpContext);
}

// 携带值的结果
public interface IValueHttpResult<out TValue>
{
    TValue? Value { get; }
}

// 携带状态码的结果
public interface IStatusCodeHttpResult
{
    int? StatusCode { get; }
}

// 🔑 最关键:提供 OpenAPI 元数据的接口
public interface IEndpointMetadataProvider
{
    static abstract void PopulateMetadata(MethodInfo method, EndpointBuilder builder);
}

四、Results<T1, T2> 源码解析

4.1 结构定义(简化版)

ASP.NET Core 源码中,Results<T1, T2> 是一个 只读结构体(readonly struct)

bash 复制代码
public readonly struct Results<TResult1, TResult2> : IResult
    where TResult1 : IResult
    where TResult2 : IResult
{
    // 内部持有真正的 IResult 实例
    private readonly IResult _activeResult;

    // 私有构造(只能通过隐式转换创建)
    private Results(IResult activeResult)
    {
        _activeResult = activeResult;
    }

    // ✅ 隐式转换:T1 → Results<T1,T2>
    public static implicit operator Results<TResult1, TResult2>(TResult1 result)
        => new(result);

    // ✅ 隐式转换:T2 → Results<T1,T2>
    public static implicit operator Results<TResult1, TResult2>(TResult2 result)
        => new(result);

    // ✅ 委托执行:转发给真正的 IResult
    public Task ExecuteAsync(HttpContext httpContext)
        => _activeResult.ExecuteAsync(httpContext);
}

💡 关键设计 :使用 readonly struct 避免堆分配,隐式转换让调用方代码自然流畅,无需 new 或强转。

4.2 为何能自动生成 OpenAPI 元数据?

Results<T1, T2> 还实现了 IEndpointMetadataProvider

bash 复制代码
public readonly struct Results<TResult1, TResult2> : IResult, IEndpointMetadataProvider
    where TResult1 : IResult
    where TResult2 : IResult
{
    // 静态抽象方法实现(C# 11 特性)
    static void IEndpointMetadataProvider.PopulateMetadata(
        MethodInfo method, EndpointBuilder builder)
    {
        // 遍历每个泛型参数,如果它也实现了 IEndpointMetadataProvider
        // 就递归调用其 PopulateMetadata
        PopulateMetadataForType<TResult1>(method, builder);
        PopulateMetadataForType<TResult2>(method, builder);
    }

    private static void PopulateMetadataForType<T>(
        MethodInfo method, EndpointBuilder builder)
    {
        if (typeof(IEndpointMetadataProvider).IsAssignableFrom(typeof(T)))
        {
            // 利用反射调用 T 的静态 PopulateMetadata
            T.PopulateMetadata(method, builder); // C# 11 静态抽象接口调用
        }
    }
}

每个具体结果类型(如 Ok<T>)都实现了自己的 PopulateMetadata

bash 复制代码
// Ok<T> 的元数据注入
public sealed class Ok<TValue> : IResult, IEndpointMetadataProvider, IStatusCodeHttpResult
{
    static void IEndpointMetadataProvider.PopulateMetadata(
        MethodInfo method, EndpointBuilder builder)
    {
        // 向 endpoint 注入:HTTP 200,响应体类型为 TValue
        builder.Metadata.Add(
            new ProducesResponseTypeMetadata(typeof(TValue), StatusCodes.Status200OK, "application/json")
        );
    }
}

五、完整执行流程

bash 复制代码
HTTP 请求到达
       │
       ▼
┌─────────────────────────────────┐
│    路由匹配 (Route Matcher)      │
└────────────────┬────────────────┘
                 │
                 ▼
┌─────────────────────────────────┐
│  终结点调度 (EndpointMiddleware) │
└────────────────┬────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────┐
│  RequestDelegateFactory 调用 Handler                 │
│                                                     │
│  async Task<Results<Ok<User>, NotFound>> Handler()  │
│  {                                                  │
│      if (notFound)                                  │
│          return TypedResults.NotFound();  ──────────┼──▶ 隐式转换为 Results<>
│      return TypedResults.Ok(user);        ──────────┼──▶ 隐式转换为 Results<>
│  }                                                  │
└────────────────┬────────────────────────────────────┘
                 │ 返回 Results<Ok<User>, NotFound>
                 ▼
┌─────────────────────────────────────────────────────┐
│  RequestDelegateFactory 自动生成的包装代码            │
│                                                     │
│  var result = await handler(...);                   │
│  await result.ExecuteAsync(httpContext);  ──────────┼──▶ 委托给内部 _activeResult
└────────────────┬────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────┐
│  具体 Result 执行 (e.g. Ok<User>)                    │
│                                                     │
│  1. httpContext.Response.StatusCode = 200           │
│  2. httpContext.Response.ContentType = "app/json"   │
│  3. JsonSerializer.SerializeAsync(user, ...)        │
└────────────────┬────────────────────────────────────┘
                 │
                 ▼
             HTTP 响应写出

六、RequestDelegateFactory:幕后英雄

Results<T1, T2> 能无缝工作,背后有一个关键角色:RequestDelegateFactory(RDF)。

它在应用启动时(非请求时)做两件事:

6.1 编译期:生成元数据

bash 复制代码
// 伪代码,表达 RDF 的逻辑
internal static RequestDelegateResult Create(Delegate handler, ...)
{
    var method = handler.Method;
    var returnType = method.ReturnType; // e.g. Results<Ok<User>, NotFound>

    // 如果返回类型实现了 IEndpointMetadataProvider
    if (typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType))
    {
        // 调用静态方法填充元数据 → Swagger 就知道响应类型了
        IEndpointMetadataProvider.PopulateMetadata(returnType, method, endpointBuilder);
    }

    // 生成 RequestDelegate(运行时调用的委托)
    var requestDelegate = CreateRequestDelegate(handler, ...);
    return new RequestDelegateResult(requestDelegate, endpointBuilder.Metadata);
}

6.2 运行期:执行委托

bash 复制代码
// RDF 为异步返回 IResult 的 Handler 生成的代码(简化)
async Task GeneratedRequestDelegate(HttpContext httpContext)
{
    // 1. 参数绑定
    var id = ...; // 从路由/查询/Body 绑定

    // 2. 调用用户 Handler
    var result = await userHandler(id);
    // result 类型是 Results<Ok<User>, NotFound>

    // 3. 调用 ExecuteAsync(多态分派给 _activeResult)
    if (result is IResult r)
        await r.ExecuteAsync(httpContext);
}

七、Results vs TypedResults 的区别

对比项 Results(静态类) TypedResults(静态类)
返回类型 IResult(接口) 具体类型,如 Ok<T>
OpenAPI 推断 ❌ 无法自动推断 ✅ 自动推断
单元测试 较难(需检查响应流) ✅ 直接断言返回值
搭配 Results<> ❌ 类型丢失 ✅ 类型完整保留
bash 复制代码
// ❌ 用 Results,类型信息丢失
app.MapGet("/a", () => Results.Ok(new User()));
// Swagger 不知道响应体是 User

// ✅ 用 TypedResults,类型信息保留
app.MapGet("/b", () => TypedResults.Ok(new User()));
// Swagger 自动生成 User 的 Schema

八、单元测试友好性

Results<T1, T2> 的一大优势是让 Handler 可以脱离 HTTP 环境进行单元测试:

bash 复制代码
// 被测 Handler
static Results<Ok<User>, NotFound> GetUser(int id, IUserRepo repo)
{
    var user = repo.Find(id);
    return user is null
        ? TypedResults.NotFound()
        : TypedResults.Ok(user);
}

// 单元测试
[Fact]
public void GetUser_ReturnsOk_WhenUserExists()
{
    var repo = new FakeRepo(new User { Id = 1, Name = "Alice" });

    var result = GetUser(1, repo);

    // ✅ 直接断言类型,无需 Mock HttpContext
    var ok = Assert.IsType<Ok<User>>(result.Result);
    Assert.Equal("Alice", ok.Value!.Name);
}

[Fact]
public void GetUser_ReturnsNotFound_WhenMissing()
{
    var repo = new FakeRepo(null);
    var result = GetUser(99, repo);

    Assert.IsType<NotFound>(result.Result); // ✅
}

💡 注意:访问内部值需要 result.Result 属性(Results<> 暴露的内部 IResult)。


九、泛型参数扩展:T1 ~ T6

ASP.NET Core 提供了最多 6 个类型参数 的重载,均为独立的 struct 定义:

bash 复制代码
Results<T1, T2>
Results<T1, T2, T3>
Results<T1, T2, T3, T4>
Results<T1, T2, T3, T4, T5>
Results<T1, T2, T3, T4, T5, T6>

每个都遵循相同模式:持有 IResult _activeResult,通过隐式转换赋值,调用时委托执行。

实际使用示例:

bash 复制代码
app.MapPost("/user", async (CreateUserRequest req, IUserService svc)
    => (Results<Created<User>, BadRequest<ValidationProblemDetails>, Conflict, UnauthorizedHttpResult>)
    await svc.CreateAsync(req));

十、 设计 亮点总结 ✨

设计点 技术手段 解决的问题
零堆分配 readonly struct 高频请求下的 GC 压力
自然语法 隐式转换运算符 无需显式 new 或强转
自动文档 IEndpointMetadataProvider + C# 11 静态抽象接口 OpenAPI 零标注
可测试性 具体返回类型而非 IResult 接口 脱离 HTTP 环境断言
多态执行 _activeResult.ExecuteAsync() 委托 运行时正确分派
编译安全 泛型约束 where T : IResult 防止传入非法类型

十一、一个完整的实战示例 💻

bash 复制代码
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();

app.MapGet("/users/{id:int}", async Task<Results<Ok<UserDto>, NotFound, BadRequest<string>>>
    (int id, IUserRepository repo) =>
{
    if (id <= 0)
        return TypedResults.BadRequest("ID 必须大于 0");

    var user = await repo.FindAsync(id);
    if (user is null)
        return TypedResults.NotFound();

    return TypedResults.Ok(new UserDto(user.Id, user.Name));
})
.WithName("GetUser")
.WithTags("Users");
// 🎉 Swagger 自动展示:200(UserDto) / 400(string) / 404

app.Run();

record UserDto(int Id, string Name);

生成的 Swagger UI 会自动出现:

bash 复制代码
GET /users/{id}
  Responses:
    200  OK          → UserDto { id: int, name: string }
    400  Bad Request → string
    404  Not Found   → (no body)

总结

Results<T1, T2>ASP.NET Core 在 Minimal API 上的一次精妙设计:

  • readonly struct + 隐式转换 实现了轻量级的联合类型
  • IEndpointMetadataProvider + 静态抽象接口 实现了零标注的 OpenAPI 元数据
  • RequestDelegateFactory 在启动时完成元数据收集,在运行时完成透明的 ExecuteAsync 分派

它的出现让 Minimal API 真正做到了:代码即文档,类型即契约

相关推荐
TYKJ0232 小时前
Java服务器:8核16G真的适合你吗
后端
砍材农夫2 小时前
物联网 基于netty构建mqtt协议规范(轻量级二进制协议)
后端
Cache技术分享2 小时前
412. Java 文件操作基础 - 用装饰者模式定制 BufferedReader 实现结构化文本读取
前端·后端
逍遥德2 小时前
SpringBoot自带TaskScheduler 接口使用详解:(02)微服务多实例模式下,爆发任务重复执行问题
spring boot·分布式·后端·微服务·中间件
考虑考虑2 小时前
JDK26中的LazyConstant
java·后端·java ee
Gauss松鼠会3 小时前
【GaussDB】基于SpringBoot实现操作GaussDB(DWS)的项目实战
java·数据库·经验分享·spring boot·后端·sql·gaussdb
用户4099322502123 小时前
Composable的命名规矩和参数约定,别再瞎写了
前端·javascript·后端
时间长河里你我皆过客3 小时前
linux ssh链接断断续续排查
后端
传说之后3 小时前
以Hadoop为例,解读分布式计算设计
后端·架构