C# – Protéger le code avec de nouvelles fonctionnalités

14 mai 2026

C# – Protéger le code avec de nouvelles fonctionnalités

Le C# a évolué de manière constante (notamment après le C# 8 et le 10), où le système de types et le compilateur prennent en charge la garantie que l’état d’un objet est valide, plutôt que de s’appuyer uniquement sur de multiples if (x == null) disséminés dans le code.

Aujourd’hui je vais présenter certains de ces outils en montrant comment ils vous aident à protéger votre code de manière simple.

1- Membres obligatoires (Required Members)

Pendant longtemps, le C# ne disposait pas d’un moyen fiable pour dire « cette propriété doit être définie ». Les constructeurs aidaient, mais dès que les initialisations d’objets sont devenues courantes, il était facile de créer des objets invalides par inadvertance.

var utilisateur = new Utilisateur
{
    Nom = "Marie"
    // Email oublié
};

Le compilateur était parfaitement satisfait. L’exécution, elle, l’était moins. Le modificateur required corrige cela au niveau du langage.

public class Utilisateur
{
    public required string Nom { get; init; }
    public required string Email { get; init; }
}

Désormais, ce code échouera lors de la compilation :

var utilisateur = new Utilisateur
{
    Nom = "Marie"
};

Ce n’est pas seulement une sucette syntaxique. Il change fondamentalement la façon dont vous modélisez les invariants. Plutôt que d’attendre que l’appelant se rappelle ce qui est important, vous encodez cela directement dans le système de types.

Si vous avez déjà ajouté des vérifications de nullité en fond de votre code parce que « quelqu’un pourrait oublier de définir cela », les membres required sont la correction que vous recherchiez vraiment.

2- Propriétés à initialisation unique (Init-Only Setters)

Les objets mutables présentent un intérêt pratique, mais ils peuvent aussi être à l’origine de dégradations silencieuses des règles métier. Le init se situe à un juste milieu : il permet de définir les valeurs lors de la construction, mais interdit toute modification ultérieure.

public class Commande
{
    public Guid Id { get; init; }
    public DateTime CreeLe { get; init; }
}

Ce code fonctionne :

var commande = new Commande
{
    Id = Guid.NewGuid(),
    CreeLe = DateTime.UtcNow
};

En revanche, ce code ne fonctionne pas :

commande.CreeLe = DateTime.UtcNow.AddDays(-1);

La propriété ne peut recevoir une valeur que lors du constructeur de la classe ou via un initialiseur d’objet. Une fois l’objet terminé et l’exécution quittant les accolades ({…}), la propriété devient en lecture seule.

Maintenant, combiné avec required, cela devient extrêmement puissant. Vous pouvez définir des objets qui doivent être entièrement initialisés, correctement, exactement une fois. Cela simplifie radicalement le raisonnement sur l’état, surtout dans les systèmes plus vastes où les objets circulent entre couches.

Exemple :

public class Commande
{
    // Le compilateur oblige l’attribution et l’init empêche toute modification ultérieure
    public required Guid Id { get; init; }    
     public required DateTime CreeLe { get; init; }
    // Propriété optionnelle : peut être définie lors de la création, mais jamais modifiée ensuite
    public string? Observation { get; init; }
}

 

La combinaison required + init résout le problème des constructeurs interminables.

Autrefois, pour garantir qu’un objet soit valide et immuable, il fallait un constructeur à dix paramètres. Avec cette combinaison :

Lisibilité : vous utilisez la syntaxe d’initialisation d’objet ({ Prop = Valeur }), bien plus claire qu’une longue liste d’arguments dans le constructeur.

Sécurité (Fail-fast) : l’erreur se produit à la compilation, pas à l’exécution. Vous éliminez le risque de NullReferenceException pour oubli de remplissage.

Contrat clair : celui qui consomme votre classe sait exactement ce qui est indispensable pour que l’objet existe de manière intègre.

Pourquoi init n’est-il pas identique à private set ?

À première vue, init peut sembler une syntaxe plus jolie pour private set. Tous deux semblent empêcher la mutation externe, mais ils résolvent des problèmes différents à des moments différents de la vie d’un objet.

