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:

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
 +--> Domain

A fitness test could enforce:

Domain -> Infrastructure = forbidden
Domain -> API            = forbidden
Application -> API       = forbidden

The 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
Tests

If 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 Drift

Fitness tests add another checkpoint:

AI Generated Code
       |
       v
Build + Unit Tests
       |
       v
Architecture Fitness Tests
       |
       v
Review

Start With Explicit Architecture Rules

Architecture tests work best when the rules are precise.

For example:

LayerAllowed Dependencies
APIApplication
ApplicationDomain
DomainNone
InfrastructureApplication, Domain

This can be represented conceptually as:

API
 |
 v
Application
 |
 v
Domain

Infrastructure
 |
 +--> Application
 +--> Domain

The 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 Configuration

Test 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 objects

A fitness rule can therefore reject dependencies such as:

MyApp.Domain
      |
      X
Microsoft.EntityFrameworkCore

This 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 Core

Detect Circular Dependencies

AI-generated code can also introduce circular dependencies.

For example:

Application
    |
    v
Infrastructure
    |
    v
Application

Circular 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     = Restricted

The 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
Repository

the 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
DbContext

Instead:

Controllers
    |
    v
Application Services

A 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 helper

The package may work but violate organizational standards.

A dependency fitness test can check the package graph for:

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

A generated project should not automatically reference everything.

Define allowed relationships:

Company.Web
    -> Company.Core
    -> Company.Security

Company.Core
    -> no Web dependency

Fitness 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:

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 -> Domain

A test should fail if:

Domain -> Application
Domain -> Infrastructure
Application -> Infrastructure

when those dependencies are not part of the intended design.

Test Naming Conventions

Naming conventions can also support architecture.

For example:

*Controller
*ApplicationService
*Repository
*RepositoryImplementation

A rule can enforce that repository implementations remain in the infrastructure layer.

For example:

Domain/OrderRepository.cs

might 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.Infrastructure

A 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 API

The 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 Request

The 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    FAIL

This 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
Review

If 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:

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 = 6

This 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 Core

The 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 Service

Create a Rule Catalog

As the application grows, maintain architecture rules as a documented catalog.

For example:

RuleSeverity
Domain cannot reference InfrastructureCritical
Domain cannot reference ASP.NET CoreCritical
Controllers cannot inject DbContextHigh
Application cannot reference APIHigh
New external package requires approvalHigh
Maximum controller dependenciesMedium

This makes architecture governance explicit.

Distinguish Hard Rules From Heuristics

Not every architectural guideline should fail the build.

Hard Rules

Examples:

Heuristics

Examples:

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
└── PublicApiRulesTests

This 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 Tests

The 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

  1. Define architecture rules explicitly.

  2. Convert important rules into automated tests.

  3. Enforce dependency direction.

  4. Protect domain boundaries.

  5. Prevent direct persistence access from presentation layers.

  6. Detect forbidden framework dependencies.

  7. Review new package dependencies.

  8. Detect circular dependencies.

  9. Separate hard rules from heuristics.

  10. Run architecture tests in CI.

  11. Keep failure messages understandable.

  12. Version architecture rules alongside the codebase.

  13. Review rules when the architecture intentionally changes.

  14. Use architecture tests as a guardrail for AI-generated code.

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