AI-assisted development has changed how quickly .NET teams can produce code. Developers can now use AI coding tools to generate controllers, services, data-access code, tests, infrastructure definitions, and entire application features.
The productivity gain can be significant, but generated code introduces a new architectural challenge.
AI can produce code that compiles and passes a basic test while still violating the architecture of the application.
For example:
Controller
|
+--> Repository
|
+--> DbContext
|
+--> External API
The code may work, but the application's architecture may require:
Controller
|
v
Application Service
|
v
Domain
|
v
Infrastructure
|
v
Database
Traditional code review can catch some of these violations, but reviewing every AI-generated change manually does not scale.
This is where architecture fitness functions become useful.
An architecture fitness function is an automated check that continuously verifies whether an application still satisfies important architectural rules.
For AI-generated .NET code, these rules can become automated guardrails around dependency direction, project boundaries, API contracts, naming, persistence access, and other architectural constraints.
Introduction
Imagine a .NET solution with four projects:
MyApp.Api
MyApp.Application
MyApp.Domain
MyApp.Infrastructure
The intended dependency direction is:
Api
|
v
Application
|
v
Domain
Infrastructure
|
v
Domain
Now an AI coding assistant generates this:
public class OrdersController : ControllerBase
{
private readonly AppDbContext _db;
public OrdersController(AppDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<IActionResult> GetOrders()
{
return Ok(await _db.Orders.ToListAsync());
}
}
There is nothing inherently wrong with this code from a compiler's perspective.
But if the architecture requires controllers to communicate only with application services, this is an architectural violation.
The challenge is therefore not:
Can AI generate valid C#?
It is:
Can the development system automatically prevent AI-generated code from gradually weakening the architecture?
Architecture fitness functions provide one answer.
What Is an Architecture Fitness Function?
A fitness function is an automated test that evaluates an architectural property.
For example:
Rule:
API must not reference Infrastructure
The fitness function evaluates the solution:
MyApp.Api
|
+--> MyApp.Application PASS
+--> MyApp.Domain PASS
+--> MyApp.Infrastructure FAIL
The important distinction is that the rule describes an architectural property rather than a specific implementation.
A fitness function can validate:
Project dependencies
Namespace boundaries
Layering
Dependency direction
API exposure
Database access rules
Domain isolation
Naming conventions
Technology restrictions
Test architecture
Module boundaries
Why AI-Generated Code Needs Architectural Guardrails
AI coding systems optimize for the requested task.
If a prompt says:
Create an endpoint that returns orders from PostgreSQL.
the generated solution may directly inject DbContext into a controller because that is a straightforward implementation.
But the application may require:
Controller
|
v
IOrderService
|
v
OrderRepository
|
v
DbContext
The AI does not automatically know every architectural constraint unless those constraints are explicitly available in its context.
Even when architecture documentation is provided, generated code should still be validated automatically.
Fitness Functions as Executable Architecture
Traditional architecture documentation often looks like:
API should not access database directly.
A fitness function turns that statement into an executable rule:
Api Project
|
X Infrastructure
Now the architecture is not just documentation.
It is testable.
Architecture Decision
|
v
Fitness Function
|
v
Automated Validation
|
v
CI/CD Gate
This is particularly valuable when code is being generated rapidly.
Define Architectural Rules First
Before implementing fitness functions, identify the architecture's most important invariants.
For example:
| Architectural Rule | Enforcement |
|---|---|
| API cannot reference Infrastructure | Dependency test |
| Domain cannot reference EF Core | Assembly test |
| Domain cannot reference ASP.NET Core | Dependency test |
| Controllers should use application services | Static analysis |
| Infrastructure implements application abstractions | Dependency test |
| Public APIs require authorization metadata | API test |
| Database access stays in Infrastructure | Namespace/type test |
Do not try to automate every style preference.
Start with rules that protect architectural integrity.
Project Dependency Fitness Function
One of the simplest checks is validating project references.
Suppose:
MyApp.Api
MyApp.Application
MyApp.Domain
MyApp.Infrastructure
The rule is:
Api -> Application
Api -> Domain
Application -> Domain
Infrastructure -> Application
Infrastructure -> Domain
The API must not reference Infrastructure directly.
A test can inspect assembly dependencies.
Using a reflection-based approach:
var apiAssembly =
typeof(ApiAssemblyMarker).Assembly;
var references = apiAssembly
.GetReferencedAssemblies()
.Select(x => x.Name)
.ToHashSet();
Assert.DoesNotContain(
"MyApp.Infrastructure",
references);
The exact implementation depends on how the solution is structured, but the principle is straightforward.
Assembly-Level Dependency Rules
A more scalable approach is to use architecture-testing libraries or custom reflection-based tests.
Conceptually:
var domainAssembly = typeof(DomainAssemblyMarker)
.Assembly;
var forbiddenNamespaces = new[]
{
"Microsoft.EntityFrameworkCore",
"Microsoft.AspNetCore"
};
The test can inspect referenced assemblies and fail when the Domain project introduces an unwanted dependency.
For example:
Domain
|
+-- System.* Allowed
+-- MyApp.Domain.* Allowed
+-- Microsoft.EntityFrameworkCore FAIL
This prevents infrastructure concerns from leaking into the domain layer.
Namespace Fitness Functions
Project boundaries alone are not always enough.
A project may contain:
MyApp.Application
|
+-- Services
+-- Infrastructure
+-- Persistence
The project itself may be correct, but namespace placement can reveal architectural drift.
A fitness function can enforce rules such as:
Domain namespace
|
X Infrastructure namespace
This is especially useful in modular monoliths where multiple architectural modules share one project.
Dependency Direction
Dependency direction is one of the most important architectural properties.
For example:
Presentation
|
v
Application
|
v
Domain
The fitness function should prevent:
Domain
|
X Presentation
and:
Domain
|
X Infrastructure
A useful test is to maintain an explicit dependency matrix:
Domain Application Infrastructure API
Domain YES NO NO NO
Application YES YES NO NO
Infrastructure YES YES YES NO
API YES YES NO YES
The exact matrix depends on the architecture, but making it explicit is important.
Prevent Direct DbContext Access
Suppose the architecture says only Infrastructure can access EF Core.
A fitness function can scan assemblies for references to:
Microsoft.EntityFrameworkCore
and verify that only approved projects contain them.
For example:
Domain PASS
Application PASS
Infrastructure PASS
Api FAIL
This is a powerful guardrail because an AI-generated controller may otherwise introduce direct database access very easily.
Detect Infrastructure Leakage
Another common problem is infrastructure-specific types entering application contracts.
For example:
public Task<NpgsqlDataReader> GetOrdersAsync()
inside an application service is an architectural smell if the application layer is intended to remain persistence-independent.
A fitness function can scan public APIs for forbidden types:
Application
|
X Npgsql*
X DbConnection
X DbContext
The goal is not to prohibit these types everywhere.
The goal is to ensure they remain within the appropriate architectural boundary.
Detect Framework Leakage
A domain model should usually not depend directly on ASP.NET Core.
For example:
public class Order
{
public IActionResult Execute()
{
...
}
}
This is a clear boundary violation.
A fitness function can reject dependencies such as:
Microsoft.AspNetCore.*
Microsoft.Extensions.*
Microsoft.EntityFrameworkCore.*
Npgsql.*
from the Domain assembly where those dependencies are not part of the intended architecture.
API Contract Fitness Functions
Architecture is not limited to project dependencies.
API contracts can also be governed.
For example:
All public controller endpoints
|
+--> Authorization metadata required
A test can inspect endpoint metadata and identify publicly accessible endpoints that should require authorization.
Conceptually:
var endpoints = app.Services
.GetRequiredService<EndpointDataSource>()
.Endpoints;
foreach (var endpoint in endpoints)
{
// Validate endpoint metadata.
}
The exact validation depends on the application's authentication model.
Prevent Unapproved Public APIs
An AI-generated class can accidentally expose public methods that were intended to remain internal.
For example:
public class PaymentProcessor
{
public void InternalReconciliation()
{
}
}
If the architecture requires internal application services to remain non-public, an automated rule can detect unexpected public types.
This is especially useful in modular systems where public interfaces define module boundaries.
Module Boundary Fitness Functions
Consider a modular monolith:
Sales
|
+-- Domain
+-- Application
+-- Infrastructure
Billing
|
+-- Domain
+-- Application
+-- Infrastructure
Identity
|
+-- Domain
+-- Application
+-- Infrastructure
A rule might be:
Sales -> Billing.Application
is allowed only through a defined contract.
But:
Sales -> Billing.Infrastructure
is forbidden.
Fitness functions can enforce this boundary.
Detect Cross-Module Database Access
One particularly dangerous architectural shortcut is direct database access across modules.
For example:
Sales
|
X BillingDbContext
The correct architecture might require:
Sales
|
v
Billing Contract
|
v
Billing
A fitness function can prevent one module from referencing another module's persistence layer.
Architecture Rules for AI-Generated Code
The most useful rules are usually structural.
Examples:
Controllers cannot reference DbContext.
Domain cannot reference EF Core.
Domain cannot reference ASP.NET Core.
Application cannot reference PostgreSQL provider types.
Modules cannot reference another module's Infrastructure.
Infrastructure cannot reference API.
Public API endpoints require authorization metadata.
Repositories cannot be used directly by controllers.
These rules are relatively stable and therefore suitable for automation.
Fitness Functions Should Be Executable Tests
Place architecture tests alongside the application.
For example:
tests/
|
+-- UnitTests
+-- IntegrationTests
+-- ArchitectureTests
Then CI can run:
dotnet test
and architecture validation becomes part of the normal test lifecycle.
This is important because an architecture rule that is never executed is effectively documentation.
Example Architecture Test
A simple dependency rule can be represented as:
[Fact]
public void Domain_Should_Not_Reference_Infrastructure()
{
var assembly =
typeof(DomainAssemblyMarker).Assembly;
var references = assembly
.GetReferencedAssemblies()
.Select(x => x.Name)
.ToHashSet();
Assert.DoesNotContain(
"MyApp.Infrastructure",
references);
}
The test is intentionally simple.
The important concept is that the architecture becomes executable.
Make Failures Actionable
An architecture test should explain what went wrong.
Bad:
Test failed.
Better:
Architecture violation:
MyApp.Api references MyApp.Infrastructure.
Rule:
API must depend on Application, not Infrastructure.
Move persistence access behind an application abstraction.
AI-generated changes are often reviewed quickly, so useful failure messages reduce remediation time.
Detect Architectural Drift
Architecture usually degrades gradually.
For example:
Month 1:
5 projects
Month 6:
17 projects
Month 12:
34 projects
A small shortcut added to each feature can eventually create a dependency graph that is difficult to maintain.
Fitness functions detect these changes immediately.
Good Architecture
|
v
New Feature
|
v
Architecture Test
|
+--> PASS
If the feature introduces a forbidden dependency:
|
+--> FAIL
The team can fix it before the pattern spreads.
Combine Static and Runtime Fitness Functions
Not every architectural rule can be checked through reflection.
Use multiple categories.
Static Fitness Functions
Validate:
Project references
Assembly dependencies
Namespace usage
Forbidden types
Public API structure
Runtime Fitness Functions
Validate:
Endpoint authorization
Database configuration
Service registration
Runtime dependency behavior
Health-check requirements
Integration Fitness Functions
Validate:
Module boundaries
Database access patterns
External service contracts
Messaging architecture
A mature architecture test suite can combine all three.
Architecture Rules for Dependency Injection
AI-generated code frequently changes dependency injection registrations.
For example:
builder.Services.AddScoped<IOrderService, OrderService>();
A fitness function can verify that:
Required interfaces are registered.
Infrastructure implementations are registered in the appropriate composition root.
Domain services are not accidentally registered as infrastructure components.
API projects do not instantiate infrastructure classes directly.
This helps maintain composition-root discipline.
Prevent new on Infrastructure Services
Consider:
var repository = new SqlOrderRepository();
inside a controller.
Even if the code works, it bypasses dependency injection.
A static analysis rule can flag direct construction of restricted infrastructure classes.
The desired pattern is:
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
This is a good example of an architectural property that can be enforced automatically.
Enforce Domain Purity
For domain-driven designs, the domain layer often has stricter requirements.
A fitness function might require:
Domain
|
+-- Domain entities
+-- Value objects
+-- Domain services
+-- Domain events
and prohibit:
X EF Core
X ASP.NET Core
X Npgsql
X HTTP clients
X Configuration providers
The exact rule depends on the architecture.
The important principle is to enforce the intended dependency boundary rather than assuming developers or AI tools will always remember it.
Prevent Infrastructure Types in DTOs
Suppose an API DTO contains:
public DbConnection Connection { get; set; }
This is an obvious architectural leak.
A fitness function can inspect public DTO properties and reject infrastructure-specific types.
For example:
API Contract
|
X DbContext
X DbConnection
X NpgsqlConnection
This can protect external contracts from internal implementation details.
Architecture Fitness Functions and Pull Requests
A strong workflow is:
Developer / AI
|
v
Code Change
|
v
Unit Tests
|
v
Architecture Tests
|
v
Integration Tests
|
v
Pull Request
This means AI-generated code receives the same architectural validation as manually written code.
That is important.
The goal should not be to create a separate lower standard for AI-generated changes.
Add Architecture Checks to CI
A simple pipeline might be:
steps:
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build
- name: Architecture Tests
run: dotnet test tests/ArchitectureTests
The exact CI syntax depends on the platform.
The key requirement is that architecture tests run automatically before code can be merged.
Use Architecture Tests as Release Gates
For critical architecture violations:
Architecture Test
|
+--> PASS -> Continue
|
+--> FAIL -> Block
Do not make architecture tests advisory if the underlying rule is genuinely mandatory.
For example, if the domain must never depend on infrastructure, that should be a blocking rule.
Avoid Over-Constraining the Architecture
Fitness functions can also become harmful when they enforce too many minor details.
For example:
Class must contain exactly three methods.
This is usually not an architectural invariant.
Prefer rules such as:
Domain cannot depend on infrastructure.
The strongest fitness functions protect architectural intent rather than implementation style.
Test the Tests
An architecture fitness function is itself production-critical engineering logic.
If a test is accidentally too broad or too weak, it can provide false confidence.
For example:
Rule:
Domain must not reference EF Core.
Verify that the test actually fails when EF Core is introduced.
This is sometimes called testing the architecture tests themselves.
Baseline Existing Violations
Large legacy applications may already violate several architectural rules.
If you enable 50 strict rules immediately, CI may become unusable.
Instead:
Existing Violations
|
v
Baseline
|
v
New Changes
|
v
No New Violations
Then gradually reduce the baseline.
This is especially useful when introducing architecture fitness functions into an existing .NET application.
Track Architectural Debt
A useful report might say:
Architecture Report
Rules: 18
Passed: 16
Failed: 2
Existing Violations: 7
New Violations: 0
This separates historical debt from newly introduced problems.
The goal is to prevent architectural debt from increasing while the team gradually pays down existing issues.
AI-Specific Architectural Guardrails
AI-assisted development creates several useful opportunities for additional checks.
Generated Code Must Follow Existing Boundaries
If a new controller references DbContext, the architecture test should catch it regardless of whether a human or AI created it.
Generated Dependencies Must Be Approved
If AI introduces a new package, dependency scanning should identify it.
Generated APIs Must Follow Security Rules
If AI creates a new endpoint, API-level fitness functions can verify authorization requirements.
Generated Code Must Preserve Module Boundaries
AI-generated shortcuts should not allow one module to directly access another module's infrastructure.
The principle is:
AI Assistance
|
v
Generated Code
|
v
Same Architectural Rules
|
v
Automated Validation
Common Mistakes
Treating Architecture Documentation as Enforcement
Documentation explains architecture but does not prevent violations.
Checking Only Project References
Namespace, type, API, and runtime rules may still be violated.
Creating Too Many Rules
Excessive rules create noise and reduce developer trust.
Blocking Legacy Code Immediately
Use a baseline when introducing architecture tests into an existing system.
Writing Unclear Failure Messages
Developers need to know which architectural rule was violated and why.
Testing Only During Release
Architecture violations should be detected during development.
Ignoring AI-Generated Changes
AI-generated code should pass the same architecture checks as human-written code.
Not Testing the Fitness Functions
A broken architecture test can create false confidence.
Advantages
Continuous Architecture Enforcement
Rules are checked every time code changes.
AI-Compatible Guardrails
Generated code is automatically subjected to architectural constraints.
Early Detection
Violations are found before they spread across the codebase.
Reduced Review Burden
Code review can focus more on business logic and design decisions.
Better Long-Term Maintainability
The architecture becomes harder to accidentally degrade.
Measurable Architectural Health
Teams can track violations and architectural debt over time.
Disadvantages
Maintenance Cost
Architecture rules evolve as the system evolves.
False Positives
Overly broad rules can reject valid implementations.
Initial Investment
Legacy applications may require substantial cleanup before strict enforcement is possible.
Tooling Complexity
A mature architecture test suite can involve static analysis, reflection, integration tests, and CI integration.
Best Practices
Identify architectural invariants before writing tests.
Start with high-value dependency rules.
Enforce dependency direction explicitly.
Prevent domain-to-infrastructure dependencies.
Restrict direct database access to approved layers.
Protect module boundaries.
Validate public API architecture.
Add architecture tests to the normal test suite.
Run them on every pull request.
Use clear failure messages.
Baseline existing violations in legacy applications.
Prevent new violations while reducing the baseline.
Test the architecture tests themselves.
Avoid enforcing minor implementation preferences.
Combine static and runtime fitness functions where necessary.
Track architectural debt over time.
Apply the same rules to human-written and AI-generated code.
Treat critical architectural violations as CI gates.
Review rules when the architecture changes.
Keep the fitness-function suite smaller than the business test suite but strong enough to protect architectural boundaries.
Frequently Asked Questions
Are architecture fitness functions the same as unit tests?
No. Unit tests verify behavior of small pieces of code. Architecture fitness functions verify structural or architectural properties of the system.
Can fitness functions prevent AI-generated bad code?
They cannot prevent every problem, but they can automatically detect many structural violations such as forbidden dependencies, direct database access, module-boundary violations, and unwanted framework references.
Should architecture tests run on every pull request?
Yes, especially for critical architectural rules. Fast structural checks are particularly suitable for pull-request validation.
What if the existing application already violates the architecture?
Create a baseline for known violations and fail only on new violations initially. Then reduce the baseline over time.
Should every architecture decision become a fitness function?
No. Automate stable, objectively testable architectural invariants. Keep decisions that require human judgment in architecture documentation and review.
Can architecture fitness functions replace code review?
No. They automate structural constraints, while code review still evaluates design quality, business correctness, maintainability, and context.
Should AI-generated code receive different architecture rules?
No. The same architectural rules should apply regardless of how the code was produced.
How do I know whether a rule is worth automating?
Ask whether the rule is important, objectively testable, relatively stable, and costly to violate. If all four are true, it is a strong candidate for a fitness function.
Conclusion
AI-generated .NET code can dramatically increase development velocity, but faster code generation also means architectural mistakes can spread faster.
Architecture fitness functions provide a practical control mechanism.
Instead of relying entirely on documentation and manual review:
Architecture Decision
|
v
Executable Rule
|
v
Automated Test
|
v
CI/CD Gate
The most valuable rules are usually the ones protecting dependency direction, domain isolation, module boundaries, database access, API contracts, and infrastructure separation.
The goal is not to restrict developers or AI coding tools unnecessarily. It is to establish a set of architectural invariants that the system continuously protects.
When these checks run automatically on every change, AI-generated code becomes part of the same governed engineering process as manually written code. The result is not simply faster development, but faster development with measurable architectural guardrails.

Jasen FiciPosted Aug 26, 2026, 1:16 PM
We picked this up for DotNetNews here: https://dotnetnews.co/archive/the-net-news-daily-issue-527/