Introduction
AI coding assistants can generate a large amount of .NET code in a short time. They can create controllers, services, repositories, background workers, database access code, dependency injection registrations, and even complete application features.
The difficult part is not getting the code to compile.
Generated code can compile successfully while still violating the architecture of the application.
For example, an AI-generated feature might:
Access Entity Framework Core directly from a controller
Bypass an application service
Introduce a dependency from the domain layer into infrastructure
Add a circular dependency
Create unnecessary abstractions
Access configuration from the wrong layer
Introduce a new external package without approval
Bypass an existing authorization boundary
Traditional unit tests may not detect these problems.
This is where architecture fitness tests become useful.
An architecture fitness test automatically checks whether the codebase continues to satisfy predefined architectural rules. When AI-generated code is introduced frequently, these tests can act as a guardrail between code generation and architectural drift.
What Is an Architecture Fitness Test?
An architecture fitness test verifies a property of the system's architecture.
Instead of asking:
Does this method return the correct value?it asks:
Does this layer depend only on allowed layers?For example:
API
|
v
Application
|
v
Domain
Infrastructure
|
+--> Application
+--> DomainA fitness test could enforce:
Domain -> Infrastructure = forbidden
Domain -> API = forbidden
Application -> API = forbiddenThe test fails if generated code violates one of these rules.
Why AI-Generated Code Needs Architecture Tests
AI-generated code introduces a different development pattern.
A developer may normally understand the architecture before making a change.
With AI-assisted development, the workflow can look like:
Developer Request
|
v
AI Generates Code
|
v
Build
|
v
TestsIf all functional tests pass, the code may be merged.
The architecture can gradually change without anyone noticing.
Over time:
Small Violation
|
v
Another Violation
|
v
More Coupling
|
v
Architecture DriftFitness tests add another checkpoint:
AI Generated Code
|
v
Build + Unit Tests
|
v
Architecture Fitness Tests
|
v
ReviewStart With Explicit Architecture Rules
Architecture tests work best when the rules are precise.
For example:
| Layer | Allowed Dependencies |
|---|---|
| API | Application |
| Application | Domain |
| Domain | None |
| Infrastructure | Application, Domain |
This can be represented conceptually as:
API
|
v
Application
|
v
Domain
Infrastructure
|
+--> Application
+--> DomainThe important part is that the dependency direction is intentional.
Identify Architectural Boundaries
Before writing tests, identify the boundaries that matter.
Typical .NET applications might contain:
src/
├── Api/
├── Application/
├── Domain/
├── Infrastructure/
└── Worker/The exact naming is not important.
What matters is that each project has a defined responsibility.
For example:
Domain
- Entities
- Value Objects
- Domain Rules
Application
- Use Cases
- Interfaces
- DTOs
Infrastructure
- EF Core
- External APIs
- Persistence
API
- Controllers
- HTTP ConfigurationTest Project Dependencies
One of the simplest architecture checks is validating assembly references.
For example, the domain project should not reference infrastructure.
A fitness test can inspect the loaded assemblies and their references.
A conceptual test could be:
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var references = typeof(Order).Assembly
.GetReferencedAssemblies();
references.Should().NotContain(
x => x.Name == "MyApp.Infrastructure");
}The exact implementation depends on the architecture and test framework.
The important principle is that dependency rules become executable.
Detect Namespace Dependencies
Project references are not the only problem.
A generated file can introduce an architectural dependency through namespaces.
For example:
using MyApp.Infrastructure.Persistence;inside the domain project is an immediate warning sign.
A fitness test can inspect types and namespaces to detect such dependencies.
This is particularly useful when projects are large and developers may not immediately notice an inappropriate reference.
Prevent EF Core From Entering the Domain Layer
A common architectural boundary is keeping persistence concerns outside the domain.
The domain should generally not need to know about:
DbContext
DbSet
Entity Framework configuration
SQL-specific types
Database connection objectsA fitness rule can therefore reject dependencies such as:
MyApp.Domain
|
X
Microsoft.EntityFrameworkCoreThis prevents generated code from quietly introducing persistence concerns into business logic.
Example: Detecting Forbidden Dependencies
A reusable rule might look like:
public static void AssertNoForbiddenDependency(
Assembly assembly,
string forbiddenAssembly)
{
var references = assembly
.GetReferencedAssemblies()
.Select(x => x.Name);
if (references.Contains(forbiddenAssembly))
{
throw new InvalidOperationException(
$"{assembly.GetName().Name} depends on {forbiddenAssembly}.");
}
}The test can then enforce:
Domain
X Infrastructure
Domain
X EF Core
Domain
X ASP.NET CoreDetect Circular Dependencies
AI-generated code can also introduce circular dependencies.
For example:
Application
|
v
Infrastructure
|
v
ApplicationCircular dependencies make the architecture harder to understand and maintain.
A fitness test should detect cycles in the project dependency graph.
The desired structure should be a directed acyclic graph where architectural dependencies have a clear direction.
Protect the Domain From Framework Coupling
Suppose generated code adds:
using Microsoft.AspNetCore.Http;to a domain service.
The application may still compile.
But the domain is now coupled to an HTTP framework.
A fitness rule can prohibit framework namespaces from entering the domain layer.
For example:
Domain -> ASP.NET Core = Forbidden
Domain -> EF Core = Forbidden
Domain -> Logging = RestrictedThe exact rules depend on the architecture.
Test API Layer Responsibilities
Controllers are a common place for architecture drift.
A generated controller might contain:
[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request)
{
var order = new Order();
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
return Ok(order);
}This may work technically.
But if the architecture expects:
Controller
|
v
Application Service
|
v
Domain
|
v
Repositorythe controller is bypassing the intended application layer.
An architecture test can identify controllers that directly depend on persistence infrastructure.
Detect Direct DbContext Injection
A useful rule for layered applications is:
Controllers
X
DbContextInstead:
Controllers
|
v
Application ServicesA test can inspect constructor parameters on controller classes.
Conceptually:
var controllers = assembly.GetTypes()
.Where(t => t.Name.EndsWith("Controller"));
foreach (var controller in controllers)
{
var parameters = controller
.GetConstructors()
.SelectMany(c => c.GetParameters());
Assert.DoesNotContain(
parameters,
p => p.ParameterType.Name == "AppDbContext");
}This type of test is particularly useful for generated code.
Protect Dependency Injection Boundaries
AI-generated code may also register services incorrectly.
For example, an application service might directly construct infrastructure classes:
var repository = new SqlOrderRepository();This bypasses dependency injection.
A preferred pattern might be:
public sealed class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}Architecture tests can identify direct construction of restricted infrastructure types.
Detect Forbidden Package Dependencies
AI-generated code can introduce a new NuGet package because it appears convenient.
For example:
Existing:
.NET libraries
Generated:
New serialization package
New HTTP library
New database helperThe package may work but violate organizational standards.
A dependency fitness test can check the package graph for:
Unapproved packages
Duplicate functionality
Restricted packages
Packages with unacceptable licenses
Deprecated packages
Packages requiring security review
This creates a useful boundary around AI-generated dependencies.
Enforce Internal Package Boundaries
Large organizations often maintain multiple internal libraries.
For example:
Company.Core
Company.Security
Company.Data
Company.AI
Company.WebA generated project should not automatically reference everything.
Define allowed relationships:
Company.Web
-> Company.Core
-> Company.Security
Company.Core
-> no Web dependencyFitness tests can enforce these boundaries continuously.
Prevent Business Logic in Controllers
Another useful rule is architectural rather than dependency-based.
A controller containing large amounts of business logic may be a sign of generated code bypassing the application's design.
For example:
if (order.Status == "Pending"
&& customer.Type == "Premium"
&& order.Total > 5000)
{
// Large business rule
}The exact detection of business logic is difficult to automate perfectly.
However, you can use structural indicators such as:
Maximum method length
Maximum constructor dependencies
Forbidden namespaces
Direct database access
Direct external API calls
These are useful heuristics rather than absolute architectural truths.
Enforce Dependency Direction
Dependency direction is one of the highest-value fitness rules.
For example:
Presentation
|
v
Application
|
v
Domain
Infrastructure -> Application
Infrastructure -> DomainA test should fail if:
Domain -> Application
Domain -> Infrastructure
Application -> Infrastructurewhen those dependencies are not part of the intended design.
Test Naming Conventions
Naming conventions can also support architecture.
For example:
*Controller
*ApplicationService
*Repository
*RepositoryImplementationA rule can enforce that repository implementations remain in the infrastructure layer.
For example:
Domain/OrderRepository.csmight be forbidden if repositories are intended to be infrastructure implementations.
Test Namespace Placement
Generated code sometimes ends up in the wrong project or namespace.
For example:
MyApp.Domain.InfrastructureA namespace rule can detect this.
A simple convention could be:
Domain types -> MyApp.Domain.*
Application types -> MyApp.Application.*
Infrastructure types -> MyApp.Infrastructure.*This is not a substitute for architectural analysis, but it provides another safety layer.
Architecture Fitness Tests With NetArchTest
A .NET architecture test can also use an architecture-testing library.
For example:
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn("MyApp.Infrastructure")
.GetResult();
result.IsSuccessful.Should().BeTrue();
}This style makes architectural rules easier to read.
You can define rules such as:
Domain should not depend on Infrastructure
Controllers should not depend on DbContext
Domain should not depend on ASP.NET Core
Application should not depend on APIThe exact API depends on the architecture-testing library version being used.
Test Generated Code in CI
Architecture tests become most valuable when they run automatically.
A typical pipeline can look like:
AI-Assisted Development
|
v
Build
|
v
Unit Tests
|
v
Integration Tests
|
v
Architecture Tests
|
v
Pull RequestThe AI does not need to be trusted to preserve architectural rules.
The rules are enforced independently.
Add Architecture Checks to Pull Requests
A pull request containing a new architectural violation should fail before merge.
For example:
Build PASS
Unit Tests PASS
Integration Tests PASS
Architecture Tests FAILThis is useful because functional correctness does not override architectural constraints.
Use Architecture Fitness Tests as AI Guardrails
Architecture tests can be treated as a guardrail around code generation.
The workflow becomes:
Developer Intent
|
v
AI Code Generation
|
v
Compile
|
v
Architecture Fitness
|
v
Functional Tests
|
v
ReviewIf generated code violates the architecture, the feedback loop is immediate.
Test Public API Boundaries
Architecture can also include public API exposure.
For example, an internal domain implementation should not accidentally become publicly accessible through an API.
Fitness tests can inspect:
Public classes
Public methods
DTO boundaries
Internal types
Controller endpoints
This helps prevent accidental exposure during generated-code changes.
Test Dependency Count
A class with many dependencies may be a design warning.
For example:
public OrderService(
IOrderRepository orders,
ICustomerRepository customers,
IInvoiceRepository invoices,
IEmailService email,
IStorageService storage,
ILogger<OrderService> logger,
IConfiguration configuration)
{
}This may indicate excessive responsibility.
A fitness test can establish a soft limit:
Maximum constructor dependencies = 6This should be treated as a heuristic rather than an absolute rule.
Test Layer Leakage
A particularly useful category is detecting implementation leakage.
Examples:
API -> SQL
Domain -> HTTP
Application -> Azure SDK
Domain -> EF CoreThe exact restrictions depend on the architecture.
The general principle is:
An inner layer should not know unnecessary details about outer infrastructure.
Test External Service Access
Generated code may call external APIs directly from places where the architecture expects an abstraction.
For example:
using System.Net.Http;
public class Order
{
// Domain object directly making HTTP calls.
}This should usually be rejected by an architecture rule.
Instead:
Domain
|
Application Abstraction
|
Infrastructure Adapter
|
External ServiceCreate a Rule Catalog
As the application grows, maintain architecture rules as a documented catalog.
For example:
| Rule | Severity |
|---|---|
| Domain cannot reference Infrastructure | Critical |
| Domain cannot reference ASP.NET Core | Critical |
| Controllers cannot inject DbContext | High |
| Application cannot reference API | High |
| New external package requires approval | High |
| Maximum controller dependencies | Medium |
This makes architecture governance explicit.
Distinguish Hard Rules From Heuristics
Not every architectural guideline should fail the build.
Hard Rules
Examples:
No domain-to-infrastructure dependency
No unauthorized package
No forbidden security dependency
No circular project dependency
Heuristics
Examples:
Controller should remain below a certain size
Constructor should have fewer than a certain number of dependencies
Service should not have excessive methods
Hard rules can fail CI.
Heuristics can produce warnings for review.
Example Architecture Test Suite
A practical suite might contain:
ArchitectureTests
├── DependencyRulesTests
├── LayerBoundaryTests
├── ControllerRulesTests
├── DomainRulesTests
├── PackageRulesTests
├── NamingRulesTests
└── PublicApiRulesTestsThis makes failures easier to understand.
Test Architecture Before and After AI Changes
When introducing AI-assisted development, run the same architecture suite against:
Baseline Branch
|
v
Architecture Tests
AI-Modified Branch
|
v
Architecture TestsThe comparison shows whether generated code introduced new violations.
Common Mistakes
Testing Only Compilation
Code that compiles can still violate architectural boundaries.
Relying Only on Code Review
Reviewers may miss subtle dependency changes, especially in large generated diffs.
Making Every Guideline a Hard Failure
Some architecture guidance is subjective and better treated as a warning.
Checking Only Project References
Namespace, package, and type-level dependencies can still introduce architectural coupling.
Ignoring Package Dependencies
Generated code may introduce new external libraries without obvious architectural changes.
Allowing Controllers to Access Infrastructure Directly
This can gradually move business logic and persistence concerns into the presentation layer.
Testing Only Existing Code
Architecture tests should run continuously so new violations cannot accumulate.
Creating Rules Nobody Understands
A fitness rule should explain what boundary it protects and why.
Best Practices
Define architecture rules explicitly.
Convert important rules into automated tests.
Enforce dependency direction.
Protect domain boundaries.
Prevent direct persistence access from presentation layers.
Detect forbidden framework dependencies.
Review new package dependencies.
Detect circular dependencies.
Separate hard rules from heuristics.
Run architecture tests in CI.
Keep failure messages understandable.
Version architecture rules alongside the codebase.
Review rules when the architecture intentionally changes.
Use architecture tests as a guardrail for AI-generated code.
Combine architecture tests with functional and security testing.
Frequently Asked Questions
Are architecture fitness tests useful only for AI-generated code?
No. They are useful for any continuously changing codebase. AI-assisted development simply increases the value because code can be generated and modified at a much higher rate.
Can architecture tests replace code review?
No. They enforce structural rules but cannot fully evaluate design quality, business intent, or maintainability.
Should every architecture rule be automated?
No. Automate rules that are objective, repeatable, and important enough to enforce continuously. Subjective design guidance may remain part of code review.
Can architecture tests detect bad business logic?
Only partially. They can identify structural warning signs, but functional and domain tests are still required to determine whether business behavior is correct.
Should architecture tests run before unit tests?
The exact ordering is flexible. In CI, they should be part of the normal validation pipeline and should fail the build when a critical architectural constraint is violated.
How do architecture tests help with AI coding assistants?
They provide an independent enforcement mechanism. The generated code does not need to understand or remember every architectural rule because the codebase itself can verify those rules automatically.
Conclusion
AI-assisted development can significantly accelerate .NET development, but faster code generation also increases the risk of architectural drift.
A generated controller can bypass an application service. A generated domain class can reference infrastructure. A convenient package can introduce an unwanted dependency. None of these problems necessarily prevent the application from compiling or passing ordinary unit tests.
Architecture fitness tests provide a practical safety net.
By turning architectural principles into executable rules, teams can continuously enforce dependency direction, layer boundaries, package restrictions, API boundaries, and other structural constraints.
The most effective approach is not to try to prevent AI from generating code outside the architecture. Instead, make the architecture executable and let automated fitness tests reject changes that violate it.
This creates a scalable feedback loop where AI can generate code quickly while the codebase itself continuously protects the architecture.

Join the conversation! Your thoughts help the community grow.