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

发布时间:2026/5/20 8:43:01

ASP.NET Core Results<T1, T2>深度解析 一、从痛点说起在 ASP.NET Core Minimal API 出现之前Controller 时代的返回类型写法早已让人习惯了模糊性// Controller 时代 —— 返回类型完全不透明publicIActionResultGetUser(intid){if(id0)returnBadRequest(Invalid ID);varuser_repo.Find(id);if(usernull)returnNotFound();returnOk(user);}这段代码的问题在于Swagger/OpenAPI 不知道这个接口到底会返回什么必须靠[ProducesResponseType]手动标注。Minimal API 引入后问题依然存在// 早期 Minimal API —— 同样不透明app.MapGet(/user/{id},(intid){if(id0)returnResults.BadRequest(Invalid ID);// ...});ResultsT1, T2就是为解决这个问题而生的联合类型Union Type。二、ResultsT1, T2是什么一句话ResultsT1, T2是一个值类型联合体它携带了类型信息使框架能在编译期 / 反射期自动推断出接口所有可能的响应类型从而自动生成 OpenAPI 文档。app.MapGet(/user/{id},(intid){if(id0)returnTypedResults.BadRequest(Invalid ID);varuser_repo.Find(id);if(usernull)returnTypedResults.NotFound();returnTypedResults.Ok(user);})// 无需任何 AttributeSwagger 自动识别 200 / 400 / 404返回类型签名ResultsOkUser,BadRequeststring,NotFound三、核心类型体系3.1 整体类图IResult └── IValueHttpResult 有返回值 └── IStatusCodeHttpResult 有状态码 └── IEndpointMetadataProvider 提供 OpenAPI 元数据 TypedResults.OkT ──实现──▶ IResult, IValueHttpResultT, IStatusCodeHttpResult TypedResults.NotFound ──实现──▶ IResult, IStatusCodeHttpResult TypedResults.BadRequestT ──▶ IResult, IValueHttpResultT, IStatusCodeHttpResult ResultsT1, T2 ──持有──▶ IResult实际执行时委托给内部 IResult3.2 关键接口// 所有响应结果的根接口publicinterfaceIResult{TaskExecuteAsync(HttpContexthttpContext);}// 携带值的结果publicinterfaceIValueHttpResultoutTValue{TValue?Value{get;}}// 携带状态码的结果publicinterfaceIStatusCodeHttpResult{int?StatusCode{get;}}// 最关键提供 OpenAPI 元数据的接口publicinterfaceIEndpointMetadataProvider{staticabstractvoidPopulateMetadata(MethodInfomethod,EndpointBuilderbuilder);}四、ResultsT1, T2源码解析4.1 结构定义简化版ASP.NET Core 源码中ResultsT1, T2是一个只读结构体readonly structpublicreadonlystructResultsTResult1,TResult2:IResultwhereTResult1:IResultwhereTResult2:IResult{// 内部持有真正的 IResult 实例privatereadonlyIResult_activeResult;// 私有构造只能通过隐式转换创建privateResults(IResultactiveResult){_activeResultactiveResult;}// ✅ 隐式转换T1 → ResultsT1,T2publicstaticimplicitoperatorResultsTResult1,TResult2(TResult1result)new(result);// ✅ 隐式转换T2 → ResultsT1,T2publicstaticimplicitoperatorResultsTResult1,TResult2(TResult2result)new(result);// ✅ 委托执行转发给真正的 IResultpublicTaskExecuteAsync(HttpContexthttpContext)_activeResult.ExecuteAsync(httpContext);}关键设计使用readonly struct避免堆分配隐式转换让调用方代码自然流畅无需new或强转。4.2 为何能自动生成 OpenAPI 元数据ResultsT1, T2还实现了IEndpointMetadataProviderpublicreadonlystructResultsTResult1,TResult2:IResult,IEndpointMetadataProviderwhereTResult1:IResultwhereTResult2:IResult{// 静态抽象方法实现C# 11 特性staticvoidIEndpointMetadataProvider.PopulateMetadata(MethodInfomethod,EndpointBuilderbuilder){// 遍历每个泛型参数如果它也实现了 IEndpointMetadataProvider// 就递归调用其 PopulateMetadataPopulateMetadataForTypeTResult1(method,builder);PopulateMetadataForTypeTResult2(method,builder);}privatestaticvoidPopulateMetadataForTypeT(MethodInfomethod,EndpointBuilderbuilder){if(typeof(IEndpointMetadataProvider).IsAssignableFrom(typeof(T))){// 利用反射调用 T 的静态 PopulateMetadataT.PopulateMetadata(method,builder);// C# 11 静态抽象接口调用}}}每个具体结果类型如OkT都实现了自己的PopulateMetadata// OkT 的元数据注入publicsealedclassOkTValue:IResult,IEndpointMetadataProvider,IStatusCodeHttpResult{staticvoidIEndpointMetadataProvider.PopulateMetadata(MethodInfomethod,EndpointBuilderbuilder){// 向 endpoint 注入HTTP 200响应体类型为 TValuebuilder.Metadata.Add(newProducesResponseTypeMetadata(typeof(TValue),StatusCodes.Status200OK,application/json));}}五、完整执行流程HTTP 请求到达 │ ▼ ┌─────────────────────────────────┐ │ 路由匹配 (Route Matcher) │ └────────────────┬────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ 终结点调度 (EndpointMiddleware) │ └────────────────┬────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ RequestDelegateFactory 调用 Handler │ │ │ │ async TaskResultsOkUser, NotFound Handler() │ │ { │ │ if (notFound) │ │ return TypedResults.NotFound(); ──────────┼──▶ 隐式转换为 Results │ return TypedResults.Ok(user); ──────────┼──▶ 隐式转换为 Results │ } │ └────────────────┬────────────────────────────────────┘ │ 返回 ResultsOkUser, NotFound ▼ ┌─────────────────────────────────────────────────────┐ │ RequestDelegateFactory 自动生成的包装代码 │ │ │ │ var result await handler(...); │ │ await result.ExecuteAsync(httpContext); ──────────┼──▶ 委托给内部 _activeResult └────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ 具体 Result 执行 (e.g. OkUser) │ │ │ │ 1. httpContext.Response.StatusCode 200 │ │ 2. httpContext.Response.ContentType app/json │ │ 3. JsonSerializer.SerializeAsync(user, ...) │ └────────────────┬────────────────────────────────────┘ │ ▼ HTTP 响应写出六、RequestDelegateFactory幕后英雄ResultsT1, T2能无缝工作背后有一个关键角色RequestDelegateFactoryRDF。它在应用启动时非请求时做两件事6.1 编译期生成元数据// 伪代码表达 RDF 的逻辑internalstaticRequestDelegateResultCreate(Delegatehandler,...){varmethodhandler.Method;varreturnTypemethod.ReturnType;// e.g. ResultsOkUser, NotFound// 如果返回类型实现了 IEndpointMetadataProviderif(typeof(IEndpointMetadataProvider).IsAssignableFrom(returnType)){// 调用静态方法填充元数据 → Swagger 就知道响应类型了IEndpointMetadataProvider.PopulateMetadata(returnType,method,endpointBuilder);}// 生成 RequestDelegate运行时调用的委托varrequestDelegateCreateRequestDelegate(handler,...);returnnewRequestDelegateResult(requestDelegate,endpointBuilder.Metadata);}6.2 运行期执行委托// RDF 为异步返回 IResult 的 Handler 生成的代码简化asyncTaskGeneratedRequestDelegate(HttpContexthttpContext){// 1. 参数绑定varid...;// 从路由/查询/Body 绑定// 2. 调用用户 HandlervarresultawaituserHandler(id);// result 类型是 ResultsOkUser, NotFound// 3. 调用 ExecuteAsync多态分派给 _activeResultif(resultisIResultr)awaitr.ExecuteAsync(httpContext);}七、ResultsvsTypedResults的区别对比项Results静态类TypedResults静态类返回类型IResult接口具体类型如OkTOpenAPI 推断❌ 无法自动推断✅ 自动推断单元测试较难需检查响应流✅ 直接断言返回值搭配Results❌ 类型丢失✅ 类型完整保留// ❌ 用 Results类型信息丢失app.MapGet(/a,()Results.Ok(newUser()));// Swagger 不知道响应体是 User// ✅ 用 TypedResults类型信息保留app.MapGet(/b,()TypedResults.Ok(newUser()));// Swagger 自动生成 User 的 Schema八、单元测试友好性ResultsT1, T2的一大优势是让 Handler 可以脱离 HTTP 环境进行单元测试// 被测 HandlerstaticResultsOkUser,NotFoundGetUser(intid,IUserReporepo){varuserrepo.Find(id);returnuserisnull?TypedResults.NotFound():TypedResults.Ok(user);}// 单元测试[Fact]publicvoidGetUser_ReturnsOk_WhenUserExists(){varreponewFakeRepo(newUser{Id1,NameAlice});varresultGetUser(1,repo);// ✅ 直接断言类型无需 Mock HttpContextvarokAssert.IsTypeOkUser(result.Result);Assert.Equal(Alice,ok.Value!.Name);}[Fact]publicvoidGetUser_ReturnsNotFound_WhenMissing(){varreponewFakeRepo(null);varresultGetUser(99,repo);Assert.IsTypeNotFound(result.Result);// ✅} 注意访问内部值需要result.Result属性Results暴露的内部IResult。九、泛型参数扩展T1 ~ T6ASP.NET Core 提供了最多6 个类型参数的重载均为独立的 struct 定义ResultsT1,T2ResultsT1,T2,T3ResultsT1,T2,T3,T4ResultsT1,T2,T3,T4,T5ResultsT1,T2,T3,T4,T5,T6每个都遵循相同模式持有IResult _activeResult通过隐式转换赋值调用时委托执行。实际使用示例app.MapPost(/user,async(CreateUserRequestreq,IUserServicesvc)(ResultsCreatedUser,BadRequestValidationProblemDetails,Conflict,UnauthorizedHttpResult)awaitsvc.CreateAsync(req));十、设计亮点总结 ✨设计点技术手段解决的问题零堆分配readonly struct高频请求下的 GC 压力自然语法隐式转换运算符无需显式new或强转自动文档IEndpointMetadataProvider C# 11 静态抽象接口OpenAPI 零标注可测试性具体返回类型而非IResult接口脱离 HTTP 环境断言多态执行_activeResult.ExecuteAsync()委托运行时正确分派编译安全泛型约束where T : IResult防止传入非法类型十一、一个完整的实战示例 varbuilderWebApplication.CreateBuilder(args);builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();varappbuilder.Build();app.UseSwagger();app.UseSwaggerUI();app.MapGet(/users/{id:int},asyncTaskResultsOkUserDto,NotFound,BadRequeststring(intid,IUserRepositoryrepo){if(id0)returnTypedResults.BadRequest(ID 必须大于 0);varuserawaitrepo.FindAsync(id);if(userisnull)returnTypedResults.NotFound();returnTypedResults.Ok(newUserDto(user.Id,user.Name));}).WithName(GetUser).WithTags(Users);// Swagger 自动展示200(UserDto) / 400(string) / 404app.Run();recordUserDto(intId,stringName);生成的 Swagger UI 会自动出现GET /users/{id} Responses: 200 OK → UserDto { id: int, name: string } 400 Bad Request → string 404 Not Found → (no body)总结ResultsT1, T2是 ASP.NET Core 在 Minimal API 上的一次精妙设计用readonly struct 隐式转换实现了轻量级的联合类型用IEndpointMetadataProvider 静态抽象接口实现了零标注的 OpenAPI 元数据用RequestDelegateFactory在启动时完成元数据收集在运行时完成透明的ExecuteAsync分派它的出现让 Minimal API 真正做到了代码即文档类型即契约。

相关新闻