Skip to content

Architecture Tests in ConnectSoft Microservice Template

Purpose & Overview

Architecture Tests are automated tests that validate architectural rules and constraints at build time, ensuring that the codebase maintains its intended structure and prevents architectural violations. In the ConnectSoft Microservice Template, architecture tests use NetArchTest.Rules to enforce Clean Architecture principles, dependency rules, naming conventions, and design patterns.

Why Architecture Tests?

Architecture tests provide several critical benefits:

  • Build-Time Enforcement: Architectural violations fail builds automatically
  • Living Documentation: Tests serve as executable documentation of architectural rules
  • Prevent Architectural Drift: Catch violations before they spread through the codebase
  • Team Alignment: Shared understanding of architectural constraints
  • Refactoring Safety: Confidence that refactoring won't break architectural rules
  • CI/CD Integration: Automated validation in every build
  • Code Review Support: Objective, measurable architecture validation

Architecture Tests Philosophy

Architecture tests transform architectural rules from documentation into executable code. They fail fast, provide clear feedback, and prevent architectural degradation over time. Unlike manual code reviews, architecture tests are objective, repeatable, and catch violations before they become problems.

Architecture Overview

Architecture Tests in the Template

The ConnectSoft Microservice Template includes comprehensive architecture tests organized by concern:

ArchitectureTests Project
├── EnforcingLayeredArchitectureUnitTests.cs
│   ├── ControllersShouldNotDirectlyReferencePersistenceModel
│   ├── ControllersShouldNotDependOnAnyDomainModelClassServiceImplementation
│   ├── GrpcServicesShouldNotDirectlyReferencePersistenceModel
│   ├── GrpcServicesShouldNotDependOnAnyDomainModelClassServiceImplementation
│   ├── DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations
│   ├── LowerLayersShouldNotReferenceUpperLayers
│   ├── ApplicationModelShouldNotReferenceConcretePersistenceImplementations
│   └── EntityModelShouldNotReferenceInfrastructureLayers
├── OptionsArchitectureUnitTests.cs
│   ├── OptionsShouldBeImplementedAsClasses
│   ├── OptionsShouldBeSealedClasses
│   ├── OptionsClassesShouldBePublic
│   └── OptionsClassesShouldHavePublicProperties
├── NamingConventionsArchitectureUnitTests.cs
│   ├── InterfacesShouldStartWithIPrefix
│   ├── RepositoryInterfacesShouldEndWithRepository
│   ├── UseCaseInterfacesShouldEndWithUseCase
│   ├── InputClassesShouldEndWithInput
│   ├── OutputClassesShouldEndWithOutput
│   ├── CommandsShouldEndWithCommand
│   ├── EventsShouldEndWithEvent
│   ├── OptionsClassesShouldEndWithOptions
│   ├── ExtensionMethodContainersShouldEndWithExtensions
│   └── ExceptionsShouldEndWithException
├── DomainEventPatternArchitectureUnitTests.cs
│   ├── DomainEventsShouldBeInDomainLayer
│   └── DomainEventsShouldNotBeInInfrastructureLayers
├── DomainServicePatternArchitectureUnitTests.cs
│   ├── DomainServiceInterfacesShouldImplementIDomainService
│   ├── DomainServiceImplementationsShouldBeInDomainModelImpl
│   ├── DomainServicesShouldNotDependOnInfrastructure
│   └── DomainServiceInterfacesShouldBeInDomainModel
├── UseCasePatternArchitectureUnitTests.cs
│   ├── UseCaseInterfacesShouldImplementIUseCase
│   ├── UseCaseInterfacesShouldEndWithUseCase
│   ├── UseCaseInterfacesShouldBeInDomainModel
│   ├── UseCaseImplementationsShouldBeInDomainModelImpl
│   └── UseCasesShouldHaveCorrespondingInputOutputClasses
├── RepositoryPatternArchitectureUnitTests.cs
│   ├── RepositoryInterfacesShouldInheritFromIGenericRepository
│   ├── RepositoryImplementationsShouldBeInImplementationProjects
│   ├── RepositoryImplementationsShouldImplementInterfaces
│   └── DomainModelShouldNotReferenceRepositoryImplementations
├── MessagingModelArchitectureUnitTests.cs
│   ├── CommandsShouldImplementICommand
│   ├── EventsShouldImplementIEvent
│   ├── CommandsShouldEndWithCommand
│   ├── EventsShouldEndWithEvent
│   ├── MessagesShouldBeInMessagingModelOnly
│   └── MessagesShouldNotHaveInfrastructureDependencies
├── CrossCuttingConcernsArchitectureUnitTests.cs
│   ├── ExceptionsShouldEndWithException
│   ├── ExtensionMethodsShouldBeInternalAndStatic
│   ├── OptionsClassesShouldUseDataAnnotationsForValidation
│   └── ConstantsShouldBeInConstantClasses
├── EntityModelIsolationArchitectureUnitTests.cs
│   ├── EntityModelShouldNotReferencePersistenceImplementations
│   ├── EntityModelShouldNotReferenceMessagingModels
│   ├── EntityModelShouldNotReferenceServiceModels
│   ├── EntityInterfacesShouldImplementIGenericEntity
│   └── AggregateRootInterfacesShouldImplementIGenericAggregateRoot
├── ApplicationModelIsolationArchitectureUnitTests.cs
│   ├── ApplicationModelShouldNotReferenceConcretePersistenceImplementations
│   └── ExtensionMethodClassesShouldBeInternalAndStatic
└── SpecificationPatternArchitectureUnitTests.cs
    ├── SpecificationInterfacesShouldImplementISpecification
    ├── SpecificationsShouldEndWithSpecification
    └── SpecificationsShouldBeInPersistenceModel

