Elvis Access Modifier in C#: How to Implement Controlled Access to Private Members Without Breaking Encapsulation
Elvis access modifier is a custom attribute system and Roslyn analyzer for C# that declaratively allows controlled access to private and internal class members by strictly limited 'friendly' types. Unlike C++ friend, Elvis doesn’t require modifying the target class when adding new trusted consumers and provides strict compile-time validation. This solution is aimed at middle/senior developers working on high-load, multithreaded, or framework-dependent projects where the principles of immutable-by-default and localized mutability are critical for correctness.
How Elvis Works: Architecture and Key Components
Elvis consists of three interconnected layers:
- Compile-Time Attributes —
[Elvis],[OnlyType],[OnlyNs],[ExcludeType],[ExcludeNs]. These annotate members (properties, methods, fields), specifying which types or namespaces are allowed access. - Roslyn Analyzer — performs semantic checks in the IDE and during compilation: it analyzes calls, determines the context of access ('attractor'), compares it with attribute rules, and issues diagnostic messages when violations occur.
- VSIX Extension and NuGet Package — ensures integration into the development workflow: auto-completion of attributes, quick application of templates, and visualization of permitted relationships in the decision tree.
Importantly, Elvis does not modify IL code, use runtime reflection, or rely on InternalsVisibleTo. All validation happens statically—during compilation. This guarantees that forbidden access won’t slip through even in debug builds.
Use Cases: From Basic to Advanced Scenarios
Consider a basic task: the Me class contains private decimal Money, but needs to allow another class, MyFriend, to safely modify its value without granting access to all code in the 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); // ✅ Allowed: MyFriend is specified in [Elvis]
// me._money = 0; // ❌ Compile-time error: direct access to private field is prohibited
}
}
New features added in versions 2026-03 and 2026-04 include:
- Types via String Names:
[Elvis("MyNamespace.MyFriend")]—useful for cross-assembly connections when referencing a type isn’t possible. - Regular Expressions:
[Elvis(@"^MyNamespace\\.Tests\\.*$")]—allows access to all test classes within a namespace. - Exclusion Attributes:
[ExcludeType(typeof(ObsoleteHelper))]—blocks a specific type even if it falls under the general[OnlyNs]rule. - Namespaces:
[OnlyNs("MyCompany.Core")]—restricts access only to types from the specified namespace.
Combinatorics and Behavior in Hierarchies
Elvis supports composite rules. When multiple attributes are applied to a single member, AND logic is used by default, but exceptions take priority:
- If both
[OnlyNs("A")]and[ExcludeNs("A.Sub")]are specified, access is granted toA.exceptA.Sub.. - In inheritance: if the base class is marked
[Elvis(typeof(BaseFriend))]and the derived class is marked[Elvis(typeof(DerivedFriend))], both types gain access to the base class’s members. However, the derived class does not inherit rights to its ownprivatemembers—they must be annotated separately. - For overloaded methods, attributes apply individually to each signature.
Notably, Elvis does not affect protected members—they remain accessible according to standard inheritance rules. Attributes only work with private and internal members.
What Matters
- Elvis is not a replacement for DI or interfaces; it’s an additional layer of encapsulation control where interfaces are inconvenient (e.g., when working with DTOs, configuration objects, or internal framework mechanisms).
- All rules are checked statically—zero runtime overhead, full compatibility with AOT compilation and trimming.
- Support for regular expressions and string-based type names makes the solution applicable in scenarios where types aren’t available at compile time (plugin architectures, code generation).
- Exclusion attributes (
[Exclude*]) enable creating “whitelists with exceptions,” which is crucial when migrating large codebases. - Elvis doesn’t violate SOLID: it doesn’t weaken DIP but complements it, allowing formalization of contracts between components without creating artificial wrapper interfaces.
Practical Limitations and Recommendations
Elvis doesn’t solve all access problems. It should be used when:
- Precise, predictable access to
privatestate is needed without exposing it throughpublic/internalAPIs. - The architecture requires strict isolation of responsibility zones (e.g., framework core and its extensions).
- Patterns are used where classes are logically tightly coupled but must remain independent from a compilation standpoint (e.g.,
BuilderandBuilderResult).
Avoid using Elvis when:
- Refactoring poorly designed dependencies is necessary—if you need 15
friendclasses for oneprivatefield, the logic likely belongs in a separate service. - CI/CD pipelines are absent, so analyzers aren’t included in the build process—rules will be ignored outside the IDE.
- For
publicmembers—the attributes simply ignore them since access is already open.
When implementing in an existing project, start with private fields in DTO classes and internal factories, where the risk of violating invariants is highest and the cost of refactoring is lowest.
— Editorial Team
No comments yet.