Skip to content

Microservice Template Architecture

Architecture Model

The Architecture Model in the ConnectSoft Microservice Template provides both visual representation and automated enforcement of the microservice's architectural structure and dependency rules. It encompasses Visual Studio architecture modeling tools (Code Maps) for visualization and automated architecture tests using NetArchTest.Rules to enforce Clean Architecture principles at build time.

The Architecture Model ensures:

  • Visual Documentation: Code Maps (DGML) provide visual representation of solution structure and dependencies
  • Automated Enforcement: Architecture tests prevent dependency violations automatically
  • Build-Time Validation: Architectural violations are caught during CI/CD builds
  • Design Intent: Architecture rules are codified as executable tests
  • Documentation: Architecture tests serve as living documentation of architectural constraints

Architecture Model Philosophy

Architecture is not just a design—it's a contract that must be enforced. The Architecture Model codifies architectural rules as executable tests, ensuring that Clean Architecture principles are maintained automatically, preventing architectural drift and preserving design intent over time.

Architecture Model Components

Architecture Model
├── ArchitectureModel Project
│   ├── CodeMap.dgml (Visual Studio Code Map)
│   └── ArchitectureModel.modelproj (Architecture Modeling Project)
└── ArchitectureTests Project
    ├── EnforcingLayeredArchitectureUnitTests.cs
    └── OptionsArchitectureUnitTests.cs

Two-Pronged Approach

1. Visual Architecture Modeling (ArchitectureModel Project) - Visual Studio Code Maps for dependency visualization - Architecture diagrams for documentation - Interactive exploration of solution structure

2. Automated Architecture Testing (ArchitectureTests Project) - NetArchTest.Rules for dependency rule enforcement - Build-time validation of architectural constraints - CI/CD integration for continuous validation

ArchitectureModel Project

Purpose

The ArchitectureModel project contains Visual Studio architecture modeling files that provide visual representation of the solution's architecture and dependencies.

Project Structure

ConnectSoft.MicroserviceTemplate.ArchitectureModel/
├── ConnectSoft.MicroserviceTemplate.CodeMap.dgml
└── ConnectSoft.MicroserviceTemplate.ArchitectureModel.modelproj

CodeMap.dgml

Purpose: Visual Studio Code Map (DGML) file that visualizes solution structure and dependencies.

Features: - Dependency Visualization: Visual representation of project dependencies - Assembly Relationships: Shows how assemblies reference each other - Namespace Organization: Displays namespace structure - Interactive Exploration: Click-through navigation in Visual Studio

Usage: 1. Open CodeMap.dgml in Visual Studio 2. View dependency relationships between projects 3. Explore assembly references 4. Identify potential architectural violations visually

ArchitectureModel.modelproj

Purpose: Visual Studio Architecture Modeling project file that defines the architecture modeling project.

Usage: - Visual Studio architecture modeling tools - Code map generation - Dependency analysis - Architecture documentation

ArchitectureTests Project

Purpose

The ArchitectureTests project contains automated tests that enforce architectural rules using NetArchTest.Rules, ensuring Clean Architecture principles are maintained automatically.

Project Structure

ConnectSoft.MicroserviceTemplate.ArchitectureTests/
├── EnforcingLayeredArchitectureUnitTests.cs
├── OptionsArchitectureUnitTests.cs
└── ConnectSoft.MicroserviceTemplate.ArchitectureTests.csproj

NetArchTest.Rules

Library: NetArchTest.Rules is used for architecture testing.

Features: - Fluent API: Expressive syntax for defining architecture rules - Type-Based Testing: Test assemblies, namespaces, and types - Dependency Analysis: Validate dependency relationships - Build Integration: Fail builds on architectural violations

Installation:

<PackageReference Include="NetArchTest.Rules" />

Architecture Enforcement Tests

Controllers Should Not Directly Reference Persistence Model

Purpose: Ensures REST controllers cannot directly access repositories, enforcing Clean Architecture boundaries.

// EnforcingLayeredArchitectureUnitTests.cs
[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void ControllersShouldNotDirectlyReferencePersistenceModel()
{
    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

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

    Assert.IsTrue(result);
}

What It Validates: - Controllers cannot reference IMicroserviceAggregateRootsRepository assembly - Controllers cannot reference concrete repository implementations (NHibernate, MongoDB) - Controllers must use Application Layer (Processors/Retrievers) instead

Why It Matters: - Enforces Clean Architecture boundaries - Prevents direct database access from controllers - Ensures controllers remain thin and delegate to Application Layer

Controllers Should Not Depend on Domain Model Implementations

Purpose: Ensures controllers only depend on Application Layer interfaces, not domain model implementations.

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

    // Arrange
    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");
}

What It Validates: - Controllers cannot directly reference DefaultMicroserviceAggregateRootsRetriever - Controllers cannot depend on domain model implementation classes - Controllers must depend on interfaces defined in DomainModel project

Why It Matters: - Enforces dependency inversion principle - Keeps controllers decoupled from implementation details - Enables easy swapping of implementations

