Introduction

Dans certaines grandes entreprises, où de multiples services et équipes interagissent via des API, le problème de la rupture des contrats d'API en production est particulièrement problématique. Avez-vous déjà été confronté à l'urgence de revoir l'intégration des API que vous consommiez parce que les contrats sur lesquels vous comptiez ont été modifiés sans avertissement adéquat ?

Cette situation, pas si inhabituelle dans les environnements complexes où diverses équipes gèrent différents services, oblige souvent les développeurs à abandonner le développement de fonctionnalités métier essentielles pour corriger en urgence des changements inattendus.

Cet article ne se propose pas d'explorer les différentes stratégies de versioning des contrats d'API, mais plutôt de présenter une méthode spécifique et un outil conçu pour prévenir les modifications accidentelles de ces contrats.

Nous aborderons la manière dont une approche de sérialisation des données en .NET, en association avec l'utilisation d'un outil spécifique - la librairie JsonEnumValueBinding - peut jouer un rôle clé dans le maintien de l'intégrité des contrats d'API en évitant notamment les erreurs involontaires.

Sérialisation Basée sur la Convention de Nommage

L'une des stratégies les plus répandues pour définir les contrats d'API en .NET consiste à se baser sur les noms des propriétés des Data Transfer Objects (DTO). Dans cette approche les noms des propriétés du DTO déterminent directement la structure du JSON sérialisé. Par exemple, considérons le DTO suivant en C# :

public class User {
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public Gender Gender { get; set; }
}

Lorsqu'un objet de ce type est sérialisé, il produit un JSON qui reflète fidèlement la structure et les noms des propriétés du DTO :

{
    "FirstName": "John",
    "LastName": "Doe",
    "Age": 30,
    "Gender": 3
}

Cette méthode est intuitive et facile à mettre en œuvre. Cependant, elle peut avoir des limitations, comme l'impossibilité de se conformer à des règles de nommage particulières à l'entreprise. Elle peut également entraîner des problèmes de versioning des contrats d'API si les noms des propriétés sont modifiés dans le DTO - cela changerait la structure du JSON sans avertissement préalable pour les consommateurs de l'API.

Refactoring

Le refactoring, et en particulier le soin apporté au naming, joue un rôle crucial dans le développement logiciel, quelle que soit l'approche ou le langage utilisé. Des noms de variables, de méthodes et de classes bien choisis rendent le code plus lisible, compréhensible et maintenable. Le refactoring régulier pour améliorer le naming est essentiel - règle du boy-scout - car il aide à garder le code aligné avec son but et son utilisation actuelle.

La sérialisation par convention de nommage peut involontairement introduire des risques. Une simple modification de nom dans le cadre d'un refactoring peut, sans le vouloir, rompre le contrat d'une API. Ce genre d'erreur, facile à commettre, peut avoir des répercussions immédiates et étendues, brisant la communication entre les services et nécessitant des corrections en urgence.

Serialisation Basée sur les Attributs

L'utilisation des attributs pour la sérialisation en .NET, bien qu'elle ajoute une couche de code supplémentaire, apporte un contrôle précis sur la représentation des données dans les contrats d'API. Cette méthode permet de déterminer explicitement comment les propriétés d'un objet sont sérialisées, indépendamment des noms des propriétés dans le code, ce qui est particulièrement utile lors du refactoring.

Dans le cadre des contrats d'API, l'avantage de la sérialisation basée sur les attributs est indéniable. Elle offre une stabilité et une prévisibilité accrues, assurant que les changements internes au code ne vont pas altérer involontairement la structure des données exposées par l'API.

Voici le même exemple de code que précédemment mais utilisant les attributs:

using System.Text.Json.Serialization;

public class User {

    [JsonPropertyName("first_name")]
    public string FirstName { get; set; }

    [JsonPropertyName("last_name")]
    public string LastName { get; set; }

    [JsonPropertyName("age")]
    public int Age { get; set; }

    [JsonPropertyName("gender")]
    public Gender Gender { get; set; }
}

La sérialisation de cet objet en JSON produirait la sortie suivante :

{
    "first_name": "John",
    "last_name": "Doe",
    "age": 30,
    "gender": 3
}

Considérons un scénario où une erreur typographique se glisse dans la sérialisation du DTO User. Imaginons que l'erreur s'est glissée dans l'attribut de sérialisation de la propriété FirstName :

using System.Text.Json.Serialization;

public class User {

