NetArchTest vs ArchUnitNET vs Roslyn Analyzers: Choosing How to Enforce .NET Architecture Rules

Verified against .NET 10, xUnit v3, NetArchTest.eNhancedEdition, ArchUnitNET, and current Roslyn analyzer guidance. Last reviewed: July 23, 2026. Package versions remain pinned outside the prose because the testing ecosystem continues to evolve.

They Enforce Rules—but They Do Not Enforce Them at the Same Time

NetArchTest.eNhancedEdition, ArchUnitNET, and Roslyn analyzers are often compared as if they were interchangeable libraries. They are not. The first two usually run as tests against compiled assemblies. A Roslyn analyzer runs during code analysis and can report a diagnostic while the developer is still editing the source.

That timing difference changes what each tool can see and how quickly it can respond. A dependency rule such as “Domain must not reference Infrastructure” is easy to express as an architecture test. A rule that depends on a specific syntax pattern, source symbol, or instant IDE feedback is a better analyzer candidate.

The right question is therefore not “which library is best?” It is “what information does this rule need, and when should the developer see the failure?”

NetArchTest.eNhancedEdition: Concise Rules for the Common 80 Percent

NetArchTest.eNhancedEdition is a maintained successor to the familiar NetArchTest-style fluent API. It is well suited to straightforward dependency direction, naming, sealing, immutability, statelessness, and access-modifier rules.

Its strength is readability. A rule can select types from an assembly, describe a condition, and return a result that lists failures. That makes it easy to place architecture checks in the same xUnit project as other tests and fail the CI pipeline with dotnet test.

For a team that wants a small set of clear structural rules without introducing a source-analyzer project, this is often the simplest starting point.

Package warning: Many older articles still install `NetArchTest.Rules`. For new work, verify the current package status and prefer the maintained `NetArchTest.eNhancedEdition` fork or ArchUnitNET.
LayerRules.cs
using NetArchTest.Rules;

public sealed class LayerRules
{
    [Fact]
    public void Domain_must_not_depend_on_infrastructure()
    {
        TestResult result = Types
            .InAssembly(typeof(DomainAssemblyMarker).Assembly)
            .ShouldNot()
            .HaveDependencyOn(
                "ArchitectureGuard.Infrastructure")
            .GetResult();

        Assert.True(
            result.IsSuccessful,
            string.Join(
                Environment.NewLine,
                result.FailingTypes.Select(
                    type => type.FullName)));
    }
}

ArchUnitNET: A Richer Architecture Model and Familiar ArchUnit Style

ArchUnitNET imports assemblies into an architecture model through ArchLoader, then applies rules created with ArchRuleDefinition. It can express dependencies between classes, members, interfaces, layers, and slices using an API that is familiar to Java teams already using ArchUnit.

The richer model is useful when the solution has complex module rules, repeated slices, or a team that wants to describe architecture in a more explicit vocabulary. Loading the architecture once in a fixture also keeps the suite fast.

The trade-off is a larger conceptual surface than the concise NetArchTest style. Choose it when that model improves clarity for your rules, not simply because it offers more API.

LayerRules.ArchUnit.cs
using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

public sealed class ArchitectureFixture
{
    public Architecture Architecture { get; } =
        new ArchLoader()
            .LoadAssemblies(
                typeof(DomainAssemblyMarker).Assembly,
                typeof(InfrastructureAssemblyMarker).Assembly)
            .Build();
}

public sealed class ArchUnitLayerRules(
    ArchitectureFixture fixture)
{
    [Fact]
    public void Domain_must_not_depend_on_infrastructure()
    {
        IArchRule rule = Types()
            .That()
            .ResideInAssembly(
                typeof(DomainAssemblyMarker).Assembly)
            .Should()
            .NotDependOnAny(
                Types().That().ResideInAssembly(
                    typeof(InfrastructureAssemblyMarker).Assembly))
            .Because(
                "Domain must remain independent of infrastructure");

        rule.Check(fixture.Architecture);
    }
}

The Shared Limitation: Both Fluent Libraries See Compiled Code

Both architecture-test libraries work from compiled assemblies or bytecode-level information. That is why they can inspect dependencies without running the application—and why some source constructs are invisible after compilation.

A nameof(SomeType) expression is a common example. The compiler replaces it with a string literal, so the resulting assembly no longer contains a type dependency that an architecture test can detect. Similar problems appear when a rule depends on syntax, comments, source locations, or information erased by the compiler.

Do not assume a green rule means the selector saw what you intended. Prove each important rule can fail by deliberately introducing a violation. If the rule depends on source-level meaning, move it to Roslyn.

Vacuous success is dangerous: A misspelled namespace selector can match zero types and still produce a green test. Add a guard that confirms the selector found the expected population.
SelectorGuard.cs
Type[] handlers = typeof(ApplicationAssemblyMarker)
    .Assembly
    .GetTypes()
    .Where(type => type.Name.EndsWith(
        "Handler",
        StringComparison.Ordinal))
    .ToArray();

Assert.NotEmpty(handlers);

TestResult result = Types
    .InAssembly(typeof(ApplicationAssemblyMarker).Assembly)
    .That()
    .HaveNameEndingWith("Handler")
    .Should()
    .BeSealed()
    .GetResult();

Assert.True(result.IsSuccessful);

Roslyn Analyzers: Use Them When the Rule Must Fail in the Editor

A Roslyn analyzer inspects C# syntax trees and semantic symbols during development and build. It can underline a violation in the IDE, place a diagnostic in the Error List, and optionally offer a code fix. That is a different developer experience from waiting for the architecture-test project to run.