Test Categories

All architecture tests are tagged with [TestCategory("Architecture Unit Tests")] for easy filtering:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
    // Test implementation
}

NetArchTest.Rules

What is NetArchTest.Rules?

NetArchTest.Rules is a fluent testing library for .NET that validates architectural rules using reflection. It provides a fluent API for querying assemblies, types, and dependencies.

Key Features

  • Fluent API: Expressive, readable test syntax
  • Type-Based Queries: Query assemblies, namespaces, and types
  • Dependency Analysis: Validate dependency relationships
  • Pattern Matching: Match types by name, namespace, inheritance, interfaces
  • Performance: Fast execution using reflection

Installation

<PackageReference Include="NetArchTest.Rules" />

Basic Usage Pattern

using NetArchTest.Rules;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestMethod]
public void MyArchitectureRule()
{
    // Arrange: Define types to test
    var types = Types.InAssembly(typeof(MyClass).Assembly)
        .That()
        .ResideInNamespace("My.Namespace")
        .And()
        .AreClasses();

    // Act: Apply rules
    var result = types
        .Should()
        .NotHaveDependencyOn("Forbidden.Assembly")
        .GetResult();

    // Assert: Validate results
    Assert.IsTrue(result.IsSuccessful, result.FailingTypes?.ToString());
}

Architecture Test Patterns

Dependency Rule Tests

Pattern: Validate that certain types don't depend on forbidden assemblies.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
    // Arrange
    Type controllersType = typeof(MicroserviceAggregateRootsServiceController);
    List<string> repositoriesDependencies = new();

    string? repositoriesAssemblyName = typeof(IMicroserviceAggregateRootsRepository).Assembly.FullName;
    if (!string.IsNullOrEmpty(repositoriesAssemblyName))
    {
        repositoriesDependencies.Add(repositoriesAssemblyName);
    }

#if UseMongoDb
    string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
    {
        repositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
    }
#endif

#if UseNHibernate
    string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
    {
        repositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
    }
#endif

    // Act
    bool result = Types.InAssembly(controllersType.Assembly)
        .That()
        .ResideInNamespace(controllersType.Namespace)
        .ShouldNot()
        .HaveDependencyOnAny(repositoriesDependencies.ToArray())
        .GetResult()
        .IsSuccessful;

    // Assert
    Assert.IsTrue(result);
}

Key Techniques: - Conditional Compilation: Use #if directives for optional features - Assembly References: Use typeof() to get assembly information - Multiple Dependencies: Check against multiple forbidden dependencies - Namespace Filtering: Limit scope to specific namespaces

Implementation Dependency Tests

Pattern: Validate that types depend on interfaces, not implementations.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDependOnAnyDomainModelClassServiceImplementation()
{
    // Arrange
    PredicateList webApiControllers =
        Types.InAssembly(typeof(MicroserviceAggregateRootsServiceController).Assembly)
            .That()
            .AreClasses()
            .And().ArePublic()
            .And().Inherit(typeof(ControllerBase));

    // Get all domain model implementation classes
    string?[] services = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
        .That()
        .ArePublic()
        .And().AreClasses()
        .GetTypes()
        .Where(localType => !string.IsNullOrWhiteSpace(localType.FullName))
        .Select(localType => localType.FullName)
        .Distinct(StringComparer.Ordinal)
        .ToArray();

    // Act
    NetArchTest.Rules.TestResult testResult = webApiControllers
        .Should()
        .NotHaveDependencyOnAny(services)
        .GetResult();

    // Assert
    Assert.IsTrue(
        testResult.IsSuccessful, 
        "Web API controllers must * not * depend on any domain model service implementation");
}

Key Techniques: - Type Queries: Use GetTypes() to get all types matching criteria - LINQ Filtering: Use LINQ to filter and transform types - PredicateList: Use PredicateList for complex queries - Clear Error Messages: Include descriptive assertion messages

Layer Dependency Tests

Pattern: Validate unidirectional dependency flow (outer → inner).

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void LowerLayersShouldNotReferenceUpperLayers()
{
    // Domain model should not reference service model
    bool result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
        .ShouldNot()
        .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);

    // Persistence model should not reference service model
    result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
        .ShouldNot()
        .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);

    // Persistence model should not reference domain model
    result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
        .ShouldNot()
        .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.DomainModel")
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);
}

Key Techniques: - Multiple Assertions: Test multiple layer relationships in one test - Namespace Matching: Use string-based namespace matching - Assembly-Based: Start from assembly to ensure comprehensive coverage