    [JsonPropertyName("first_nale")]
    public string FirstName { get; set; }

}

Si cette API est publiée et utilisée pendant plusieurs mois, corriger directement l'erreur typographique dans l'attribut pourrait casser le contrat pour les consommateurs de l'API qui se sont adaptés à cette faute de frappe. Une meilleure approche consisterait à renommer la propriété FirstName en quelque chose comme FirstName_typo_error pour expliciter l'erreur aux développeurs de l'API, tout en conservant son attribut sérialisé inchangé. Ensuite, ajouter une nouvelle propriété FirstName correctement attribuée garantit la continuité du contrat.

Code C# modifié pour corriger l'erreur :

using System.Text.Json.Serialization;

public class User {

    [Obsolete("Utiliser FirstName. Cette propriété est conservée pour compatibilité.")]
    [JsonPropertyName("first_nale")]
    public string FirstName_typo_error { get; set; }

    [JsonPropertyName("first_name")]
    public string FirstName { get; set; }
}

Cette méthode permet de corriger l'erreur tout en préservant la compatibilité avec les versions précédentes de l'API. De plus, le marquage de l'ancienne propriété comme obsolète avec l'attribut [Obsolete] fournit une indication claire aux développeurs sur l'erreur et la transition vers la nouvelle propriété correcte.

De la Sérialisation des Enums

En .NET, la sérialisation des énumérations se fait par défaut en utilisant leur valeur numérique (int). Bien que cela puisse sembler pratique et efficace en termes de taille de données, ce n'est généralement pas considéré comme une bonne pratique, surtout dans le contexte des contrats d'API. La raison principale est que la sérialisation en valeurs numériques manque de clarté et de lisibilité pour les consommateurs de l'API. Les valeurs numériques ne communiquent pas le sens ou le contexte des données comme le feraient les noms des énumérations, rendant ainsi le contrat d'API moins intuitif et plus difficile à comprendre.

Pour remédier à cela, il est possible de sérialiser les énumérations en tant que chaînes de caractères. Cela peut être accompli en .NET avec des configurations spécifiques du sérialiseur. Par exemple, avec System.Text.Json, vous pouvez ajouter JsonStringEnumConverter à vos options de sérialisation :

