ArchUnitNET Documentation

repository·main·Indexed 23 days ago

https://github.com/tng/archunitnet

A C# library for checking code architecture by analyzing bytecode. It provides a fluent API to enforce architectural rules, dependency constraints, and naming conventions through automated tests. Features include support for xUnit, NUnit, and MSTestV2, architecture caching, and the ability to both enforce rules from and generate dependency diagrams using PlantUML.

Tokens
17.7K
Snippets
18
Records
38
Agent score
79%

What's inside ArchUnitNET

  1. Overview of ArchUnitNET

    main

    ArchUnitNET is a library for checking the architecture of C# code. It is a C# port of the ArchUnit Java library. It allows developers to automatically test architecture and coding rules by analyzing C# bytecode. You can use it to check dependencies between various code elements, including:

    • Classes
    • Members
    • Interfaces
    • And more
  2. License information for ArchUnitNET

    main
    ArchUnitNET is published under the Apache License 2.0. This license provides a perpetual, worldwide, non-exclusive, no-charge, royalty-free, and irrevocable copyright and patent license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the work.
  3. Understand ArchUnitNET limitations

    main

    ArchUnitNET has specific technical limitations regarding how it inspects code. Users should be aware of the following:

    • Debug Artifacts: Information regarding how debug information affects analysis.
    • Constant Fields: Information regarding how constant fields are handled during architecture testing.
  4. Use Object predicates to define architectural rules

    main

    In ArchUnitNET, Object predicates allow you to define constraints on the properties and relationships of architectural elements (such as classes, methods, or types). These predicates can be used to assert that an object matches a specific state, name, visibility, or dependency.

    Predicates are categorized into:

    • Identity and State: Checking if an object exists, its name, its full name, or its visibility (e.g., BePublic, BePrivate).
    • Relationships: Checking what an object calls (CallAny), what it depends on (DependOnAny, OnlyDependOn), or what attributes it possesses (HaveAnyAttributes).
    • Negations: Most predicates have a corresponding negation (e.g., NotExist, NotBe, NotCallAny) to assert that a condition is not met.
  5. Understand limitations regarding constant fields

    main

    ArchUnitNET cannot detect type dependencies when a class accesses a const field.

    When a method accesses a constant (e.g., public const string ConstField = "Value";), the C# compiler replaces the field access with the literal value (using the Ldstr opcode) at compile time. Because the field reference is not present in the IL (Intermediate Language), ArchUnitNET's dependency analysis cannot trace the dependency back to the class containing the constant field.

    If you attempt to assert that a method has a type dependency on a class containing only constants, the assertion will fail.

    class ClassWithStaticField
    {
        public const string ConstField = "ConstField";
    }
    
    class ClassAccessingField
    {
        public void MethodAccessingConstField()
        {
            // The compiler replaces this with the literal string, 
            // removing the dependency on ClassWithStaticField in the IL.
            var a = ClassWithStaticField.ConstField;
        }
    }
    
    // This assertion will FAIL:
    var method = Architecture.GetClassOfType(typeof(ClassAccessingField))
        .GetMethodMembers()
        .First(member => member.FullNameContains("MethodAccessingConstField"));
    
    var methodTypeDependencies = method.GetTypeDependencies().ToList();
    Assert.Contains(Architecture.GetClassOfType(typeof(ClassWithStaticField)), methodTypeDependencies);
  6. Control diagram depth with slice patterns

    main

    When building diagrams, you can control the depth of the slices by using single asterisks (*) in your pattern. A single asterisk represents one level of depth. Note that you cannot mix single (*) and double (**) asterisks in a single pattern.

    Common patterns:

    • ArchUnitNET.(*) : One slice deep.
    • ArchUnitNET.(*).(*) : Two slices deep.
    • ArchUnitNET.Fluent.(*).(*).(*) : Three slices deep.
  7. Use Type predicates to define architecture rules

    main

    ArchUnitNET provides a suite of predicates for the Type abstraction to validate the structure and location of types within your codebase. These predicates allow you to assert properties such as type identity, assignability, interface implementation, namespace/assembly residency, and member existence.

    Predicates are categorized into positive assertions (e.g., Are, ImplementInterface) and negations (e.g., AreNot, DoNotImplementInterface).

  8. Enforce rules using PlantUML diagrams

    main

    You can derive dependency rules directly from PlantUML component diagrams. ArchUnitNET will ensure that your code adheres strictly to the dependencies defined in the diagram.

    Requirements:

    • The diagram must be a component diagram.
    • You must associate types to components using stereotypes (e.g., <<Model.*>>).
    • ArchUnitNET uses a regex as the namespace identifier (unlike standard ArchUnit which uses two dots).

    Note: Only dependencies explicitly specified in the diagram are considered. Unknown dependencies are ignored.

    Example PlantUML:

    @startuml
    [Model] <<Model.*>>
    [Controller] <<Controller.*>>
    
    [Controller] --> [Model]
    @enduml

    Usage in C#:

    string myDiagram = "./Resources/my-diagram.puml";
    IArchRule someRule = Types().Should().AdhereToPlantUmlDiagram(myDiagram);
    someRule.Check(Architecture);
    String myDiagram = "./Resources/my-diagram.puml";
    IArchRule someRule = Types().Should().AdhereToPlantUmlDiagram(myDiagram);
    someRule.Check(Architecture);
  9. Use Object Conditions to define architectural rules

    main

    ArchUnitNET provides a wide range of object conditions to validate the properties, visibility, and relationships of code elements (like classes, methods, or types). These conditions can be used directly or negated using their Not counterparts (e.g., Be(...) vs NotBe(...)).

    Common categories of object conditions include:

    • Existence: Exist(), NotExist()
    • Identity/Matching: Be(...), HaveName(...), HaveFullName(...), HaveNameStartingWith(...), etc.
    • Visibility: BePublic(), BePrivate(), BeProtected(), BeInternal(), etc.
    • Dependencies: DependOnAny(...), OnlyDependOn(...)
    • Method Calls: CallAny(...)
    • Attributes: HaveAnyAttributes(...), OnlyHaveAttributes(...)
  10. Run ArchUnitNET tests

    main

    Because ArchUnitNET analyzes architecture by reading compiled binaries, it is highly recommended to run your tests in the Debug configuration to ensure proper analysis of the artifacts.

    Use the following command to run tests via the .NET CLI:

    dotnet test -c Debug
  11. Define and check ArchUnitNET rules

    main

    ArchUnitNET uses a fluent API to define architectural rules. You typically follow this workflow:

    1. Import: Use using static ArchUnitNET.Fluent.ArchRuleDefinition; to simplify rule creation.
    2. Declare Layers/Groups: Use Types(), Classes(), or Interfaces() combined with predicates (like ResideInNamespace or ImplementInterface) to create IObjectProvider variables. Use .As("description") to provide human-readable names for failures.
    3. Define Rules: Combine providers using .Should() and predicates (like .Be(), .NotDependOnAny(), or .HaveNameContaining()).
    4. Execute: Call .Check(Architecture) to validate the rules against your loaded architecture.
    using static ArchUnitNET.Fluent.ArchRuleDefinition;
    
    // 1. Declare layers
    private readonly IObjectProvider<IType> ExampleLayer = 
        Types().That().ResideInAssembly("ExampleAssembly").As("Example Layer");
    
    private readonly IObjectProvider<Class> ExampleClasses = 
        Classes().That().ImplementInterface("IExampleInterface").As("Example Classes");
    
    // 2. Define and check rules
    [Fact]
    public void TypesShouldBeInCorrectLayer()
    {
        IArchRule rule = Classes().That().Are(ExampleClasses).Should().Be(ExampleLayer);
        rule.Check(Architecture);
    }
    
    [Fact]
    public void ExampleLayerShouldNotAccessForbiddenLayer()
    {
        IArchRule rule = Types().That().Are(ExampleLayer)
            .Should().NotDependOnAny(ForbiddenLayer)
            .Because("it's forbidden");
        rule.Check(Architecture);
    }
  12. Create ArchUnitNET architecture tests

    main

    ArchUnitNET allows you to define architectural rules using a fluent API. The workflow involves three main steps:

    1. Load the Architecture: Use ArchLoader to load the assemblies you want to analyze. For performance, load the architecture once (e.g., in a static field).
    2. Define Selectors: Use methods like Types(), Classes(), and Interfaces() combined with predicates (e.g., ResideInAssembly, ImplementInterface) to select the code elements you want to target. Use .As("description") to provide human-readable names for these selections.
    3. Define and Check Rules: Use the .Should() syntax to define constraints (e.g., Be(), NotDependOnAny(), HaveNameContaining()) and call .Check(Architecture) to validate the rules against the loaded architecture.

    Rules can be combined using the .And() method.

    using ArchUnitNET.Domain;
    using ArchUnitNET.Loader;
    using ArchUnitNET.Fluent;
    using Xunit;
    
    // Use static import for easier rule definition
    using static ArchUnitNET.Fluent.ArchRuleDefinition;
    
    namespace ExampleTest
    {
        public class ExampleArchUnitTest
        {
            // 1. Load architecture once
            private static readonly Architecture Architecture = new ArchLoader().LoadAssemblies(
                System.Reflection.Assembly.Load("ExampleClassAssemblyName"),
                System.Reflection.Assembly.Load("ForbiddenClassAssemblyName")
            ).Build();
    
            // 2. Define selectors
            private readonly IObjectProvider<IType> ExampleLayer =
                Types().That().ResideInAssembly("ExampleAssembly").As("Example Layer");
    
            private readonly IObjectProvider<Class> ExampleClasses =
                Classes().That().ImplementInterface("IExampleInterface").As("Example Classes");
    
            // 3. Write and check rules
            [Fact]
            public void TypesShouldBeInCorrectLayer()
            {
                IArchRule rule = Classes().That().Are(ExampleClasses).Should().Be(ExampleLayer);
                rule.Check(Architecture);
            }
    
            [Fact]
            public void ExampleLayerShouldNotAccessForbiddenLayer()
            {
                // Rules can include custom reasons for failure
                IArchRule rule = Types().That().Are(ExampleLayer).Should()
                    .NotDependOnAny(Types().That().ResideInNamespace("ForbiddenNamespace"))
                    .Because("it's forbidden");
                rule.Check(Architecture);
            }
        }
    }