Use an analyzer when the rule must be enforced on every compile, depends on source syntax, or needs precise symbol information. Examples include banning a particular namespace import, requiring an attribute on a source declaration, or enforcing an API-usage pattern before the project reaches the test stage.

The cost is higher. An analyzer needs its own package, diagnostics, tests, severity configuration, and ongoing compatibility work. Do not build one for a simple layer dependency that a ten-line architecture test already expresses clearly.

ForbiddenDependencyAnalyzer.cs
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForbiddenDependencyAnalyzer
    : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "ARCH001",
        title: "Forbidden architecture dependency",
        messageFormat:
            "Domain code must not reference '{0}'",
        category: "Architecture",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor>
        SupportedDiagnostics => [Rule];

    public override void Initialize(
        AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(
            GeneratedCodeAnalysisFlags.None);

        context.EnableConcurrentExecution();

        context.RegisterSyntaxNodeAction(
            AnalyzeUsing,
            SyntaxKind.UsingDirective);
    }

    private static void AnalyzeUsing(
        SyntaxNodeAnalysisContext context)
    {
        var directive = (UsingDirectiveSyntax)context.Node;
        string namespaceName = directive.Name?.ToString() ?? "";

        if (namespaceName.StartsWith(
            "ArchitectureGuard.Infrastructure",
            StringComparison.Ordinal))
        {
            context.ReportDiagnostic(
                Diagnostic.Create(
                    Rule,
                    directive.GetLocation(),
                    namespaceName));
        }
    }
}

Custom Reflection Tests: The Practical Middle Ground

Some conventions do not need a full analyzer but are awkward in a fluent architecture API. A custom reflection test is often the simplest middle ground. It still runs through xUnit, remains easy to understand, and can inspect runtime type metadata directly.

Good candidates include verifying that every EF Core entity has an IEntityTypeConfiguration<T>, every endpoint class carries a required attribute, or every handler has a public constructor with a specific dependency shape.

Reflection tests are still limited to information available after compilation. Use them for behavioral or metadata conventions, not source syntax.

ReflectionRules.cs
[Fact]
public void Every_endpoint_requires_authorization()
{
    Type[] endpointTypes = typeof(ApiAssemblyMarker)
        .Assembly
        .GetTypes()
        .Where(type =>
            type.IsClass &&
            type.Name.EndsWith(
                "Endpoint",
                StringComparison.Ordinal))
        .ToArray();

    Assert.NotEmpty(endpointTypes);

    Type[] missingAuthorization = endpointTypes
        .Where(type =>
            !type.IsDefined(
                typeof(AuthorizeAttribute),
                inherit: true))
        .ToArray();

    Assert.True(
        missingAuthorization.Length == 0,
        "Endpoints missing [Authorize]:" +
        Environment.NewLine +
        string.Join(
            Environment.NewLine,
            missingAuthorization.Select(
                type => type.FullName)));
}

A Simple Decision Rule for Choosing the Tool

Start with the cheapest tool that can express the rule accurately. Use NetArchTest.eNhancedEdition or ArchUnitNET for compiled structural rules. Use a custom reflection test when the convention depends on runtime metadata. Use Roslyn when the rule needs source syntax, semantic symbols, or immediate IDE feedback.

Do not split the same decision across several tools. If “Domain must not depend on Infrastructure” is enforced in ArchUnitNET, do not recreate it in NetArchTest and a Roslyn analyzer. Duplication creates inconsistent messages and three places to maintain one architectural decision.

For legacy systems, add a checked-in baseline of known violations and fail only on new ones. That gives the team a green pipeline immediately while preventing further drift.

ArchitectureToolDecision.txt
RULE NEEDS COMPILED TYPE / DEPENDENCY INFORMATION
    → NetArchTest.eNhancedEdition or ArchUnitNET

RULE NEEDS RUNTIME METADATA OR A CUSTOM CONVENTION
    → Reflection test in the architecture-test project

RULE NEEDS SOURCE SYNTAX, SEMANTIC SYMBOLS,
IDE SQUIGGLES, OR FAILURE ON EVERY COMPILE
    → Roslyn analyzer

TEAM ALREADY KNOWS JAVA ARCHUNIT
    → ArchUnitNET is usually the natural fit

TEAM WANTS A SMALL, CONCISE FLUENT TEST API
    → NetArchTest.eNhancedEdition is usually the simpler fit

IMPORTANT
    → Pick one fluent library per solution
    → Prove every rule can fail
    → Prefer typed assembly markers over namespace strings

What Developers Want to Know

Should I use NetArchTest or ArchUnitNET for a new .NET project?

Choose one based on the team's preferred API and rule complexity. NetArchTest.eNhancedEdition is concise for common dependency and convention rules. ArchUnitNET offers a richer ArchUnit-style model and is familiar to teams coming from Java. Do not mix both for the same concern.

Why should I avoid the original NetArchTest.Rules package?

The original package has not kept pace with the maintained ecosystem. For new work, use the actively maintained NetArchTest.eNhancedEdition fork or choose ArchUnitNET instead. Recheck current package status before publication or adoption.

When is a Roslyn analyzer better than an architecture test?

Use a Roslyn analyzer when the rule depends on source syntax or semantic information, or when developers must see the violation in the IDE and every compile. Architecture-test libraries are simpler for assembly dependency and naming rules that can run in the normal test suite.

Can architecture tests prove that the system has good architecture?

No. They only enforce the rules you wrote. A green test suite proves that the current code follows those declared boundaries; it does not prove the boundaries are correct, useful, or complete.

Back to Articles