Powrót do strony głównej

Elvis-modyfikator dostępu w C# — statyczna kontrola dostępu

Elvis-modyfikator dostępu — to zestaw atrybutów i Roslyn-analizatorów dla C#, umożliwiający deklaratywnie zezwalanie dostępu do private i internal członków ściśle określonym typom. Rozwiązanie jest weryfikowane na etapie kompilacji, nie wymaga runtime-refleksji i integruje się w CI/CD.

Elvis w C#: bezpieczny friend bez naruszania enkapsulacji
Advertisement 728x90

Modifikator dostępu Elvis w C#: jak zaimplementować kontrolowany dostęp do prywatnych członków bez naruszenia inkapsulacji

Elvis – modifikator dostępu to niestandardowy system atrybutów i analizatorów Roslyn dla języka C#, który umożliwia deklaratywnie udostępnianie dostępu do private i internal członków klas tylko wybranym, "przyjaznym" typom. W przeciwieństwie do friend w C++, Elvis nie wymaga modyfikacji klasy docelowej podczas dodawania nowych zaufanych użytkowników i zapewnia surową weryfikację na etapie kompilacji. Rozwiązanie jest skierowane do programistów średniego i wysokiego poziomu, pracujących nad projektami o dużym obciążeniu, wielowątkowymi lub zależnymi od frameworków, gdzie zasady niezmienności (immutable-by-default) i lokalizacji zmienności są kluczowe dla poprawności działania.

Jak działa Elvis: architektura i kluczowe komponenty

Elvis składa się z trzech powiązanych warstw:

  • Atrybuty czasu kompilacji[Elvis], [OnlyType], [OnlyNs], [ExcludeType], [ExcludeNs]. Annotują one członki (właściwości, metody, pola), wskazując, które typy lub przestrzenie nazw mają prawo do dostępu.
  • Analizator Roslyn – wykonuje semantyczną weryfikację w IDE i podczas kompilacji: analizuje wywołania, określa kontekst dostępu („atraktor”), porównuje go z zasadami atrybutów i generuje komunikaty diagnostyczne w przypadku naruszeń.
  • Rozszerzenie VSIX i pakiet NuGet – zapewniają integrację z procesem pracy: automatyczne uzupełnianie atrybutów, szybkie stosowanie szablonów oraz wizualizację zezwolonych relacji w drzewie decyzji.

Ważne: Elvis nie zmienia kodu IL, nie korzysta z refleksji w czasie wykonania ani nie opiera się na InternalsVisibleTo. Cała weryfikacja odbywa się statycznie – podczas kompilacji. To gwarantuje, że niedozwolony dostęp nie przejdzie nawet w wersji debug.

Google AdInline article slot

Przykłady zastosowania: od podstawowego przypadku do zaawansowanych scenariuszy

Rozważmy podstawowe zadanie: klasa Me zawiera private decimal Money, ale musi umożliwić innemu typowi, MyFriend, bezpieczne modyfikowanie jego wartości, bez otwierania dostępu całemu kodowi w zestawie.

[Elvis(typeof(MyFriend))]
class Me
{
    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); // ✅ Dopuszczalne: MyFriend został wskazany w [Elvis]
        // me._money = 0; // ❌ Błąd kompilacji: bezpośredni dostęp do private pola jest zabroniony
    }
}

Nowe możliwości wprowadzone w wersjach 2026-03 i 2026-04:

  • Typy przez nazwy ciągów: [Elvis("MyNamespace.MyFriend")] – przydatne w przypadku łączności między zestawami, gdy bezpośrednie odwołanie do typu nie jest możliwe.
  • Wyrażenia regularne: [Elvis(@"^MyNamespace\.Tests\.*$")] – zezwala na dostęp wszystkim klasom testowym w danym namespace’u.
  • Atrybuty wykluczające: [ExcludeType(typeof(ObsoleteHelper))] – blokuje konkretny typ, nawet jeśli wpada pod ogólne zasady [OnlyNs].
  • Przestrzenie nazw: [OnlyNs("MyCompany.Core")] – ogranicza dostęp tylko do typów z podanej przestrzeni nazw.

