c#联合类型,解决响应接口返回结果有多重变体

发布时间:2026/10/11 11:41:29
c#联合类型,解决响应接口返回结果有多重变体 一、C# 没有联合类型但我们可以“搭”一个先对齐概念。联合类型Union Type的意思是一个值只能是预先定义好的几种形态之一。比如“创建订单”结果无非三种——成功、参数校验失败、业务规则拒绝。F#、Swift 这类语言有原生的 discriminated unionC# 至今没有这个语法糖C# 12 起有实验性的UnionAttribute/IUnion尝试但还不是一等公民语法。不过用 record 继承加模式匹配完全能模拟出一个够用的版本// 基类代表创建订单这个操作所有可能的结果abstract 防止外部直接实例化public abstract record CreateOrderResult; // 形态一成功携带订单号和金额 public sealed record OrderCreated(Guid OrderId, decimal Total) : CreateOrderResult; // 形态二参数校验失败 public sealed record OrderValidationFailed(string Message) : CreateOrderResult; // 形态三业务规则拒绝比如超出信用额度 public sealed record OrderRejected(string Reason) : CreateOrderResult;派生类型全部sealed应用自己控制谁能继承——这套结构就是“约定上的封闭集合”。但要说清楚它不是编译器强制的封闭联合别人照样能偷偷继承一个新类型出来所以兜底逻辑不能省后面会讲。二、让 System.Text.Json 认识这套类型光有类型层次不够序列化器得知道“这段 JSON 对应哪个派生类型”。.NET 7 之后System.Text.Json 原生支持多态序列化两个特性搞定using System.Text.Json.Serialization; // 在基类上注册所有派生类型并给每个类型起一个稳定的 JSON 判别名 [JsonPolymorphic(TypeDiscriminatorPropertyName kind)] [JsonDerivedType(typeof(OrderCreated), order_created)] [JsonDerivedType(typeof(OrderValidationFailed), validation_failed)] [JsonDerivedType(typeof(OrderRejected), order_rejected)] public abstract record CreateOrderResult; public sealed record OrderCreated(Guid OrderId, decimal Total) : CreateOrderResult; public sealed record OrderValidationFailed(string Message) : CreateOrderResult; public sealed record OrderRejected(string Reason) : CreateOrderResult;序列化出来的 JSON 长这样kind字段就是类型判别符{ kind: order_created, orderId: d8b2c01e-ff31-4d0a-a1e2-3d6c6c6e8a10, total: 149.99 }被拒绝时则是另一种形状{ kind: order_rejected, reason: 订单超出客户信用额度。 }判别值要写order_created、order_rejected这种业务语义别把 CLR 类型名直接暴露出去——类型名是实现细节哪天重构改名客户端契约就跟着崩了。另外命名风格要统一要么全 snake_case要么全 camelCase别created和validation_failed混着来。三、我踩过最狠的坑声明类型不对kind 直接消失这个坑值得单独拎出来讲。多态序列化有个前提序列化时使用的声明类型必须是注册过多态信息的基类型。// 反面教材声明类型是派生类序列化结果里不会有 kind 字段 OrderCreated created new(Guid.NewGuid(), 149.99m); string bad JsonSerializer.Serialize(created); // 正确姿势用基类型变量装着派生对象 CreateOrderResult good new OrderCreated(Guid.NewGuid(), 149.99m); string ok JsonSerializer.Serialize(good); // 这才有 kind: order_created // 也可以显式指定泛型参数效果一样 string ok2 JsonSerializer.SerializeCreateOrderResult(created);你发现没有这跟直觉有点拧对象明明是OrderCreated却必须“装”在基类型的变量里序列化。我第一次遇到时对着日志找了快半小时。记住一句话想多态序列化时用的声明类型就得是基类。四、在 Minimal API 里落地两层各干各的先把请求类型和服务接口定下来public sealed record CreateOrderRequest(string CustomerId, decimal Total); public interface IOrderService { TaskCreateOrderResult CreateAsync( CreateOrderRequest request, CancellationToken cancellationToken); }端点只做一件事——把应用层算出的结果翻译成 HTTP 响应var builder WebApplication.CreateBuilder(args); var app builder.Build(); app.MapPost(/orders, async ( CreateOrderRequest request, IOrderService orderService, CancellationToken ct) { // 应用层负责算出是哪种结果完全不知道 HTTP 的存在 CreateOrderResult result await orderService.CreateAsync(request, ct); // API 层负责把结果映射成对应的状态码 return result switch { OrderCreated created Results.Created($/orders/{created.OrderId}, created), OrderValidationFailed f Results.BadRequest(f), OrderRejected rejected Results.Conflict(rejected), _ Results.Problem() }; }); app.Run();这里有个设计上的关键OrderRejected这种领域结果里从头到尾不该出现IResult、StatusCodes这些字眼。业务拒绝和 409 状态码是相关但不等价的两件事——同一个OrderRejected到了 gRPC 端点、消息消费者或 CLI 里可能该记日志、该触发补偿而不是拼 HTTP 响应。领域模型保持干净传输层的决策留在 API 边界这个结果对象拿去给别的入口复用一点问题没有。另外那个_ Results.Problem()兜底分支虽然理论上不该被触发派生类型 sealed、约定封闭但真被触发说明有人违反了约定。建议在这里记一条警告日志而不是静默返回 500——否则问题会被掩盖。五、反序列化好用但别裸奔读队列消息、处理落库的历史 JSON 时这套多态配置同样生效string json { kind: order_rejected, reason: 信用额度超限。 } ; // 基类注册过派生类型序列化器会按 kind 自动构造对应的派生记录 CreateOrderResult? result JsonSerializer.DeserializeCreateOrderResult(json);但两句大实话必须说一是反序列化成功不等于业务合法。注册JsonDerivedType只是告诉序列化器“有哪些类型”JSON 里的值对不对、金额是不是负数它一概不管业务校验一行都不能省。二是外部传入的 payload 要做对抗测试。缺失 kind、未知判别值、字段类型乱写——默认配置下缺失判别符或未知判别值都会抛JsonException这其实是好事最怕的是它悄悄变成一个“合法”的业务结果。如果你显式配置了JsonUnknownDerivedTypeHandling.FallBackToBaseType之类的回退策略那就更要想清楚回退到基类之后业务层能不能识别出这是一个“来路不明”的结果。六、OpenAPI 文档代码对了契约未必对还有个容易翻车的盲区序列化行为正确不代表生成的 OpenAPI 文档就自动把每种形态描述清楚了。一份合格的联合类型契约至少要说清楚有哪几种 JSON 形态、kind 的取值、每种结果对应的状态码以及客户端遇到未知 kind 时该怎么办。不同版本的 ASP.NET Core 和 OpenAPI 工具链对多态类型的 schema 生成能力参差不齐能不能产出带 discriminator mapping 的 oneOf 结构别想当然打开生成的文档亲自核对。要发布 SDK 或给其他团队生成强类型客户端这一步绝对省不得。七、什么时候别用这招说句公道话联合类型不是银弹。如果每种响应结构基本一致、只有一两个可选字段不同老老实实用单个 DTO 加可空属性更省心。联合类型真正发光的场景是结果集合小而封闭、不同结果携带不同数据、调用方需要显式分支。反过来为鸡毛蒜皮的变体都建类型枚举出十几种 Result那是在制造新的混乱。说白了接口契约的好坏从来不在于返回了多少字段而在于调用方能不能一眼看懂“可能发生什么”。把结果的每一种可能摆上台面让编译器和序列化器替你把关这才是对使用者最基本的尊重。码字不易如果您觉得我的文章对您有帮助的话烦请您打赏一元我买瓶水喝您的支持将是我继续坚持分享的无限动力谢谢

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询