gRPC Services Should Not Directly Reference Persistence Model

Purpose: Ensures gRPC services follow the same Clean Architecture rules as REST controllers.

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void GrpcServicesShouldNotDirectlyReferencePersistenceModel()
{
    Type grpcType = typeof(GrpcMicroserviceAggregateRootQueryService);
    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

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

    Assert.IsTrue(result);
}

What It Validates: - gRPC services cannot directly reference repositories - gRPC services must use Application Layer (Processors/Retrievers) - Consistent architecture enforcement across all service types

Domain Model Implementors Should Reference Only Persistence Model Contracts

Purpose: Ensures Application Layer (DomainModel.Impl) only depends on repository 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: DomainModel.Impl should NOT depend on concrete repositories
    bool result = Types.InAssembly(domainModelImplType.Assembly)
        .That()
        .ResideInNamespace(domainModelImplType.Namespace)
        .ShouldNot()
        .HaveDependencyOnAny(concreteRepositoriesDependencies.ToArray())
        .GetResult()
        .IsSuccessful;

    Assert.IsTrue(result);

    // Assert: DomainModel.Impl SHOULD depend on repository 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);
}

What It Validates: - Application Layer cannot reference concrete repository implementations - Application Layer must depend on repository interfaces only - Application Layer correctly depends on IMicroserviceAggregateRootsRepository

Why It Matters: - Enforces dependency inversion principle - Enables swapping persistence implementations (NHibernate ↔ MongoDB) - Keeps Application Layer framework-agnostic

Lower Layers Should Not Reference Upper Layers

Purpose: Ensures dependency flow is unidirectional (outer → inner), preventing circular dependencies.

[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);
}

What It Validates: - Domain Model cannot reference Service Model - Persistence Model cannot reference Service Model - Persistence Model cannot reference Domain Model - Dependency flow is unidirectional (outer → inner)

Why It Matters: - Prevents circular dependencies - Maintains Clean Architecture dependency rules - Ensures inner layers remain independent

Options Architecture Tests

Options Should Be Implemented as Classes

Purpose: Ensures all options are implemented as classes (not interfaces or structs).

[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);
}

What It Validates: - All types ending with "Options" in the Options namespace are classes - Options are not interfaces or structs - Consistent options implementation pattern

Options Should Be Sealed Classes

Purpose: Ensures options classes are sealed to prevent inheritance.

[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);
}

What It Validates: - Options classes are sealed (except validators and base classes) - Prevents inheritance of options classes - Enforces immutability and consistency

Running Architecture Tests

Local Execution

Run architecture tests locally:

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

# Run specific test class
dotnet test --filter "FullyQualifiedName~EnforcingLayeredArchitectureUnitTests"

# Run with detailed output
dotnet test --filter "TestCategory=Architecture Unit Tests" --logger "console;verbosity=detailed"

CI/CD Integration

Architecture tests run automatically in CI/CD pipelines:

# azure-pipelines.yml
- 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)'

Benefits: - Build-Time Validation: Architectural violations fail the build - Early Detection: Issues caught before code review - Continuous Enforcement: Rules enforced on every commit

Architecture Rules Summary

Dependency Rules Enforced

Rule Test Purpose
Controllers → Persistence ControllersShouldNotDirectlyReferencePersistenceModel Controllers cannot access repositories directly
Controllers → Domain Impl ControllersShouldNotDependOnAnyDomainModelClassServiceImplementation Controllers depend on interfaces, not implementations
gRPC → Persistence GrpcServicesShouldNotDirectlyReferencePersistenceModel gRPC services cannot access repositories directly
gRPC → Domain Impl GrpcServicesShouldNotDependOnAnyDomainModelClassServiceImplementation gRPC services depend on interfaces
Domain Impl → Persistence Impl DomainModelImplementorsShouldReferenceOnlyPersistenceModelContractsAndNotImplementations Application Layer uses interfaces only
Lower → Upper Layers LowerLayersShouldNotReferenceUpperLayers Dependency flow is unidirectional

Options Rules Enforced

Rule Test Purpose
Options as Classes OptionsShouldBeImplementedAsClasses Options are classes, not interfaces/structs
Options Sealed OptionsShouldBeSealedClasses Options classes are sealed

Extending Architecture Tests

Adding New Architecture Rules

Create new architecture tests following the pattern:

[TestMethod]
[TestCategory("Architecture Unit Tests")]
public void YourCustomArchitectureRule()
{
    // Arrange
    var typesToTest = Types.InAssembly(typeof(YourType).Assembly)
        .That()
        .ResideInNamespace("Your.Namespace")
        .And()
        .AreClasses();

    // Act
    var result = typesToTest
        .Should()
        .HaveDependencyOn("Expected.Dependency")
        .Or()
        .Should()
        .NotHaveDependencyOn("Forbidden.Dependency")
        .GetResult();

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

NetArchTest.Rules Patterns

Common Patterns:

// Test assembly dependencies
Types.InAssembly(typeof(MyClass).Assembly)
    .That()
    .ResideInNamespace("My.Namespace")
    .ShouldNot()
    .HaveDependencyOn("Forbidden.Assembly")
    .GetResult();

// Test type inheritance
Types.InAssembly(typeof(MyClass).Assembly)
    .That()
    .Inherit(typeof(MyBaseClass))
    .Should()
    .BeSealed()
    .GetResult();

// Test interface implementation
Types.InAssembly(typeof(MyClass).Assembly)
    .That()
    .ImplementInterface(typeof(IMyInterface))
    .Should()
    .BePublic()
    .GetResult();

// Test naming conventions
Types.InAssembly(typeof(MyClass).Assembly)
    .That()
    .HaveNameEndingWith("Repository")
    .Should()
    .ImplementInterface(typeof(IRepository))
    .GetResult();

Best Practices

Do's

  1. Write Architecture Tests for All Rules

    // ✅ GOOD - Architecture rule codified as test
    [TestMethod]
    public void ControllersShouldNotDirectlyReferencePersistenceModel()
    {
        // Test implementation
    }
    

  2. Run Tests in CI/CD

    # ✅ GOOD - Architecture tests in pipeline
    - task: DotNetCoreCLI@2
      inputs:
        command: 'test'
        arguments: '--filter "TestCategory=Architecture Unit Tests"'
    

  3. Provide Clear Error Messages

    // ✅ GOOD - Descriptive assertion message
    Assert.IsTrue(result.IsSuccessful, 
        "Web API controllers must * not * depend on any domain model service implementation");
    

  4. Group Related Rules

    // ✅ GOOD - Related tests in same class
    [TestClass]
    public class EnforcingLayeredArchitectureUnitTests
    {
        // All layered architecture tests
    }
    

Don'ts

  1. Don't Skip Architecture Tests

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

  2. Don't Make Tests Too Permissive

    // ❌ BAD - Too permissive, allows violations
    Types.InAssembly(assembly)
        .That()
        .ResideInNamespace("Controllers")
        .ShouldNot()
        .HaveDependencyOn("Some.Repository") // Too specific
    
    // ✅ GOOD - Comprehensive, prevents all violations
    Types.InAssembly(assembly)
        .That()
        .ResideInNamespace("Controllers")
        .ShouldNot()
        .HaveDependencyOnAny(allRepositoryAssemblies)
    

  3. Don't Ignore Test Failures

    # ❌ BAD - Ignoring architecture test failures
    dotnet test --filter "TestCategory=Architecture Unit Tests" || true
    
    # ✅ GOOD - Fail build on architecture violations
    dotnet test --filter "TestCategory=Architecture Unit Tests"
    

Visual Studio Code Maps

Generating Code Maps

Visual Studio: 1. Right-click on solution/project 2. Select "Generate Code Map for Solution" 3. View dependency relationships 4. Export as DGML file

Usage: - Dependency Analysis: Visualize project dependencies - Architecture Review: Review architecture during code review - Documentation: Include code maps in architecture documentation - Onboarding: Help new team members understand structure

Code Map Features

Interactive Exploration: - Click nodes to expand/collapse - Filter by namespace, type, or assembly - Search for specific types or namespaces - Highlight dependencies

Export Options: - Export as DGML (for version control) - Export as image (PNG, SVG) - Share with team members

Troubleshooting

Issue: Architecture Test Fails

Symptom: Architecture test fails with dependency violation.

Solutions: 1. Review test failure message to identify violating types 2. Check test code to understand the rule being enforced 3. Refactor code to comply with architecture rules 4. If exception is needed, document reason and consider updating rules

Example Fix:

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

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

Issue: False Positives

Symptom: Architecture test fails but code is actually correct.

Solutions: 1. Review test logic for errors 2. Check if test is too restrictive 3. Update test to handle edge cases 4. Document exception if legitimate

Issue: Code Map Not Updating

Symptom: Code Map shows outdated dependencies.

Solutions: 1. Rebuild solution 2. Regenerate Code Map 3. Clear Visual Studio cache 4. Restart Visual Studio

Summary

The Architecture Model in the ConnectSoft Microservice Template provides:

  • Visual Documentation: Code Maps for dependency visualization
  • Automated Enforcement: Architecture tests prevent dependency violations
  • Build-Time Validation: Architectural violations fail builds automatically
  • Design Intent: Architecture rules codified as executable tests
  • Living Documentation: Tests serve as documentation of architectural constraints
  • CI/CD Integration: Continuous validation of architecture rules
  • Extensibility: Easy to add new architecture rules

By following these patterns, teams can:

  • Maintain Clean Architecture: Automated enforcement prevents architectural drift
  • Catch Violations Early: Build-time validation catches issues before code review
  • Document Architecture: Tests and code maps serve as living documentation
  • Enforce Consistency: Consistent architecture across all projects
  • Enable Refactoring: Confidence to refactor knowing architecture is protected

The Architecture Model ensures 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.