Elvis-Zugriffsmodifikator in C#: So implementieren Sie kontrollierten Zugriff auf private Member ohne die Kapselung zu verletzen
Der Elvis-Zugriffsmodifikator ist ein benutzerdefiniertes Attributsystem und ein Roslyn-Analyzer für C#, das deklarativ einen kontrollierten Zugriff auf private und internal Klassenmitglieder durch streng begrenzte ‚freundliche‘ Typen ermöglicht. Im Gegensatz zu C++ friend erfordert Elvis keine Änderung der Zielklasse, wenn neue vertrauenswürdige Verbraucher hinzugefügt werden, und bietet eine strenge Kompilierzeitvalidierung. Diese Lösung richtet sich an mittlere und senior Entwickler, die an hochbelasteten, multithreaded oder frameworkabhängigen Projekten arbeiten, bei denen die Prinzipien von immutable-by-default und lokalisierte Mutabilität für die Korrektheit entscheidend sind.
Wie Elvis funktioniert: Architektur und Hauptkomponenten
Elvis besteht aus drei miteinander verbundenen Schichten:
- Kompilierzeitattribute —
[Elvis],[OnlyType],[OnlyNs],[ExcludeType],[ExcludeNs]. Diese annotieren Mitglieder (Eigenschaften, Methoden, Felder) und legen fest, welche Typen oder Namespaces Zugriff haben dürfen. - Roslyn-Analyzer — führt semantische Prüfungen in der IDE und während der Kompilierung durch: Er analysiert Aufrufe, bestimmt den Kontext des Zugriffs (‚Attractor‘), vergleicht ihn mit den Attributregeln und gibt Diagnosemeldungen aus, wenn Verstöße auftreten.
- VSIX-Erweiterung und NuGet-Paket — sorgt für die Integration in den Entwicklungsworkflow: Autovervollständigung von Attributen, schnelle Anwendung von Vorlagen und Visualisierung der zulässigen Beziehungen im Entscheidungsbaum.
Wichtig ist, dass Elvis weder IL-Code modifiziert, noch Laufzeitreflexion verwendet oder auf InternalsVisibleTo angewiesen ist. Alle Validierungen erfolgen statisch – während der Kompilierung. Dies garantiert, dass verbotener Zugriff selbst in Debug-Builds nicht durchschlüpft.
Anwendungsfälle: Von einfachen bis zu fortgeschrittenen Szenarien
Betrachten wir eine einfache Aufgabe: Die Klasse Me enthält private decimal Money, soll aber einer anderen Klasse, MyFriend, erlauben, ihren Wert sicher zu ändern, ohne Zugriff auf den gesamten Code in der Assembly zu gewähren.
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); // ✅ Erlaubt: MyFriend ist in [Elvis] angegeben
// me._money = 0; // ❌ Kompilierzeitfehler: direkter Zugriff auf privates Feld ist verboten
}
}
Neue Funktionen, die in den Versionen 2026-03 und 2026-04 hinzugefügt wurden, umfassen:
- Typen über String-Namen:
[Elvis("MyNamespace.MyFriend")]—nützlich für assemblierungsübergreifende Verbindungen, wenn ein Typ nicht referenziert werden kann. - Reguläre Ausdrücke:
[Elvis(@"^MyNamespace\.Tests\.*$")]—ermöglicht Zugriff auf alle Testklassen innerhalb eines Namespaces. - Ausschlussattribute:
[ExcludeType(typeof(ObsoleteHelper))]—blockiert einen bestimmten Typ, selbst wenn er unter die allgemeine[OnlyNs]-Regel fällt. - Namespaces:
[OnlyNs("MyCompany.Core")]—beschränkt den Zugriff nur auf Typen aus dem angegebenen Namespace.
Kombinatorik und Verhalten in Hierarchien
Elvis unterstützt zusammengesetzte Regeln. Wenn mehrere Attribute auf ein einzelnes Mitglied angewendet werden, gilt standardmäßig AND-Logik, doch Ausnahmen haben Vorrang:
- Wenn sowohl
[OnlyNs("A")]als auch[ExcludeNs("A.Sub")]angegeben sind, wird Zugriff aufA.gewährt, außerA.Sub.. - Bei Vererbung: Wenn die Basisklasse mit
[Elvis(typeof(BaseFriend))]markiert ist und die abgeleitete Klasse mit[Elvis(typeof(DerivedFriend))]markiert ist, erhalten beide Typen Zugriff auf die Mitglieder der Basisklasse. Allerdings erbt die abgeleitete Klasse nicht die Rechte auf ihre eigenenprivateMitglieder – diese müssen separat annotiert werden. - Bei überladenen Methoden gelten Attribute jeweils einzeln für jede Signatur.
Bemerkenswert ist, dass Elvis protected Mitglieder nicht beeinflusst – sie bleiben gemäß den üblichen Vererbungsregeln zugänglich. Attribute wirken nur auf private und internal Mitglieder.
Worauf es ankommt
- Elvis ist kein Ersatz für DI oder Interfaces; es ist eine zusätzliche Ebene der Kapselungskontrolle, wo Interfaces unpraktisch sind (z. B. beim Arbeiten mit DTOs, Konfigurationsobjekten oder internen Framework-Mechanismen).
- Alle Regeln werden statisch geprüft – null Laufzeitoverhead, volle Kompatibilität mit AOT-Kompilierung und Trimmen.
- Unterstützung für reguläre Ausdrücke und typbasierte String-Namen macht die Lösung in Szenarien anwendbar, in denen Typen zur Kompilierzeit nicht verfügbar sind (Plugin-Architekturen, Codegenerierung).
- Ausschlussattribute (
[Exclude*]) ermöglichen das Erstellen von „Whitelists mit Ausnahmen“, was bei der Migration großer Codebasen entscheidend ist. - Elvis verletzt SOLID nicht: Es schwächt DIP nicht, sondern ergänzt es und ermöglicht die Formalisierung von Verträgen zwischen Komponenten, ohne künstliche Wrapper-Interfaces zu schaffen.
Praktische Einschränkungen und Empfehlungen
Elvis löst nicht alle Zugriffsprobleme. Es sollte eingesetzt werden, wenn:
- Präziser, vorhersehbarer Zugriff auf
privateZustände benötigt wird, ohne sie überpublic/internalAPIs offenzulegen. - Die Architektur eine strikte Isolation von Verantwortungszonen erfordert (z. B. Framework-Core und seine Erweiterungen).
- Muster verwendet werden, bei denen Klassen logisch eng gekoppelt sind, aber aus kompilatorischer Sicht unabhängig bleiben müssen (z. B.
BuilderundBuilderResult).
Vermeiden Sie die Verwendung von Elvis, wenn:
- Refactoring schlecht entworfener Abhängigkeiten erforderlich ist – wenn Sie für ein
privateFeld 15friend-Klassen brauchen, gehört die Logik wahrscheinlich in einen separaten Service. - CI/CD-Pipelines fehlen, sodass Analyser nicht in den Build-Prozess eingebunden sind – Regeln werden außerhalb der IDE ignoriert.
- Für
publicMitglieder – die Attribute ignorieren sie einfach, da der Zugriff bereits offen ist.
Wenn Sie in einem bestehenden Projekt implementieren, beginnen Sie mit private Feldern in DTO-Klassen und internen Fabriken, wo das Risiko, Invarianten zu verletzen, am höchsten ist und die Kosten für Refactoring am niedrigsten sind.
— Editorial Team
Noch keine Kommentare.