Open sandboxFocusImprove this doc

Verifying usage of a class, member, or namespace

Defining dependencies between components—who can call whom—is a critical aspect of software design. In C#, this concept is called accessibility. For optimal design, always grant the least necessary accessibility. This minimizes unintended coupling between components and makes future changes easier.

In C#, accessibility is defined across two boundaries: assemblies and types. private members are only accessible from the current type, protected members are accessible from the current and any derived type, public members are universally visible, and internal members are only accessible from the current assembly unless InternalsVisibleTo extends accessibility to other assemblies.

However, large projects often require finer control over accessibility than what C# can provide out of the box.

For instance, you might want to enforce rules such as:

  • Requiring a specific method or constructor to be called from unit tests only, based on the caller namespace.
  • Forbidding a type from being accessed from outside its home namespace.
  • Requiring a whole namespace only to be used by a friend namespace.
  • Forbidding internal members of a namespace from being accessed outside of their home namespace.

The traditional approach is to use code comments and rely on manual code reviews to enforce the desired design intent. However, this approach is prone to human error and has a lengthy feedback loop. Another approach is to split the codebase into more fine-grained projects, but this increases build and deployment complexity and negatively affects application startup time.

With Metalama, you can fine-tune the intended accessibility of your namespaces, types, or members using custom attributes or a compile-time API.

Validating usage with custom attributes

To fine-tune the accessibility of hand-picked types or members, use custom attributes.

