Modificateur d'accès Elvis en C# : comment mettre en œuvre un accès contrôlé aux membres privés sans compromettre l'encapsulation
Elvis est un système d'attributs personnalisés et un analyseur Roslyn pour C# qui permet, de manière déclarative, un accès contrôlé aux membres privés et internes des classes par des types 'amis' strictement limités. Contrairement à friend en C++, Elvis ne nécessite pas de modifier la classe cible lorsqu'on ajoute de nouveaux consommateurs de confiance et assure une validation stricte au moment de la compilation. Cette solution s'adresse aux développeurs de niveau intermédiaire ou senior travaillant sur des projets à forte charge, multithreadés ou dépendants de frameworks, où les principes d'immuabilité par défaut et de mutabilité localisée sont essentiels pour garantir la correcte exécution.
Comment fonctionne Elvis : architecture et composants clés
Elvis se compose de trois couches interconnectées :
- Attributs au moment de la compilation —
[Elvis],[OnlyType],[OnlyNs],[ExcludeType],[ExcludeNs]. Ces attributs annotent les membres (propriétés, méthodes, champs) en spécifiant quels types ou espaces de noms sont autorisés à y accéder. - Analyseur Roslyn — effectue des vérifications sémantiques dans l'IDE et pendant la compilation : il analyse les appels, détermine le contexte d'accès ('attracteur'), le compare aux règles des attributs et émet des messages de diagnostic en cas de violation.
- Extension VSIX et package NuGet — assure l'intégration dans le flux de travail de développement : complétion automatique des attributs, application rapide des modèles et visualisation des relations autorisées dans l'arbre décisionnel.
Il est important de noter qu'Elvis ne modifie pas le code IL, n'utilise pas la réflexion à l'exécution ni ne repose sur InternalsVisibleTo. Toute validation se fait statiquement — lors de la compilation. Cela garantit que les accès interdits ne passeront pas inaperçus, même dans les builds de débogage.
Cas d'utilisation : des scénarios basiques aux plus avancés
Prenons une tâche simple : la classe Me contient un champ private decimal Money, mais elle doit permettre à une autre classe, MyFriend, de modifier en toute sécurité sa valeur sans accorder un accès complet à l'ensemble du code de l'assembly.
class Me
{
[Elvis(typeof(MyFriend))]
private decimal _money = 100;
public decimal Money => _money;
public void SetMoney(decimal value) => _money = value;
}
class MyFriend
{
public void TakeHalf(Me me)
{
var half = me.Money / 2;
me.SetMoney(me.Money - half); // ✅ Autorisé : MyFriend est spécifié dans [Elvis]
// me._money = 0; // ❌ Erreur de compilation : l'accès direct au champ privé est interdit
}
}
Les nouvelles fonctionnalités ajoutées dans les versions 2026-03 et 2026-04 incluent :
- Types via des noms de chaîne :
[Elvis("MyNamespace.MyFriend")]— utile pour les connexions inter-assemblies lorsque la référence à un type n'est pas possible. - Expressions régulières :
[Elvis(@"^MyNamespace\.Tests\.*$")]— permet d'autoriser l'accès à toutes les classes de test au sein d'un espace de noms. - Attributs d'exclusion :
[ExcludeType(typeof(ObsoleteHelper))]— bloque un type spécifique même s'il relève de la règle générale[OnlyNs]. - Espaces de noms :
[OnlyNs("MyCompany.Core")]— restreint l'accès uniquement aux types provenant de l'espace de noms spécifié.
Combinatoire et comportement dans les hiérarchies
Elvis prend en charge les règles composites. Lorsque plusieurs attributs sont appliqués à un même membre, la logique AND est utilisée par défaut, mais les exceptions prennent la priorité :
- Si à la fois
[OnlyNs("A")]et[ExcludeNs("A.Sub")]sont spécifiés, l'accès est accordé àA.saufA.Sub.. - En héritage : si la classe de base est marquée
[Elvis(typeof(BaseFriend))]et que la classe dérivée est marquée[Elvis(typeof(DerivedFriend))], les deux types obtiennent l'accès aux membres de la classe de base. Toutefois, la classe dérivée ne hérite pas des droits sur ses propres membres privés — ceux-ci doivent être annotés séparément. - Pour les méthodes surchargées, les attributs s'appliquent individuellement à chaque signature.
Notamment, Elvis n'affecte pas les membres protected — ils restent accessibles selon les règles d'héritage standard. Les attributs ne fonctionnent qu'avec les membres private et internal.
Ce qui compte
- Elvis n'est pas un remplacement pour la DI ou les interfaces ; c'est une couche supplémentaire de contrôle de l'encapsulation là où les interfaces sont peu pratiques (par exemple, lorsqu'on travaille avec des DTO, des objets de configuration ou des mécanismes internes de framework).
- Toutes les règles sont vérifiées statiquement — zéro surcharge à l'exécution, compatibilité totale avec la compilation AOT et le trimming.
- La prise en charge des expressions régulières et des noms de types sous forme de chaîne rend la solution applicable dans des scénarios où les types ne sont pas disponibles au moment de la compilation (architectures de plugins, génération de code).
- Les attributs d'exclusion (
[Exclude*]) permettent de créer des « listes blanches avec exceptions », ce qui est crucial lors de la migration de vastes bases de code. - Elvis ne viole pas SOLID : il ne fragilise pas DIP mais le complète, permettant de formaliser les contrats entre les composants sans créer d'interfaces wrapper artificielles.
Limitations pratiques et recommandations
Elvis ne résout pas tous les problèmes d'accès. Il doit être utilisé lorsque :
- Un accès précis et prévisible à l'état
privateest nécessaire sans le rendre accessible via des APIpublic/internal. - L'architecture exige une isolation stricte des zones de responsabilité (par exemple, le cœur du framework et ses extensions).
- Des patterns sont utilisés où les classes sont logiquement étroitement couplées mais doivent rester indépendantes du point de vue de la compilation (par exemple,
BuilderetBuilderResult).
Évitez d'utiliser Elvis lorsque :
- Il est nécessaire de refactoriser des dépendances mal conçues — si vous avez besoin de 15 classes
friendpour un seul champprivate, la logique appartient probablement à un service séparé. - Les pipelines CI/CD sont absents, de sorte que les analyseurs ne sont pas inclus dans le processus de build — les règles seront ignorées en dehors de l'IDE.
- Pour les membres
public— les attributs les ignorent simplement puisque l'accès est déjà ouvert.
Lors de la mise en œuvre dans un projet existant, commencez par les champs private dans les classes DTO et les usines internes, où le risque de violer les invariants est le plus élevé et le coût de refactoring le plus faible.
— Editorial Team
Aucun commentaire pour le moment.