Contract vs Implementation Tests

Pattern: Validate that Application Layer uses interfaces, not concrete implementations.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations()
{
    Type domainModelImplType = typeof(DefaultMicroserviceAggregateRootsRetriever);
    List<string> concreteRepositoriesDependencies = new();

#if UseMongoDb
    string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
    {
        concreteRepositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
    }
#endif

#if UseNHibernate
    string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
    {
        concreteRepositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
    }
#endif

    // Assert: Should NOT depend on concrete implementations
    bool result = Types.InAssembly(domainModelImplType.Assembly)
        .That()
        .ResideInNamespace(domainModelImplType.Namespace)
        .ShouldNot()
        .HaveDependencyOnAny(concreteRepositoriesDependencies.ToArray())
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);

    // Assert: SHOULD depend on interfaces
    result = Types.InAssembly(domainModelImplType.Assembly)
        .That()
        .HaveName(
            nameof(DefaultMicroserviceAggregateRootsRetriever),
            nameof(DefaultMicroserviceAggregateRootsProcessor))
        .And()
        .ResideInNamespace(domainModelImplType.Namespace)
        .Should()
        .HaveDependencyOn(typeof(IMicroserviceAggregateRootsRepository).AssemblyQualifiedName)
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);
}

Key Techniques: - Positive and Negative Assertions: Test both what should and shouldn't exist - Type Name Filtering: Use HaveName() for specific types - Multiple Conditions: Combine multiple conditions with And()

Naming Convention Tests

Pattern: Validate naming conventions and type characteristics across the solution.

Interface Naming:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void InterfacesShouldStartWithIPrefix()
{
    var interfaces = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .GetTypes()
        .Where(type =>
        {
            var ns = type.Namespace ?? string.Empty;
            return ns.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal);
        })
        .ToList();

    var failingTypes = interfaces
        .Where(i => !i.Name.StartsWith("I", StringComparison.Ordinal))
        .ToList();

    Assert.IsTrue(
        failingTypes.Count == 0,
        $"Interfaces not starting with 'I': {string.Join(", ", failingTypes.Select(t => t.FullName))}");
}

Options Naming:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void OptionsShouldBeImplementedAsClasses()
{
    Assembly optionsAssembly = typeof(MicroserviceOptions).Assembly;
    var result = Types.InAssembly(optionsAssembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.Options")
        .And()
        .HaveNameEndingWith("Options", System.StringComparison.Ordinal)
        .Should()
        .BeClasses()
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);
}

Key Techniques: - Name Pattern Matching: Use HaveNameEndingWith(), HaveNameMatching() for naming conventions - Type Characteristics: Use BeClasses(), BeSealed(), etc. - Namespace Filtering: Limit scope to specific namespaces - Custom Filtering: Use LINQ for complex filtering logic

Sealed Class Tests