Follow these steps:

  1. Add the Metalama.Extensions.Architecture package to your project.

  2. Apply one of the following custom attributes to the type or member for which you want to limit the accessibility.

    Attribute Description
    CanOnlyBeUsedFromAttribute Reports a warning when the target declaration is accessed from outside of the given scope.
    InternalsCanOnlyBeUsedFromAttribute Reports a warning when any internal member of the type is accessed from outside the given scope.
    CannotBeUsedFromAttribute Reports a warning when the target declaration is accessed from the given scope.
    InternalsCannotBeUsedFromAttribute Reports a warning when any internal member of the type is accessed from the given scope.
  3. Set one or more of the following properties of the custom attribute, which control the scope (which declarations can or can't access the target declaration):

    Property Description
    CurrentNamespace Includes the current namespace in the scope.
    Types Includes a list of types in the scope.
    Namespaces Includes a list of namespaces in the scope by identifying them with a string. One asterisk (*) matches any namespace component but not the dot (.). A double asterisk (**) matches any substring including the dot (.).
    NamespaceOfTypes Includes a list of the namespaces in the scope by identifying them with arbitrary types of these namespaces.
  4. Optionally, set the Description property. The value of this property will be appended to the standard error message.

Example: Test-only constructor

In the following example, the class Foo has two constructors, and one of them should only be used in tests. Tests are identified as any code in a namespace ending with the .Tests suffix. We define the Description to improve the error message. You can also set the ReferenceKinds to limit the kinds of references that are validated.

1using Metalama.Extensions.Architecture.Aspects;
2
3namespace Doc.Architecture.Type_ForTestOnly
4{
5    public class Foo
6    {
7        private bool _isTest;
8
9        public Foo() { }
10
11        [CanOnlyBeUsedFrom(
12            Namespaces = new[] { "**.Tests" },
13            Description = "Use this constructor in tests only." )]
14        public Foo( bool isTest )
15        {
16            this._isTest = isTest;
17        }
18    }
19
20    internal class ForbiddenClass
21    {
22        // This call is forbidden because it is not in a **.Tests namespace.
        Warning LAMA0905: The 'Foo.Foo(bool)' constructor cannot be referenced by the 'ForbiddenClass' type. Use this constructor in tests only.

23        private Foo _c = new( true );
24    }
25
26    namespace Tests
27    {
28        internal class TestClass
29        {
30            // This call is allowed.
31            private Foo _c = new( true );
32        }
33    }
34}

Example: Type internals reserved for the current namespace

In the following example, the class Foo uses the InternalsCanOnlyBeUsedFromAttribute constraint to verify that internal members are only accessed from the same namespace. A warning is reported when an internal method of Foo is accessed from a different namespace.

1using Doc.Architecture.Type_CurrentNamespace.A;
2using Metalama.Extensions.Architecture.Aspects;
3
4namespace Doc.Architecture.Type_CurrentNamespace
5{
6    namespace A
7    {
8        [InternalsCanOnlyBeUsedFrom( CurrentNamespace = true )]
9        public class Foo
10        {
11            public void PublicMethod() { }
12
13            internal void InternalMethod() { }
14        }
15
16        public class AllowedClass
17        {
18            public void M()
19            {
20                var foo = new Foo();
21
22                // Allowed because public.
23                foo.PublicMethod();
24
25                // Allowed because same namespace.
26                foo.InternalMethod();
27            }
28        }
29    }
30
31    namespace B
32    {
33        public class ForbiddenClass
34        {
35            public void M()
36            {
37                var foo = new Foo();
38
39                // Allowed because public.
40                foo.PublicMethod();
41
42                // Forbidden because different namespace.
                Warning LAMA0905: The 'Foo.InternalMethod()' method cannot be referenced by the 'ForbiddenClass' type.

43                foo.InternalMethod();
44            }
45        }
46    }
47}

Validating usage programmatically

Custom attributes are adequate when the types or members to validate must be hand-picked. However, when these types or members can be selected by a rule, it's more efficient to do it programmatically with compile-time code and fabrics.

Follow these steps:

  1. Add the Metalama.Extensions.Architecture package to your project.

  2. Create or reuse a fabric type as described in Fabrics:

    • To concentrate the whole validation logic for the whole project into a single location, create a ProjectFabric.
    • To share the validation logic among several projects, see Adding aspects to multiple projects.
    • To split the logic on a per-namespace basis, create one NamespaceFabric in each namespace that you want to validate.
    • To validate specific types, you can use custom attributes or add a nested TypeFabric to this type.
  3. Import the Metalama.Extensions.Architecture and Metalama.Extensions.Architecture.Predicates namespaces to benefit from extension methods.

  4. Edit the AmendProject, AmendNamespace, or AmendType method.

  5. Call one of the following methods:

    Attribute Description
    CanOnlyBeUsedFrom Reports a warning when the target declaration is accessed from outside the given scope.
    InternalsCanOnlyBeUsedFrom Reports a warning when any internal member of the type is accessed from outside of the given scope.
    CannotBeUsedFrom Reports a warning when the target declaration is accessed from the given scope.
    InternalsCannotBeUsedFrom Reports a warning when any internal member of the type is accessed from the given scope.
  6. Pass a delegate like r => r.ScopeMethod() where ScopeMethod is one of the following methods:

    Method Description
    CurrentNamespace Includes the current namespace in the scope.
    NamespaceOf Includes the parent namespace of a given type in the scope
    Type Includes a given type in the scope.
    Namespace Includes a given namespace in the scope. One asterisk (*) matches any namespace component but not the dot (.). A double asterisk (**) matches any substring including the dot (.).

    For instance:

    amender.CanOnlyBeUsedFrom( r => r.CurrentNamespace() );
    

    You can create complex conditions thanks to the And, Or, and Not methods.

  7. Optionally, pass a value for the description parameter. This text will be appended to the warning message. You can also supply a ReferenceKinds to limit the kinds of references that are validated.

Example: Namespace internals reserved for the current namespace

In the following example, we use a namespace fabric to restrict the accessibility of internal members to this namespace. A warning is reported when this rule is violated, as in the ForbiddenInheritor class.

1using Doc.Architecture.Fabric_InternalNamespace.VerifiedNamespace;
2using Metalama.Extensions.Architecture;
3using Metalama.Extensions.Architecture.Predicates;
4using Metalama.Framework.Fabrics;
5
6namespace Doc.Architecture.Fabric_InternalNamespace
7{
8    namespace VerifiedNamespace
9    {
10        internal class Fabric : NamespaceFabric
11        {



12            public override void AmendNamespace( INamespaceAmender amender )
13            {
14                amender.CanOnlyBeUsedFrom( r => r.CurrentNamespace() );
15            }
16        }
17
18        internal class Foo { }



19
20        internal class AllowedInheritor : Foo { }
21    }
22
23    namespace OtherNamespace
24    {
        Warning LAMA0905: The 'Doc.Architecture.Fabric_InternalNamespace.VerifiedNamespace' namespace cannot be referenced by the 'Doc.Architecture.Fabric_InternalNamespace.OtherNamespace' namespace.

25        internal class ForbiddenInheritor : Foo { }
26    }
27}

Example: Forbidding the use of floating-point arithmetic from the Invoicing namespace

Using floating-point arithmetic in operations involving currencies is a common pitfall. Instead, decimal numbers should be used. In the following example, we use a project fabric to validate all references to the float and double types. We report a diagnostic when they're used from the **.Invoicing namespaces.

1using Metalama.Extensions.Architecture;
2using Metalama.Extensions.Architecture.Predicates;
3using Metalama.Framework.Fabrics;
4
5namespace Doc.Architecture.Fabric_ForbidFloat
6{
7    internal class Fabric : ProjectFabric
8    {



9        public override void AmendProject( IProjectAmender amender )
10        {
11            amender
12                .SelectReflectionTypes( typeof(float), typeof(double) )
13                .CannotBeUsedFrom(
14                    r => r.Namespace( "**.Invoicing" ),
15                    "Use decimal numbers instead." );
16        }
17    }
18
19    namespace Invoicing



20    {
21        internal class Invoice
22        {
            Warning LAMA0905: The 'double' type cannot be referenced by the 'Doc.Architecture.Fabric_ForbidFloat.Invoicing' namespace. Use decimal numbers instead.

23            public double Amount { get; set; }
24        }
25    }
26}