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:

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 RuleEnforcement
API cannot reference InfrastructureDependency test
Domain cannot reference EF CoreAssembly test
Domain cannot reference ASP.NET CoreDependency test
Controllers should use application servicesStatic analysis
Infrastructure implements application abstractionsDependency test
Public APIs require authorization metadataAPI test
Database access stays in InfrastructureNamespace/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:

Runtime Fitness Functions

Validate:

Integration Fitness Functions

Validate:

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:

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

  1. Identify architectural invariants before writing tests.

  2. Start with high-value dependency rules.

  3. Enforce dependency direction explicitly.

  4. Prevent domain-to-infrastructure dependencies.

  5. Restrict direct database access to approved layers.

  6. Protect module boundaries.

  7. Validate public API architecture.

  8. Add architecture tests to the normal test suite.

  9. Run them on every pull request.

  10. Use clear failure messages.

  11. Baseline existing violations in legacy applications.

  12. Prevent new violations while reducing the baseline.

  13. Test the architecture tests themselves.

  14. Avoid enforcing minor implementation preferences.

  15. Combine static and runtime fitness functions where necessary.

  16. Track architectural debt over time.

  17. Apply the same rules to human-written and AI-generated code.

  18. Treat critical architectural violations as CI gates.

  19. Review rules when the architecture changes.

  20. 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.