Avec private set, la propriété est toujours mutable à l’intérieur de la classe, à tout moment.

public class Commande
{
    public DateTime CreeLe { get; private set; }
    public void Recalculer()
    {
        CreeLe = DateTime.UtcNow; // tout à fait légal
    }
}

Cela signifie que la valeur peut changer bien après la construction, potentiellement en réponse à une logique non reliée. Le compilateur ne vous aide pas ici. Si cette mutation est valide ou non dépend uniquement de votre discipline.

Le init bouleverse les règles. Il garantit que l’objet ne devient mutable que pendant sa « montage ». Une fois l’objet prêt, il se comporte comme un objet immuable pour ces propriétés.

public class Commande
{
     public DateTime CreeLe { get; init; }
}

3- ConfigureAwaitOptions

Le ConfigureAwaitOptions est une fonctionnalité introduite avec .NET 8 pour donner plus de souplesse et de clarté dans le contrôle des contextes asynchrones.

Cela existe depuis des années, mais c’était autrefois un outil brut. Soit vous capturiez le contexte, soit non. Avec .NET 8, le ConfigureAwaitOptions rend l’intention explicite.

Autrefois, vous aviez juste la méthode ConfigureAwait(bool continueOnCapturedContext). Si vous passiez false, vous évitiez de revenir dans le contexte d’origine (utile pour éviter les deadlocks dans les UI et améliorer les performances dans les bibliothèques). Toutefois, .NET 8 a transformé cela en une Enum avec des indicateurs (Flags), permettant de combiner différents comportements.

await SomeAsyncWork().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

Cette option modifie la façon dont les exceptions remontent via await, permettant un traitement plus sûr dans certains scénarios avancés, notamment dans les codes d’infrastructure ou les frameworks.

Plus important encore, l’existence d’options force à réfléchir au pourquoi de la configuration des awaits. Cherchez-vous à éviter les deadlocks ? Êtes-vous en train d’écrire du code de bibliothèque ? Supprimez-vous intentionnellement le comportement du contexte ?

Imaginez que vous écrivez un code d’infrastructure où vous souhaitez éviter le contexte d’origine et aussi forcer l’exécution asynchrone pour ne pas bloquer le thread principal :

using System.Threading.Tasks;
public async Task ProcesserDonneesAsynchrones()
{
    // Combinaison d’options: ne capture pas le contexte ET force l’exécution asynchrone
    await Task.Delay(100).ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext | 
                                         ConfigureAwaitOptions.ForceAsync);
    // Code qui s’exécute après l’await

Pourquoi utiliser cette fonctionnalité plutôt que ConfigureAwait(false) ?

L’ancien ConfigureAwait(false) était ce que l’on appelle un outil « aveugle ». Il indiquait simplement : « je me fiche du contexte ». En revanche, avec les ConfigureAwaitOptions, le code devient auto-explicatif. Si vous utilisez ConfigureAwaitOptions.SuppressThrowing, tout développeur qui lit votre code comprendra immédiatement que vous traitez les exceptions de manière particulière à cet endroit, ce que ne communiquerait pas un simple false.

4- CallerArgumentExpression

Le CallerArgumentExpression est un attribut introduit dans le C# 10 qui permet au compilateur de capturer le texte littéral (le code source) d’un argument passé à une méthode.

En termes simples : il permet à votre code de savoir exactement ce que le développeur a écrit entre les parenthèses lors de l’appel d’une fonction.

L’usage principal réside dans la validation et les diagnostics. Avant cette fonctionnalité, si vous vouliez lancer une exception détaillée, vous deviez répéter manuellement le nom de la variable ou utiliser la réflexion/nameof, ce qui était limité.

Ainsi, pendant des années, la validation des arguments signifiait répéter les noms des paramètres ou écrire des messages d’erreur vagues.

if (utilisateur == null)
       throw
 new ArgumentNullException(nameof(utilisateur));

Cela fonctionne, mais échoue rapidement sur des expressions plus complexes.

Valider(valeur);

Qu’est-ce qui a échoué ? Quelle valeur était invalide ? Le CallerArgumentExpression résout ce problème en laissant le compilateur dire quelle expression a été passée.

