O ASP.NET Core no .NET 11 oferece suporte prático ao uso de tipos union e hierarquias fechadas com System.Text.Json, permitindo representar um valor que pode pertencer a mais de um tipo ou a uma família específica de tipos. Esse suporte se estende a Minimal APIs, controladores MVC, SignalR, Blazor e documentação OpenAPI, com diferenças importantes na forma de leitura e escrita e nas limitações do binding de parâmetros.
Quando um union é adequado?
Um union é adequado para contratos que aceitam alternativas independentes sem a necessidade de um contêiner JSON ou de um campo discriminator. Exemplos incluem um valor que pode ser um número inteiro ou uma cadeia de caracteres, ou uma configuração que aceita um valor booleano ou um objeto com opções mais detalhadas. Durante a serialização, o caso ativo é escrito diretamente em seu formato natural; não é adicionado nenhum contêiner específico do tipo union nem discriminator.
Os casos que podem ser distinguidos claramente pela estrutura JSON são serializados automaticamente. No entanto, se os casos compartilharem a mesma estrutura, como um union que contenha os objetos Cat e Dog, o System.Text.Json não conseguirá determinar o tipo apenas a partir do token JSON. Nesse caso, é possível fornecer um JsonTypeClassifier personalizado para escolher o caso correto. O mesmo se aplica a alguns contratos de solicitação que aceitam um número ou uma cadeia de caracteres, pois as configurações JSON específicas da Web permitem ler números a partir de cadeias, o que pode tornar o texto interpretável como qualquer um dos dois casos.
Hierarquias fechadas são diferentes de union
Usar o modificador closed não significa que a serialização polimórfica será ativada automaticamente. Ao lidar com um tipo derivado concreto, como PaymentAuthorized, o System.Text.Json o trata como qualquer outro tipo e produz um JSON que inclui paymentId e amount sem discriminator.
Já ao usar o tipo base PaymentEvent em uma API, é necessário especificar o tipo derivado que deve ser criado. Isso pode ser ativado no tipo base usando JsonPolymorphic(InferClosedTypePolymorphism = true) ou por meio de JsonSerializerOptions.InferClosedTypePolymorphism. Nesse caso, o System.Text.Json infere os tipos derivados na serialização fechada e usa seus nomes como identificadores, como $type com o valor PaymentAuthorized.
A escolha entre os dois modelos depende da natureza do contrato: um union representa alternativas que talvez não pertençam a uma única família, enquanto hierarquias fechadas são adequadas a um conjunto relacionado de eventos ou entidades que precisam de um discriminator claro.
Suporte nas camadas do ASP.NET Core
Nas Minimal APIs, um union pode ser usado como parâmetro do corpo da solicitação ou como tipo de retorno, tanto no caminho de execução habitual quanto por meio do Request Delegate Generator, com comportamento idêntico entre os dois caminhos. Ele também pode aparecer dentro de outro modelo, como elemento de IAsyncEnumerable<T> ou dentro de um contêiner que use AsParameters, e as configurações de ConfigureHttpJsonOptions continuam influenciando a serialização.
Os controladores MVC oferecem suporte a unions como parâmetros de ações e tipos de retorno, incluindo Task<TUnion> e ValueTask<TUnion>. Como o MVC usa as configurações JsonSerializerDefaults.Web, o problema de ambiguidade entre número e cadeia de caracteres nas entradas HTTP também se aplica a ele.
No SignalR, o suporte funciona por meio do JsonHubProtocol para parâmetros, valores de retorno e elementos de streaming. Um union do tipo int ou string não precisa de classifier ao usar esse protocolo, porque o JsonHubProtocol não trata o token JSON de cadeia de caracteres como ambíguo nesse caso. Porém, unions cujos casos compartilham o token StartObject, como UnionPet(Cat, Dog), continuam precisando de um classifier. O SignalR não oferece suporte a essa funcionalidade ao usar os hub protocols MessagePack ou Newtonsoft.Json.
Blazor e OpenAPI
Os parâmetros de componentes Blazor dentro do processo são tratados por atribuição direta e, portanto, não precisam de serialização. No entanto, a interoperabilidade com JavaScript, o estado persistido dos componentes e os parâmetros durante o prerendering passam pelo System.Text.Json e seguem as mesmas regras dos unions. Por exemplo, é possível passar um union para JavaScript no formato Boolean ou como um objeto de opções, de acordo com o caso ativo, sem adicionar um contêiner ou discriminator.
No OpenAPI, um union é representado usando um esquema anyOf que inclui um tipo para cada caso. Os casos reutilizam os mesmos nomes dos componentes Cat e Dog quando aparecem como tipos independentes. Isso difere dos tipos polimórficos que contêm um discriminator e têm seus esquemas publicados com nomes distintos. O ApiExplorer também pode representar vários tipos de resposta para o mesmo código de status e tipo de conteúdo dentro de anyOf.
Limitações a considerar
O suporte a unions depende do System.Text.Json e, portanto, não funciona com fontes de binding que não passam pela análise de JSON. Isso inclui valores de query string, valores de rota, cabeçalhos e campos de formulário. Um valor como ?id=42 não é suficiente para determinar se a intenção é int, string ou Guid. No Blazor, a limitação se aplica a [SupplyParameterFromQuery] e ao binding de formulários usando [SupplyParameterFromForm]. A fonte indica que o binding de union a partir dessas fontes ainda está em estudo e em fase de coleta de feedback.
Leitura editorial: o valor principal aqui não é adicionar um novo formato ao JSON, mas unificar o tratamento de contratos com múltiplos casos em grandes partes do ecossistema do .NET 11. No entanto, o sucesso do design depende da capacidade de distinguir os casos e da fonte de dados; por isso, é recomendável definir um classifier ou discriminator antecipadamente e não presumir que o suporte à serialização signifique suporte a todos os métodos de binding de parâmetros.