Kombinatoryka i zachowanie w hierarchii

Elvis obsługuje złożone reguły. Jeśli na jednym członku występuje kilka atrybutów, domyślnie stosowana jest logika AND, jednak z priorytetem wyjątków:

Google AdInline article slot
  • Jeśli podano [OnlyNs("A")] i [ExcludeNs("A.Sub")], to dostęp jest zezwalany dla A., z wyjątkiem A.Sub..
  • W przypadku dziedziczenia: jeśli klasa bazowa jest oznaczona [Elvis(typeof(BaseFriend))], a pochodna – [Elvis(typeof(DerivedFriend))], to oba typy otrzymują dostęp do członków klasy bazowej. Jednak klasa pochodna nie nabywa uprawnień do własnych private członków – te należy anotować oddzielnie.
  • W przypadku metod z overloadem, atrybuty stosowane są indywidualnie do każdej sygnatury.

Cecha szczególna: Elvis nie wpływa na protected członki – pozostają one dostępne zgodnie ze standardowymi zasadami dziedziczenia. Atrybuty działają wyłącznie na private i internal.

Co jest ważne

  • Elvis nie jest zastępstwem DI ani interfejsów, lecz dodatkowym poziomem kontroli nad inkapsulacją tam, gdzie interfejsy są niekomfortowe (np. przy pracy z DTO, obiektami konfiguracyjnymi czy wewnętrznymi mechanizmami frameworków).
  • Wszystkie zasady są sprawdzane statycznie – zero nakładu w czasie wykonania, pełna kompatybilność z kompilacją AOT i trimmingiem.
  • Obsługa wyrażeń regularnych i nazw typów w formie ciągów czyni rozwiązanie użytecznym w scenariuszach, gdzie typy nie są dostępne na etapie kompilacji (architektury plug-inowe, generacja kodu).
  • Atrybuty wykluczające ([Exclude*]) umożliwiają tworzenie „białych list z wyjątkami”, co jest kluczowe podczas migracji dużych baz kodu.
  • Elvis nie łamie SOLID: nie osłabia DIP, lecz go uzupełnia, pozwalając formalizować umowy między komponentami bez tworzenia sztucznych interfejsów-owijaków.

Praktyczne ograniczenia i rekomendacje

Elvis nie rozwiązuje wszystkich problemów z dostępem. Powinien być stosowany, gdy:

  • Potrzebny jest punktowy, przewidywalny dostęp do private stanu bez ujawniania go przez public/internal API.
  • Architektura zakłada ścisłą izolację stref odpowiedzialności (np. jądro frameworku i jego rozszerzenia).
  • Wykorzystywane są wzorce, w których klasy są logicznie ściśle powiązane, ale powinny pozostać niezależne pod względem kompilacji (np. Builder i BuilderResult).

Nie warto stosować Elvisa:

Google AdInline article slot
  • Zamiast refaktoringu źle zaprojektowanych zależności – jeśli potrzeba 15 klas friend dla jednego private pola, najprawdopodobniej logika powinna zostać przeniesiona do osobnego serwisu.
  • W projektach bez CI/CD, gdzie analizatory nie są włączone do pipeline’a – wtedy zasady będą ignorowane podczas kompilacji poza IDE.
  • Do public członków – atrybuty są po prostu ignorowane, ponieważ dostęp jest już otwarty.

Podczas wdrażania w istniejący projekt zaleca się zaczynać od private pól w klasach DTO i wewnętrznych fabrykach, gdzie ryzyko naruszenia inwariantów jest największe, a koszt refaktoringu minimalny.

— Editorial Team

Advertisement 728x90

Czytaj dalej