Back to Home

Elvis access modifier in C# — static access control

Elvis access modifier — a set of attributes and Roslyn analyzers for C# that declaratively allow access to private and internal members for strictly specified types. The solution is checked at compile time, requires no runtime reflection, and integrates into CI/CD.

Elvis in C#: safe friend without breaking encapsulation
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
  • If both [OnlyNs("A")] and [ExcludeNs("A.Sub")] are specified, access is granted to A. except A.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 own private members—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 private state is needed without exposing it through public/internal APIs.
  • 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., Builder and BuilderResult).

Avoid using Elvis when:

Google AdInline article slot
  • Refactoring poorly designed dependencies is necessary—if you need 15 friend classes for one private field, 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 public members—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

Advertisement 728x90

Read Next