ASP.NET Core dans .NET 11 fournit une prise en charge pratique de l’utilisation des types union et des hiérarchies fermées avec System.Text.Json, ce qui permet de représenter une valeur pouvant appartenir à plusieurs types ou à une famille précise de types. Cette prise en charge s’étend aux Minimal APIs, aux contrôleurs MVC, à SignalR, à Blazor et à la documentation OpenAPI, avec des différences importantes dans la manière de lire et d’écrire les données ainsi que dans les limites de la liaison des paramètres.
Quand une union est-elle appropriée ?
Une union convient aux contrats qui acceptent des alternatives indépendantes sans nécessiter d’enveloppe JSON ni de champ discriminator. Il peut s’agir, par exemple, d’une valeur pouvant être un entier ou une chaîne, ou d’une configuration acceptant une valeur booléenne ou un objet contenant des options plus détaillées. Lors de la sérialisation, l’état actif est écrit directement dans son format naturel : aucune enveloppe propre au type union ni aucun discriminator n’est ajouté.
Les états pouvant être distingués clairement à partir de la structure JSON sont sérialisés automatiquement. Toutefois, si les états partagent la même structure, comme dans une union comprenant les objets Cat et Dog, System.Text.Json ne peut pas déterminer le type à partir du seul jeton JSON. Dans ce cas, il est possible de fournir un JsonTypeClassifier personnalisé pour sélectionner le bon état. Il en va de même pour certains contrats de requête qui acceptent un nombre ou une chaîne, car les paramètres JSON propres au web permettent de lire les nombres depuis des chaînes, ce qui peut rendre le texte interprétable comme l’un ou l’autre des deux états.
Les hiérarchies fermées diffèrent des unions
L’utilisation du modificateur closed n’implique pas que la sérialisation polymorphe sera activée automatiquement. Lorsqu’il traite un type dérivé concret tel que PaymentAuthorized, System.Text.Json le traite comme n’importe quel autre type et produit un JSON contenant paymentId et amount, sans discriminator.
En revanche, lors de l’utilisation du type de base PaymentEvent dans une API, il est nécessaire de spécifier le type dérivé qui doit être créé. Cela peut être activé sur le type de base avec JsonPolymorphic(InferClosedTypePolymorphism = true), ou via JsonSerializerOptions.InferClosedTypePolymorphism. System.Text.Json déduit alors les types dérivés dans la sérialisation fermée et utilise leurs noms comme identifiants, par exemple $type avec la valeur PaymentAuthorized.
Le choix entre les deux modèles dépend de la nature du contrat : une union représente des alternatives qui n’appartiennent pas nécessairement à une même famille, tandis que les hiérarchies fermées conviennent à un ensemble lié d’événements ou d’entités nécessitant un discriminator explicite.
Prise en charge dans les couches d’ASP.NET Core
Dans les Minimal APIs, une union peut être utilisée comme paramètre du corps de la requête ou comme type de retour, que ce soit dans le chemin d’exécution habituel ou via le Request Delegate Generator, avec un comportement identique entre les deux chemins. Une union peut également apparaître dans un autre modèle, comme élément d’un IAsyncEnumerable<T> ou dans un conteneur utilisant AsParameters, et les paramètres de ConfigureHttpJsonOptions continuent d’influencer la sérialisation.
Les contrôleurs MVC prennent en charge les unions comme paramètres d’action et comme types de retour, notamment Task<TUnion> et ValueTask<TUnion>. Comme MVC utilise les paramètres JsonSerializerDefaults.Web, le problème d’ambiguïté entre un nombre et une chaîne dans les entrées HTTP s’applique également à MVC.
Dans SignalR, la prise en charge fonctionne via JsonHubProtocol pour les paramètres, les valeurs de retour et les éléments des flux. Une union de type int ou string ne nécessite pas de classifier avec ce protocole, car JsonHubProtocol ne considère pas le jeton JSON de type chaîne comme ambigu dans ce cas. Toutefois, les unions dont les états partagent le jeton StartObject, comme UnionPet(Cat, Dog), nécessitent toujours un classifier. SignalR ne prend pas en charge cette fonctionnalité avec les protocoles hub MessagePack ou Newtonsoft.Json.
Blazor et OpenAPI
Les paramètres des composants Blazor intra-processus sont transmis par affectation directe et ne nécessitent donc pas de sérialisation. En revanche, l’interopérabilité JavaScript, l’état persistant des composants et les paramètres lors du prerendering passent par System.Text.Json et suivent donc les mêmes règles que les unions. Il est par exemple possible de transmettre une union à JavaScript sous la forme d’un Boolean ou d’un objet d’options, selon l’état actif, sans ajouter d’enveloppe ni de discriminator.
Dans OpenAPI, une union est représentée à l’aide d’un schéma anyOf comprenant un type pour chaque état. Les états réutilisent les mêmes noms de composants Cat et Dog lorsqu’ils apparaissent comme types indépendants. Cela diffère des types polymorphes qui possèdent un discriminator et dont les schémas sont exposés sous des noms distinctifs. ApiExplorer peut également représenter plusieurs types de réponse pour le même code d’état et le même type de contenu au sein de anyOf.
Limites à prendre en compte
La prise en charge des unions repose sur System.Text.Json et ne fonctionne donc pas avec les sources de liaison qui ne passent pas par l’analyse JSON. Cela inclut les valeurs des chaînes de requête, les valeurs des chemins, les en-têtes et les champs de formulaire. Une valeur telle que ?id=42 ne suffit pas à déterminer si le type visé est int, string ou Guid. Dans Blazor, cette limite s’applique à [SupplyParameterFromQuery] et à la liaison depuis les formulaires avec [SupplyParameterFromForm]. La source indique que la liaison des unions depuis ces sources fait encore l’objet d’une étude et d’une collecte de retours.
Lecture éditoriale : la valeur essentielle ici ne réside pas dans l’ajout d’un nouveau format JSON, mais dans l’unification de la gestion des contrats à plusieurs états à travers une grande partie de l’écosystème .NET 11. Toutefois, la réussite de la conception dépend de la possibilité de distinguer les états et de la source des données ; il convient donc de définir rapidement un classifier ou un discriminator et de ne pas supposer que la prise en charge de la sérialisation implique la prise en charge de toutes les méthodes de liaison des paramètres.