services.AddControllers().AddJsonOptions(options => {
    options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

Cette modification garantit que toutes les énumérations dans votre application seront sérialisées par leurs noms, rendant le JSON généré plus lisible et compréhensible. Cependant, cette approche vous lie de nouveau aux conventions de nommage des énumérations. Si vous modifiez le nom d'un membre de l'énumération, cela change également la représentation JSON, ce qui peut potentiellement casser le contrat d'API si les clients ne sont pas mis à jour en conséquence.

Sérialisation des Enums en String

Une autre approche efficace pour gérer la sérialisation des énumérations dans les contrats d'API consiste à représenter les valeurs d'énumération en tant que chaînes dans le DTO, et à utiliser un mapper pour la correspondance entre les énumérations et ces chaînes. Cela permet de maintenir la stabilité du contrat d'API indépendamment des changements de noms dans l'énumération.

Supposons que nous ayons l'énumération Status :

public enum Status {
    InProgress,
    Completed
}

Au lieu de sérialiser cette énumération directement, nous représentons ses valeurs dans le DTO en utilisant des constantes string définies dans un mapper :

public static class StatusMapper {

    public const string InProgress = "in_progress";
    public const string Completed = "completed";

    public static string Map(Status input) {
        return switch {
            input.InProgress => InProgress,
            input.Completed => Completed,
            _ => throw new ArgumentOutOfRangeException()
        };
    }

    public static Status Map(string input) {
        if (input == null) { throw new ArgumentNullException(); }

         return switch {
            InProgress => input.InProgress,
            Completed => input.Completed,
            _ => throw new ArgumentOutOfRangeException()
        };
    }
}

Avec cette méthode, les changements dans les noms des membres de l'énumération Status n'affectent pas la sérialisation, assurant ainsi la stabilité du contrat d'API. Le recours au switch dans le mapper facilite également la maintenance et la mise à jour des mappages.

NOTE

En .NET, cette approche de représentation des énumérations pose un problème pour la documentation Swagger : elle ne peut plus documenter automatiquement les valeurs possibles des énumérations, nécessitant ainsi des ajustements manuels dans la documentation.

Librairie JsonEnumValueBinding

Pour résoudre le problème de documentation Swagger avec les énumérations en .NET, la librairie JsonEnumValueBinding apporte une solution élégante. Elle propose un attribut JsonEnumValueAttribute, qui fonctionne pour les valeurs d'énumération de la même manière que JsonPropertyName le fait pour les noms de propriétés.

Cet attribut permet de définir des valeurs de chaîne personnalisées pour les membres d'énumération, garantissant ainsi une sérialisation et une désérialisation cohérentes, tout en étant parfaitement géré par Swagger pour la documentation automatique.

Utilisation de la librairie JsonEnumValueBinding

Pour commencer à utiliser JsonEnumValueBinding dans votre projet .NET, suivez ces étapes simples :

  1. Installation : Commencez par installer le package NuGet Reefact.JsonEnumValueBinding. Cela peut être fait via la ligne de commande avec :
   dotnet add package Reefact.JsonEnumValueBinding
  1. Activation : Ensuite, activez la fonctionnalité dans votre fichier Startup.cs. Ajoutez la méthode AddJsonEnumValueBinding dans la configuration des services :
   public class Startup {
       public void ConfigureServices(IServiceCollection services) {
           services.AddControllers()
                   .AddJsonEnumValueBinding(); // Activation du binding
           // Autres configurations...
       }
   }
  1. Application de l'attribut : Appliquez l'attribut JsonEnumValueAttribute sur les valeurs de votre énumération pour personnaliser leur sérialisation. Par exemple :
   public enum ProductStatus {
       [JsonEnumValue("available")] Available,
       [JsonEnumValue("out_of_stock")] OutOfStock,
       [JsonEnumValue("discontinued")] Discontinued
   }

Ces étapes simples permettent d'utiliser JsonEnumValueBinding pour personnaliser la sérialisation des valeurs d'énumération, offrant ainsi une plus grande flexibilité et précision dans la gestion des contrats d'API.

Ainsi, l'exemple ci-dessous :

using Microsoft.AspNetCore.Mvc;

public class Product {
    public [JsonPropertyName("name")] string Name { get; set; }
    public [JsonPropertyName("status")] string Status { get; set; }
}

[ApiController]
[Route("api/products")]
public sealed class ProductsController : ControllerBase {

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id) {
        var product = new Product {
            Name = "Widget",
            Status = ProductStatus.OutOfStock
        };

        return Ok(product); 
    }
}

produit le résultat suivant :

{
    "name": "Widget",
    "status": "out_of_stock"
}

Le Binding est aussi supporté:

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase {

    private readonly List<Product> _products = new List<Product> {
        new Product { Name = "Widget 1", Status = ProductStatus.OutOfStock },
        new Product { Name = "Widget 2", Status = ProductStatus.Available },
        new Product { Name = "Widget 3", Status = ProductStatus.OutOfStock }
    };

    [HttpGet("{status}")]
    public IActionResult GetProductsByStatus([FromRoute] ProductStatus status) {
        var productsByStatus = _products.Where(p => p.Status == status).ToList();

        return Ok(productsByStatus);
    }
}

L'appel suivant :

curl http://localhost:5000/api/products/out_of_stock

produit ce résultat :

[
    {
        "name": "Widget 1",
        "status": "out_of_stock"
    },
    {
        "name": "Widget 3",
        "status": "out_of_stock"
    }
]

Conclusion

La gestion de la sérialisation des énumérations dans les contrats d'API en .NET est souvent négligée, mais elle joue un rôle essentiel dans la clarté et la stabilité des interfaces de programmation. Les défis associés à cette tâche sont liés à la préservation de contrats stables.

L'utilisation des attributs dans le cadre de la sérialisation des contrats d'API représente une approche judicieuse. Elle permet d'éviter les problèmes liés aux conventions de nommage et offre une solution robuste pour garantir la clarté et la stabilité des contrats.

La sérialisation des énumérations en .NET, notamment pour obtenir des valeurs textuelles pour une API conviviale, présente des défis. Les valeurs par défaut en int ou la sérialisation en string basée sur les conventions de nommage ne sont pas idéales pour la clarté des contrats.

La librairie JsonEnumValueBinding offre une solution qui permet de maintenir une approche traditionnelle des énumérations en .NET tout en offrant la flexibilité des JsonPropertyName et une intégration transparente avec Swagger. Elle améliore ainsi la gestion des contrats d'API pour une meilleure compréhension et utilisation dans divers contextes de développement.

Références