exemple :

void Valider(
    string valeur,
    [CallerArgumentExpression(nameof(valeur))] string? expression = null)
{
    if (string.IsNullOrWhiteSpace(valeur))
        throw new ArgumentException($"Valeur invalide: {expression}");
}

Maintenant ce code :

Valider(utilisateur.Email);

Produit un message significatif (« Valeur invalide : utilisateur.Email ») sans effort supplémentaire. Cette fonctionnalité est petite, mais son impact est réel. Elle rend les clauses de garde, les assistants de validation et les assertions de débogage dramatiquement plus utiles sans sacrifier la lisibilité.

5- Attributs de nulabilité et Analyse de flux

Le C# moderne tente de nous aider à éviter le fameux NullReferenceException. Cependant, le compilateur n’est pas « voyant »; il analyse le flux de données.

Quand vous avez une méthode qui retourne un booléen pour indiquer le succès et qui utilise un paramètre out pour renvoyer l’objet, le compilateur devient confus sans aide supplémentaire

Les types de référence nullable sont aussi bons que les informations que vous donnez au compilateur. Par défaut, le compilateur est prudent. Les attributs de nulabilité servent à lui enseigner votre intention.

Imaginez la méthode sans l’attribut :

// Sans l’attribut NotNullWhen
bool EssayerObtenirUtilisateur(int id, out Utilisateur? utilisateur)
{
     // si trouvé, renseigne utilisateur et retourne true
     // si non trouvé, utilisateur est null et retourne false
}

Quand vous utilisez cette méthode, le compilateur pense : “La variable utilisateur peut être nulle, car le type est Utilisateur?”. Même si la méthode retourne true, je ne suis pas sûr que le développeur ait effectivement rempli la variable.

Pour cette raison, il émet un Avertissement si vous tentez d’utiliser l’utilisateur juste après le if, vous obligeant à faire un if (utilisateur != null) inutile.

L’attribut [NotNullWhen(true)] est une directive explicite pour le moteur d’analyse du C#. Vous dites au compilateur :

« Hé, compilateur, je garantis que si cette méthode retourne true, le paramètre utilisateur ne sera pas nul, même si son type autorise les valeurs nulles. »

Pourquoi ceci est-il important ?

Élimine les avertissements inutiles : le compilateur cesse de se plaindre d’un point que vous connaissez sûr.
Intelligence dans le flux : si la méthode retourne false, le compilateur continuera à vous avertir que l’utilisateur peut être nul, ce qui est correct !
Contrat clair : celui qui consomme votre code (ou votre bibliothèque) sait exactement comment gérer le retour sans avoir à lire tout le code interne.

Exemple de comparaison :

Sans l’attribut :

if (EssayerObtenirUtilisateur(1, out var utilisateur))
{
// Le compilateur affiche un Avertissement ici :
    // “Possível desreferência de uma referência nula”
utilisateur.FaireQuelqueChose();
}

Avec l’attribut :

if (EssayerObtenirUtilisateur(1, out var utilisateur))
{
    // Le compilateur affiche ici : 
    // "Possível desreferência de uma referência nula"
    utilisateur.FaireQuelqueChose(); 
}

Sans l’attribut, vous obtenez soit des avertissements, soit vous ajoutez des vérifications défensives qui n’apportent pas une valeur réelle. Ils sont particulièrement précieux dans les API publiques et les bibliothèques partagées, où les suppositions se répandent rapidement et silencieusement.

Conclusion

Ce qui unit ces outils n’est pas uniquement une syntaxe moderne ou une évolution de la langue en elle-même. Il s’agit d’un changement de philosophie clair. Aucun de ces outils ne rend votre code « intelligent » en soi. Ils le rendent toutefois plus difficile à mal utiliser, plus facile à interpréter et plus simple à maintenir sous pression.

Et c’est généralement la différence entre un code qui paraît propre aujourd’hui et un code qui restera solide dans un an.

Fabien Delpont

Auteur

Fabien Delpont

Fabien Delpont, développeur et créateur du site Python Doctor.