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:
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¶
-
Write Architecture Tests for All Rules
-
Run Tests in CI/CD
-
Provide Clear Error Messages
-
Group Related Rules
Don'ts¶
-
Don't Skip Architecture Tests
-
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) -
Don't Ignore Test Failures
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.
Related Documentation¶
- Clean Architecture: Architectural principles and patterns
- Domain-Driven Design: Domain layer organization
- Solution Structure: Template solution structure and project organization
- Architecture Tests: General architecture testing patterns and best practices