Pattern: Validate that certain types are sealed (with exceptions).

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void OptionsShouldBeSealedClasses()
{
    Assembly optionsAssembly = typeof(MicroserviceOptions).Assembly;
    var result = Types.InAssembly(optionsAssembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.Options")
        .And()
        .HaveNameEndingWith("Options", System.StringComparison.Ordinal)
        .And()
        .DoNotHaveName("DnsCommonOptions")
        .And()
        .DoNotHaveName("MassTransitReceiveEndpointOptions")
        .And()
        .DoNotImplementInterface(typeof(IValidateOptions<>))
        .Should()
        .BeSealed()
        .GetResult();

    Assert.IsTrue(result.IsSuccessful);
}

Key Techniques: - Exception Handling: Use DoNotHaveName() for exceptions - Interface Exclusion: Use DoNotImplementInterface() to exclude validators - Multiple Exclusions: Chain multiple exclusion conditions

NetArchTest.Rules API Reference

Querying Types

Selecting Types:

// All types in an assembly
Types.InAssembly(typeof(MyClass).Assembly)

// Types in a namespace
Types.InAssembly(assembly).That().ResideInNamespace("My.Namespace")

// Types by name pattern
Types.InAssembly(assembly).That().HaveNameEndingWith("Repository")

// Types by inheritance
Types.InAssembly(assembly).That().Inherit(typeof(MyBaseClass))

// Types by interface
Types.InAssembly(assembly).That().ImplementInterface(typeof(IMyInterface))

Filtering Conditions:

// Combine conditions
.That()
    .AreClasses()
    .And().ArePublic()
    .And().ResideInNamespace("My.Namespace")

// Exclude conditions
.That()
    .HaveNameEndingWith("Options")
    .And().DoNotHaveName("ExceptionOptions")
    .And().DoNotImplementInterface(typeof(IValidator))

Type Characteristics:

.AreClasses()           // Must be classes
.AreInterfaces()        // Must be interfaces
.AreAbstract()          // Must be abstract
.AreSealed()            // Must be sealed
.ArePublic()            // Must be public
.AreNotPublic()         // Must not be public

Dependency Rules

Checking Dependencies:

// Should not depend on
.ShouldNot()
    .HaveDependencyOn("Forbidden.Assembly")

// Should not depend on any
.ShouldNot()
    .HaveDependencyOnAny("Assembly1", "Assembly2")

// Should depend on
.Should()
    .HaveDependencyOn("Required.Assembly")

// Should depend on any
.Should()
    .HaveDependencyOnAny("Assembly1", "Assembly2")

Dependency Matching:

// By assembly name
.HaveDependencyOn("MyAssembly")

// By fully qualified type name
.HaveDependencyOn(typeof(MyClass).AssemblyQualifiedName)

// By namespace
.HaveDependencyOn("My.Namespace")

Getting Results

Executing Queries:

// Get result
var result = types
    .Should()
    .NotHaveDependencyOn("Forbidden.Assembly")
    .GetResult();

// Check success
bool isSuccessful = result.IsSuccessful;

// Get failing types
var failingTypes = result.FailingTypes;

// Get passing types
var passingTypes = result.PassingTypes;

Result Information:

public class TestResult
{
    public bool IsSuccessful { get; }
    public IEnumerable<Type> FailingTypes { get; }
    public IEnumerable<Type> PassingTypes { get; }
    public string ErrorMessage { get; }
}

Writing Effective Architecture Tests

Test Structure

Follow AAA Pattern:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void YourArchitectureRule()
{
    // Arrange: Set up test data and queries
    var typesToTest = Types.InAssembly(typeof(MyClass).Assembly)
        .That()
        .ResideInNamespace("My.Namespace");

    // Act: Execute the rule
    var result = typesToTest
        .Should()
        .NotHaveDependencyOn("Forbidden.Assembly")
        .GetResult();

    // Assert: Validate the result
    Assert.IsTrue(
        result.IsSuccessful, 
        $"The following types violate the rule: {string.Join(", ", result.FailingTypes)}");
}

Descriptive Test Names

Use Descriptive Names:

// ✅ GOOD - Clear and descriptive
public void ControllersShouldNotDirectlyReferencePersistenceModel()
public void DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations()
public void OptionsShouldBeSealedClasses()

// ❌ BAD - Vague
public void TestDependencies()
public void ValidateOptions()

Comprehensive Error Messages

Include Failing Types in Messages:

// ✅ GOOD - Includes failing types
Assert.IsTrue(
    result.IsSuccessful, 
    $"The following types violate the rule: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");

// ❌ BAD - Generic message
Assert.IsTrue(result.IsSuccessful);

Conditional Compilation

Handle Optional Features:

#if UseMongoDb
    string? mongoDbRepositoriesAssemblyName = typeof(PersistenceModel.MongoDb.Repositories.MicroserviceAggregateRootsMongoDbRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(mongoDbRepositoriesAssemblyName))
    {
        repositoriesDependencies.Add(mongoDbRepositoriesAssemblyName);
    }
#endif

#if UseNHibernate
    string? nhibernateRepositoriesAssemblyName = typeof(PersistenceModel.NHibernate.Repositories.MicroserviceAggregateRootsRepository).AssemblyQualifiedName;
    if (!string.IsNullOrEmpty(nhibernateRepositoriesAssemblyName))
    {
        repositoriesDependencies.Add(nhibernateRepositoriesAssemblyName);
    }
#endif

Organize by Concern:

[TestClass]
public class EnforcingLayeredArchitectureUnitTests
{
    // All layered architecture tests
}

[TestClass]
public class OptionsArchitectureUnitTests
{
    // All options-related tests
}

Running Architecture Tests

Local Execution

Run All Architecture Tests:

dotnet test --filter "TestCategory=Architecture Unit Tests"

Run Specific Test Class:

dotnet test --filter "FullyQualifiedName~EnforcingLayeredArchitectureUnitTests"

Run Specific Test:

dotnet test --filter "FullyQualifiedName~ControllersShouldNotDirectlyReferencePersistenceModel"

Verbose Output:

dotnet test --filter "TestCategory=Architecture Unit Tests" --logger "console;verbosity=detailed"

CI/CD Integration

Azure DevOps Pipeline:

- task: DotNetCoreCLI@2
  displayName: 'Run Architecture Tests'
  inputs:
    command: 'test'
    projects: '**/*ArchitectureTests.csproj'
    arguments: '--filter "TestCategory=Architecture Unit Tests" --logger trx --results-directory $(Agent.TempDirectory)'

GitHub Actions:

- name: Run Architecture Tests
  run: |
    dotnet test --filter "TestCategory=Architecture Unit Tests" --logger "trx;LogFileName=architecture-tests.trx"

Benefits: - Build-Time Validation: Fail builds on violations - Early Detection: Catch issues before code review - Continuous Enforcement: Validate on every commit - Test Reports: Generate test result files for analysis

Debugging Failed Tests

Understanding Test Failures

Common Failure Scenarios:

  1. False Positives: Test is too strict
  2. Legitimate Violations: Code violates architectural rule
  3. Missing Dependencies: Test doesn't account for all cases
  4. Conditional Compilation: Test doesn't handle #if directives

Debugging Techniques

1. Inspect Failing Types:

var result = types
    .Should()
    .NotHaveDependencyOn("Forbidden.Assembly")
    .GetResult();

if (!result.IsSuccessful)
{
    foreach (var failingType in result.FailingTypes)
    {
        Console.WriteLine($"Failing type: {failingType.FullName}");
        // Inspect dependencies
        var dependencies = failingType.GetReferencedAssemblies();
        foreach (var dep in dependencies)
        {
            Console.WriteLine($"  Depends on: {dep.Name}");
        }
    }
}

2. Use Detailed Output:

dotnet test --filter "TestCategory=Architecture Unit Tests" --logger "console;verbosity=detailed"

3. Add Debugging Code:

var typesToTest = Types.InAssembly(typeof(MyClass).Assembly)
    .That()
    .ResideInNamespace("My.Namespace")
    .GetTypes(); // Get all matching types first

Console.WriteLine($"Found {typesToTest.Count()} types to test");
foreach (var type in typesToTest)
{
    Console.WriteLine($"  - {type.FullName}");
}

Fixing Violations

Example: Controller Violation:

// ❌ BAD - Violates architecture rule
public class MyController : ControllerBase
{
    private readonly IMicroserviceAggregateRootsRepository _repository; // Violation!

    public MyController(IMicroserviceAggregateRootsRepository repository)
    {
        _repository = repository;
    }
}

// ✅ GOOD - Complies with architecture rule
public class MyController : ControllerBase
{
    private readonly IMicroserviceAggregateRootsRetriever _retriever; // Correct!
    private readonly IMicroserviceAggregateRootsProcessor _processor; // Correct!

    public MyController(
        IMicroserviceAggregateRootsRetriever retriever,
        IMicroserviceAggregateRootsProcessor processor)
    {
        _retriever = retriever;
        _processor = processor;
    }
}

Additional Architecture Test Patterns

Naming Conventions Tests

Pattern: Validate consistent naming conventions across the solution.

Repository Naming:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryInterfacesShouldEndWithRepository()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
        .Should()
        .HaveNameEndingWith("Repository", StringComparison.Ordinal)
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Repository interfaces not ending with 'Repository': {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Use Case Naming:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCaseInterfacesShouldEndWithUseCase()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
        .And()
        .ImplementInterface(typeof(IUseCase<,>))
        .Should()
        .HaveNameEndingWith("UseCase", StringComparison.Ordinal)
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Use case interfaces must end with 'UseCase': {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Domain Event Pattern Tests

Pattern: Validate domain events are in correct layers and don't depend on infrastructure.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainEventsShouldBeInDomainLayer()
{
    var domainEvents = Types.InCurrentDomain()
        .That()
        .AreClasses()
        .And()
        .HaveNameEndingWith("Event", StringComparison.Ordinal)
        .GetTypes()
        .Where(type =>
        {
            var ns = type.Namespace ?? string.Empty;
            return ns.StartsWith("ConnectSoft.MicroserviceTemplate.EntityModel", StringComparison.Ordinal) ||
                   ns.StartsWith("ConnectSoft.MicroserviceTemplate.DomainModel", StringComparison.Ordinal);
        })
        .ToList();

    Assert.IsGreaterThanOrEqualTo(0, domainEvents.Count, 
        $"Found {domainEvents.Count} domain events in EntityModel or DomainModel");
}

Domain Service Pattern Tests

Pattern: Validate domain services implement interfaces and follow isolation rules.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainServiceInterfacesShouldImplementIDomainService()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
        .Should()
        .ImplementInterface(typeof(IDomainService))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Domain service interfaces must implement IDomainService: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Domain Service Isolation:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void DomainServicesShouldNotDependOnInfrastructure()
{
    var domainModelImplAssembly = typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly;

#if UseMongoDb
    var mongoDbResult = Types.InAssembly(domainModelImplAssembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel.Impl")
        .And()
        .ImplementInterface(typeof(IDomainService))
        .ShouldNot()
        .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb")
        .GetResult();

    Assert.IsTrue(mongoDbResult.IsSuccessful, 
        $"Domain services should not depend on MongoDB: {string.Join(", ", mongoDbResult.FailingTypes.Select(t => t.FullName))}");
#endif
}

Use Case Pattern Tests

Pattern: Validate use cases follow the pattern with Input/Output types.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCasesShouldHaveCorrespondingInputOutputClasses()
{
    var useCaseInterfaces = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ImplementInterface(typeof(IUseCase<,>))
        .GetTypes();

    foreach (var useCaseInterface in useCaseInterfaces)
    {
        var genericArgs = useCaseInterface.GetGenericArguments();
        if (genericArgs.Length == 2)
        {
            var inputType = genericArgs[0];
            var outputType = genericArgs[1];

            if (!inputType.Name.EndsWith("Input", StringComparison.Ordinal))
            {
                Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that does not end with 'Input'");
            }

            if (!outputType.Name.EndsWith("Output", StringComparison.Ordinal))
            {
                Assert.Fail($"Use case {useCaseInterface.Name} has output type {outputType.Name} that does not end with 'Output'");
            }
        }
    }
}

Repository Pattern Tests

Pattern: Validate repository interfaces and implementations follow the pattern.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryInterfacesShouldInheritFromIGenericRepository()
{
    var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
        .Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
                   !a.GetName().Name.Contains("Test"))
        .ToList();

    var result = Types.InAssemblies(allAssemblies)
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
        .And()
        .HaveNameEndingWith("Repository", StringComparison.Ordinal)
        .Should()
        .ImplementInterface(typeof(IGenericRepository<,>))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Repository interfaces must inherit from IGenericRepository: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Repository Implementation Validation:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoryImplementationsShouldImplementInterfaces()
{
    var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
        .Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
                   !a.GetName().Name.Contains("Test"))
        .ToList();

    var repositoryInterfaces = Types.InAssemblies(allAssemblies)
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Repositories")
        .And()
        .HaveNameEndingWith("Repository", StringComparison.Ordinal)
        .GetTypes()
        .ToList();

#if UseMongoDb
    var mongoDbRepositories = Types.InAssemblies(allAssemblies)
        .That()
        .AreClasses()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.MongoDb.Repositories")
        .And()
        .HaveNameEndingWith("Repository", StringComparison.Ordinal)
        .GetTypes();

    foreach (var repository in mongoDbRepositories)
    {
        var implementsRepositoryInterface = repositoryInterfaces.Any(iface => 
            iface.IsAssignableFrom(repository));

        if (!implementsRepositoryInterface)
        {
            Assert.Fail($"MongoDB repository {repository.Name} should implement a repository interface");
        }
    }
#endif
}

Messaging Model Tests

Pattern: Validate messaging model (commands and events) follow conventions.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void CommandsShouldImplementICommand()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreClasses()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.MessagingModel")
        .And()
        .HaveNameEndingWith("Command", StringComparison.Ordinal)
        .Should()
        .ImplementInterface(typeof(ICommand))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Command classes must implement ICommand: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Message Isolation:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void MessagesShouldNotHaveInfrastructureDependencies()
{
    var serviceModelResult = Types.InCurrentDomain()
        .That()
        .AreClasses()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.MessagingModel")
        .And()
        .ImplementInterface(typeof(ICommand))
        .Or()
        .ImplementInterface(typeof(IEvent))
        .ShouldNot()
        .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
        .GetResult();

    Assert.IsTrue(serviceModelResult.IsSuccessful, 
        $"Messages should not depend on ServiceModel: {string.Join(", ", serviceModelResult.FailingTypes.Select(t => t.FullName))}");
}

Entity Model Isolation Tests

Pattern: Validate entity model isolation from infrastructure.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void EntityInterfacesShouldImplementIGenericEntity()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.EntityModel")
        .And()
        .HaveNameEndingWith("Entity", StringComparison.Ordinal)
        .Should()
        .ImplementInterface(typeof(IGenericEntity<>))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Entity interfaces should implement IGenericEntity: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Specification Pattern Tests

Pattern: Validate specification pattern implementation.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void SpecificationInterfacesShouldImplementISpecification()
{
    var result = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel.Specifications")
        .Should()
        .ImplementInterface(typeof(ISpecification<,>))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Specification interfaces must implement ISpecification: {string.Join(", ", result.FailingTypes.Select(t => t.FullName))}");
}

Cross-Cutting Concerns Tests

Pattern: Validate cross-cutting concerns follow conventions.

Extension Methods:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ExtensionMethodsShouldBeInternalAndStatic()
{
    var allAssemblies = AppDomain.CurrentDomain.GetAssemblies()
        .Where(a => a.GetName().Name?.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal) == true &&
                   !a.GetName().Name.Contains("Test"))
        .ToList();

    var extensionClasses = Types.InAssemblies(allAssemblies)
        .That()
        .AreClasses()
        .And()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.ApplicationModel")
        .And()
        .HaveNameEndingWith("Extensions", StringComparison.Ordinal)
        .GetTypes();

    foreach (var extensionClass in extensionClasses)
    {
        var methods = extensionClass.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
            .Where(m => m.IsStatic &&
                        m.GetParameters().Length > 0 &&
                        m.GetParameters()[0].GetCustomAttribute<ExtensionAttribute>() != null);

        foreach (var method in methods)
        {
            if (method.IsPublic)
            {
                Assert.Fail($"Extension method {method.Name} in {extensionClass.Name} should be internal, not public");
            }
        }
    }
}

Advanced Patterns

Testing Multiple Rules

Combine Multiple Rules in One Test:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void LowerLayersShouldNotReferenceUpperLayers()
{
    // Test multiple relationships
    var results = new List<bool>
    {
        // Domain model should not reference service model
        Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
            .That()
            .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
            .ShouldNot()
            .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
            .GetResult()
            .IsSuccessful,

        // Persistence model should not reference service model
        Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
            .That()
            .ResideInNamespace("ConnectSoft.MicroserviceTemplate.PersistenceModel")
            .ShouldNot()
            .HaveDependencyOn("ConnectSoft.MicroserviceTemplate.ServiceModel")
            .GetResult()
            .IsSuccessful
    };

    Assert.IsTrue(results.All(r => r), "One or more layer dependency rules violated");
}

Testing Interface Implementation

Validate Interface Implementations:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void RepositoriesShouldImplementIRepository()
{
    var result = Types.InAssembly(typeof(IMicroserviceAggregateRootsRepository).Assembly)
        .That()
        .HaveNameEndingWith("Repository")
        .And()
        .AreClasses()
        .Should()
        .ImplementInterface(typeof(IMicroserviceAggregateRootsRepository))
        .GetResult();

    Assert.IsTrue(result.IsSuccessful, 
        $"Repositories must implement IMicroserviceAggregateRootsRepository: {string.Join(", ", result.FailingTypes)}");
}

Testing Naming Conventions

Validate Naming Patterns:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ProcessorsShouldEndWithProcessor()
{
    var result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsProcessor).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel.Impl")
        .And()
        .ImplementInterface(typeof(IMicroserviceAggregateRootsProcessor))
        .Should()
        .HaveNameEndingWith("Processor")
        .GetResult();

    Assert.IsTrue(result.IsSuccessful);
}

Testing with Multiple Assemblies

Scan Across Solution:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void InterfacesShouldStartWithIPrefix()
{
    // Get all ConnectSoft.MicroserviceTemplate assemblies
    var interfaces = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .GetTypes()
        .Where(type =>
        {
            var ns = type.Namespace ?? string.Empty;
            return ns.StartsWith("ConnectSoft.MicroserviceTemplate", StringComparison.Ordinal);
        })
        .ToList();

    var failingTypes = interfaces
        .Where(i => !i.Name.StartsWith("I", StringComparison.Ordinal))
        .ToList();

    Assert.IsTrue(
        failingTypes.Count == 0,
        $"Interfaces not starting with 'I': {string.Join(", ", failingTypes.Select(t => t.FullName))}");
}

Testing Generic Type Arguments

Validate Generic Type Constraints:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void UseCasesShouldHaveCorrespondingInputOutputClasses()
{
    var useCaseInterfaces = Types.InCurrentDomain()
        .That()
        .AreInterfaces()
        .And()
        .ImplementInterface(typeof(IUseCase<,>))
        .GetTypes();

    foreach (var useCaseInterface in useCaseInterfaces)
    {
        var genericArgs = useCaseInterface.GetGenericArguments();
        if (genericArgs.Length == 2)
        {
            var inputType = genericArgs[0];
            var outputType = genericArgs[1];

            // Validate naming conventions
            if (!inputType.Name.EndsWith("Input", StringComparison.Ordinal))
            {
                Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that does not end with 'Input'");
            }

            if (!outputType.Name.EndsWith("Output", StringComparison.Ordinal))
            {
                Assert.Fail($"Use case {useCaseInterface.Name} has output type {outputType.Name} that does not end with 'Output'");
            }

            // Validate namespace
            if (!inputType.Namespace?.StartsWith("ConnectSoft.MicroserviceTemplate.DomainModel", StringComparison.Ordinal) ?? false)
            {
                Assert.Fail($"Use case {useCaseInterface.Name} has input type {inputType.Name} that is not in DomainModel namespace");
            }
        }
    }
}

Best Practices

Do's

  1. Write Tests for All Architectural Rules

    // ✅ GOOD - Every architectural rule has a test
    [TestMethod]
    public void ControllersShouldNotDirectlyReferencePersistenceModel()
    

  2. Use Descriptive Test Names

    // ✅ GOOD - Clear what the test validates
    public void DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations()
    

  3. Include Failing Types in Error Messages

    // ✅ GOOD - Helps debug failures
    Assert.IsTrue(result.IsSuccessful, 
        $"Violating types: {string.Join(", ", result.FailingTypes)}");
    

  4. Handle Conditional Compilation

    // ✅ GOOD - Accounts for optional features
    #if UseMongoDb
        // MongoDB-specific test logic
    #endif
    

  5. Group Related Tests

    // ✅ GOOD - Logical organization
    [TestClass]
    public class EnforcingLayeredArchitectureUnitTests
    

Don'ts

  1. Don't Skip Architecture Tests

    // ❌ BAD - Never skip architecture validation
    [TestMethod]
    [Ignore] // Never!
    public void ControllersShouldNotDirectlyReferencePersistenceModel()
    

  2. Don't Make Tests Too Permissive

    // ❌ BAD - Too specific, misses violations
    Types.InAssembly(assembly)
        .That()
        .HaveName("MyController") // Too specific!
        .ShouldNot()
        .HaveDependencyOn("Repository")
    
    // ✅ GOOD - Comprehensive coverage
    Types.InAssembly(assembly)
        .That()
        .ResideInNamespace("Controllers")
        .ShouldNot()
        .HaveDependencyOnAny(allRepositoryAssemblies)
    

  3. Don't Ignore Test Failures

    # ❌ BAD - Hiding violations
    dotnet test --filter "TestCategory=Architecture Unit Tests" || true
    
    # ✅ GOOD - Fail builds on violations
    dotnet test --filter "TestCategory=Architecture Unit Tests"
    

  4. Don't Use Generic Error Messages

    // ❌ BAD - Not helpful
    Assert.IsTrue(result.IsSuccessful);
    
    // ✅ GOOD - Informative
    Assert.IsTrue(result.IsSuccessful, 
        "Controllers must not depend on persistence model. Violating types: " + 
        string.Join(", ", result.FailingTypes));
    

Integration with Clean Architecture

Enforcing Clean Architecture Rules

Architecture tests enforce Clean Architecture principles:

  1. Dependency Rule: Dependencies flow inward (outer → inner)
  2. Independence Rule: Inner layers don't depend on outer layers
  3. Interface Rule: Dependencies point toward abstractions
  4. Framework Independence: Domain layer has no framework dependencies

Layer Validation

Validate Each Layer:

// Domain Layer: No dependencies on infrastructure
[TestMethod]
public void DomainLayerShouldNotDependOnInfrastructure()
{
    var result = Types.InAssembly(typeof(MicroserviceAggregateRootEntity).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.EntityModel")
        .ShouldNot()
        .HaveDependencyOnAny(
            "NHibernate",
            "MongoDB",
            "Microsoft.AspNetCore")
        .GetResult();

    Assert.IsTrue(result.IsSuccessful);
}

// Application Layer: Only depends on Domain
[TestMethod]
public void ApplicationLayerShouldOnlyDependOnDomain()
{
    var result = Types.InAssembly(typeof(DefaultMicroserviceAggregateRootsRetriever).Assembly)
        .That()
        .ResideInNamespace("ConnectSoft.MicroserviceTemplate.DomainModel")
        .ShouldNot()
        .HaveDependencyOnAny(
            "ConnectSoft.MicroserviceTemplate.ServiceModel",
            "NHibernate",
            "MongoDB")
        .GetResult();

    Assert.IsTrue(result.IsSuccessful);
}

Troubleshooting

Issue: Test Fails Unexpectedly

Symptom: Test fails but code appears correct.

Solutions: 1. Check if test is too strict 2. Verify conditional compilation directives 3. Inspect FailingTypes to see which types fail 4. Check for transitive dependencies

Issue: Test Too Permissive

Symptom: Test passes but violations exist.

Solutions: 1. Review test logic for gaps 2. Add more specific conditions 3. Check for missing forbidden dependencies 4. Verify namespace matching

Issue: Performance Issues

Symptom: Tests run slowly.

Solutions: 1. Narrow scope with namespace filtering 2. Cache type queries if used multiple times 3. Use specific type filters instead of broad queries 4. Consider splitting large tests into smaller ones

Complete Test Coverage

Test Categories Summary

Category Test Count Purpose
Layered Architecture 8 tests Enforce Clean Architecture dependency rules
Options Pattern 4 tests Validate options implementation
Naming Conventions 9 tests Ensure consistent naming across solution
Domain Events 2 tests Validate domain event placement and isolation
Domain Services 4 tests Enforce domain service pattern
Use Cases 5 tests Validate use case pattern with Input/Output
Repository Pattern 4 tests Enforce repository interfaces and implementations
Messaging Model 6 tests Validate commands and events
Cross-Cutting Concerns 4 tests Validate exceptions, extensions, constants
Entity Model Isolation 5 tests Ensure entity model purity
Application Model Isolation 2 tests Validate application model isolation
Specification Pattern 3 tests Enforce specification pattern

Total: 56+ architecture tests covering all major architectural concerns.

Test Execution Statistics

Running all architecture tests:

# Run all architecture tests
dotnet test --filter "TestCategory=Architecture Unit Tests"

# Expected output:
# Passed!  - Failed:     0, Passed:    56, Skipped:     0, Total:    56

Summary

Architecture Tests in the ConnectSoft Microservice Template provide:

  • Build-Time Enforcement: Architectural violations fail builds automatically
  • Living Documentation: Tests serve as executable documentation of rules
  • Prevent Architectural Drift: Catch violations before they spread
  • Team Alignment: Shared understanding of architectural constraints
  • Refactoring Safety: Confidence when refactoring
  • CI/CD Integration: Automated validation in every build
  • Comprehensive Coverage: 56+ tests covering all architectural concerns
  • Pattern Enforcement: Tests for Clean Architecture, DDD, Repository, Specification, and Use Case patterns
  • Naming Consistency: Automated validation of naming conventions
  • Layer Isolation: Strict enforcement of dependency rules
  • Interface Compliance: Validation of interface implementations

By following these patterns, teams can:

  • Maintain Clean Architecture: Automated enforcement prevents violations
  • Catch Issues Early: Build-time validation catches problems before code review
  • Document Architecture: Tests serve as living documentation
  • Enable Refactoring: Confidence to refactor knowing architecture is protected
  • Enforce Consistency: Consistent architecture across all projects
  • Prevent Technical Debt: Architectural violations are caught immediately
  • Onboard New Developers: Tests serve as examples of architectural rules

The Architecture Tests ensure that architectural principles are not just documented but actively enforced, preventing architectural degradation over time and maintaining the integrity of Clean Architecture throughout the microservice's lifecycle. With 56+ tests covering layered architecture, naming conventions, design patterns, and isolation rules, the template provides comprehensive architectural validation that scales with the codebase.