编程与软件开发

如何在 ASP.NET Core 中使用 union 和封闭层次结构

.NET Blog 介绍了如何通过 Minimal APIs、MVC、SignalR、Blazor 和 OpenAPI,借助 System.Text.Json 使用 union 类型和封闭层次结构。同时,文章还指出了歧义情况以及不支持这些类型的绑定源。

2026-09-10
2 分钟阅读
11 浏览量
فريق تحرير certi.news
如何在 ASP.NET Core 中使用 union 和封闭层次结构

ASP.NET Core 在 .NET 11 中提供了使用 union 类型和封闭层次结构与 System.Text.Json 集成的实用支持,从而可以表示一个值属于多个类型中的任意一种,或属于一个特定的类型族。该支持扩展到 Minimal APIs、MVC 控制器、SignalR、Blazor 和 OpenAPI 文档,但在读写方式以及参数绑定限制方面存在重要差异。

何时适合使用 union?

union 适用于接受彼此独立的备选项、而无需 JSON 包装器或 discriminator 字段的契约。例如,一个值可以是整数或字符串,或者某个设置既可以接受布尔值,也可以接受包含更详细选项的对象。序列化时,活动状态会直接以其自然格式写入;不会添加专用于 union 的包装器或 discriminator。

能够从 JSON 结构中明确区分的状态会自动进行序列化。但是,如果多个状态共享相同的结构,例如包含 Cat 和 Dog 两个对象的 union,System.Text.Json 将无法仅根据 JSON token 确定类型。在这种情况下,可以提供自定义 JsonTypeClassifier 来选择正确的状态。某些同时接受数字或字符串的请求契约也存在同样的问题,因为 Web JSON 设置允许从字符串读取数字,这可能导致同一文本既可被解释为其中一种状态,也可被解释为另一种状态。

封闭层次结构不同于 union

使用 closed 修饰符并不意味着会自动启用多态序列化。处理 PaymentAuthorized 这样的具体派生类型时,System.Text.Json 会像处理其他类型一样处理它,并生成包含 paymentId 和 amount 的 JSON,而不会包含 discriminator。

但在 API 中使用基类型 PaymentEvent 时,则必须指定应创建的派生类型。可以在基类型上使用 JsonPolymorphic(InferClosedTypePolymorphism = true) 启用此功能,也可以通过 JsonSerializerOptions.InferClosedTypePolymorphism 启用。此时,System.Text.Json 会推断封闭层次结构中的派生类型,并使用其名称作为标识符,例如将 $type 的值设为 PaymentAuthorized。

两种模型的选择取决于契约的性质:union 表示可能不属于同一类型族的备选项,而封闭层次结构适合表示一组相互关联、需要明确 discriminator 的事件或实体。

ASP.NET Core 各层的支持

在 Minimal APIs 中,可以将 union 用作请求正文参数或返回类型,无论是在常规执行路径中,还是通过 Request Delegate Generator 使用,两条路径的行为都一致。union 也可以出现在另一个模型内部、作为 IAsyncEnumerable<T> 的元素,或出现在使用 AsParameters 的容器中;ConfigureHttpJsonOptions 的设置仍会影响序列化。

MVC 控制器支持将 union 用作操作方法参数和返回类型,包括 Task<TUnion> 和 ValueTask<TUnion>。由于 MVC 使用 JsonSerializerDefaults.Web 设置,因此 HTTP 输入中数字与字符串之间的歧义问题同样适用于 MVC。

在 SignalR 中,该支持通过 JsonHubProtocol 作用于参数、返回值和流元素。使用此协议时,int 或 string 类型的 union 不需要 classifier,因为 JsonHubProtocol 在这种情况下不会将字符串 JSON token 视为有歧义。但是,如果 union 的各个状态共享 StartObject token,例如 UnionPet(Cat, Dog),则仍需要 classifier。使用 MessagePack 或 Newtonsoft.Json hub protocols 时,SignalR 不支持此功能。

Blazor 和 OpenAPI

进程内 Blazor 组件参数通过直接赋值传递,因此不需要序列化。但是,JavaScript interop、保存的组件状态以及 prerendering 期间的参数都会经过 System.Text.Json,因此遵循相同的 union 规则。例如,可以根据活动状态,以 Boolean 或选项对象的形式将 union 传递给 JavaScript,而无需添加包装器或 discriminator。

在 OpenAPI 中,union 使用 anyOf schema 表示,其中为每个状态包含一个类型。当 Cat 和 Dog 作为独立类型出现时,各状态会继续使用 Cat 和 Dog 组件自身的名称。这不同于带有 discriminator 的多态类型,后者会以独特名称提升其 schema。此外,ApiExplorer 还可以在同一状态码和内容类型下,将多个响应类型表示在 anyOf 中。

需要注意的限制

union 的支持依赖于 System.Text.Json,因此不适用于不经过 JSON 分析的绑定源。这些来源包括 query string 值、路径值、请求头和表单字段。像 ?id=42 这样的值不足以确定意图是 int、string 还是 Guid。在 Blazor 中,该限制同样适用于 [SupplyParameterFromQuery] 以及使用 [SupplyParameterFromForm] 进行的表单绑定。文章指出,从这些来源绑定 union 仍处于研究和收集反馈阶段。

编辑解读:这里的核心价值并不是为 JSON 增加一种新格式,而是在 .NET 11 生态的广泛组成部分之间统一处理多状态契约。不过,设计能否成功取决于各状态是否可区分以及数据来源;因此,应尽早确定 classifier 或 discriminator,并且不要假设支持序列化就意味着支持所有参数绑定方式。

新闻来源
ف
作者

فريق تحرير certi.news

同一分类

你可能还喜欢

查看所有新闻