Software Architecture/Engineering  

Building Architecture Rules for AI-Generated Pull Requests in .NET

Introduction

AI coding tools can now generate a significant amount of application code.

A developer can describe a feature, ask an AI coding agent to implement it, and receive a pull request containing controllers, services, database queries, tests, configuration, and supporting classes.

The code may compile.

The tests may pass.

The pull request may even look reasonable during a quick review.

But compilation and functional correctness do not prove that the code follows the architecture of the application.

A generated pull request might introduce:

Controller
    |
    +---- Database access
    +---- Business rules
    +---- External API calls

inside an application that was designed to use:

API
 |
 v
Application Layer
 |
 v
Domain Layer
 |
 v
Infrastructure Layer

This creates a different type of quality problem.

The code works, but the architecture is slowly being damaged.

As AI-generated code becomes more common, teams need automated ways to verify architectural boundaries before merging pull requests.

This article explains how to build architecture rules specifically for AI-generated pull requests in .NET, how to combine static analysis with repository-level rules, and how to measure whether the approach actually improves code quality.

Why AI-Generated Code Needs Architecture Rules

Traditional code review often depends on developers recognizing architectural violations during review.

For example:

public class OrdersController : ControllerBase
{
    private readonly AppDbContext _db;

    public OrdersController(AppDbContext db)
    {
        _db = db;
    }

    public async Task<IActionResult> GetOrders()
    {
        return Ok(await _db.Orders.ToListAsync());
    }
}

This may work correctly.

But suppose the architecture requires controllers to depend only on application services.

The violation is:

Controller
   |
   X
DbContext

instead of:

Controller
   |
   v
IOrderService
   |
   v
Repository / Data Access

An AI coding agent may not understand this project-specific rule unless it is explicitly provided with the architectural constraints.

Automated validation provides a second line of defense.

Architecture Rules Should Be Executable

A document saying:

"Controllers should not access the database directly."

is useful for humans.

An executable rule is stronger:

Controller
    |
    X
Infrastructure namespace

The CI pipeline can then reject the pull request automatically.

The architecture becomes:

Architecture Rule
       |
       v
Static Analysis
       |
       v
Pull Request
       |
       +---- Pass
       |
       +---- Fail

This is especially useful when code generation happens frequently.

Start With Architecture Boundaries

Before writing rules, define the application's layers.

For example:

src/
|
+-- Api
|
+-- Application
|
+-- Domain
|
+-- Infrastructure
|
+-- Tests

Then define allowed dependencies.

Api
 |
 v
Application
 |
 v
Domain

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

The important part is defining what must not happen.

For example:

Api -> Infrastructure     Forbidden
Domain -> Infrastructure  Forbidden
Domain -> Api             Forbidden
Application -> Api        Forbidden

These rules become the basis for automated checks.

Define Dependency Direction

A simple architecture graph can be represented as:

          +---------+
          |   Api   |
          +----+----+
               |
               v
       +---------------+
       |  Application  |
       +-------+-------+
               |
               v
          +---------+
          | Domain  |
          +---------+

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

The direction matters.

If the Domain layer starts depending on Infrastructure, the architecture becomes harder to maintain.

Domain
  |
  X
Infrastructure

An AI-generated class can accidentally introduce this dependency with a single using statement.

Use Namespace Rules

One practical approach is to define namespace-based restrictions.

For example:

MyApp.Domain.*

must not reference:

Microsoft.EntityFrameworkCore.*
MyApp.Infrastructure.*
Microsoft.AspNetCore.*

Likewise:

MyApp.Api.*

should not directly reference:

MyApp.Infrastructure.Persistence.*

These rules are simple enough to automate and powerful enough to catch many architectural mistakes.

Example of a Forbidden Dependency

Suppose an AI-generated domain service contains:

using Microsoft.EntityFrameworkCore;

namespace MyApp.Domain.Services;

public class PricingService
{
    private readonly AppDbContext _db;

    public PricingService(AppDbContext db)
    {
        _db = db;
    }
}

The class may compile.

But it violates the domain boundary.

An architecture test should report:

Architecture violation:
MyApp.Domain.Services.PricingService
references Entity Framework Core.

The pull request can then be rejected before merge.

Use Architecture Tests

Architecture tests allow dependency rules to become executable.

A test can conceptually express:

[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
    // Inspect assemblies and verify dependency rules.
}

The exact implementation can use reflection, assembly analysis, or a dedicated architecture-testing library.

The important principle is that architecture should be tested like application behavior.

Unit Tests
Integration Tests
Architecture Tests
Security Tests
        |
        v
      CI

Test Assembly Dependencies

At the assembly level, you might enforce:

Domain
  -> No EF Core
  -> No ASP.NET Core
  -> No Infrastructure

Application
  -> Domain
  -> Approved abstractions

Api
  -> Application
  -> Approved contracts

This is useful because a namespace-level rule can sometimes miss a dependency introduced through a project reference.

Assembly-level validation catches broader architectural drift.

Test Project References

The .csproj file itself can reveal architectural problems.

For example:

<ItemGroup>
  <ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>

If the API project is not supposed to reference Infrastructure directly, the dependency should be rejected.

This is a good example of a rule that can be checked before compilation.

Project File
     |
     v
Dependency Analysis
     |
     +---- Allowed
     |
     +---- Forbidden

Enforce Controller Boundaries

Controllers are a common place where generated code can accumulate too much responsibility.

A useful rule is:

Controller
   |
   +---- Request validation
   +---- Authorization
   +---- Application service
   |
   X---- DbContext
   X---- SQL
   X---- External HTTP calls
   X---- Business rules

For example, this should trigger a review:

public class OrdersController : ControllerBase
{
    private readonly AppDbContext _db;

    public async Task<IActionResult> Delete(int id)
    {
        var order = await _db.Orders.FindAsync(id);

        if (order == null)
            return NotFound();

        if (order.Status == "Completed")
            return BadRequest();

        _db.Orders.Remove(order);

        await _db.SaveChangesAsync();

        return NoContent();
    }
}

The controller contains:

  • Data access

  • Business logic

  • State validation

  • Persistence

A generated implementation may frequently produce patterns like this because they are straightforward.

Architecture rules should prevent the pattern when the application uses a layered design.

Enforce Domain Boundaries

The domain layer should generally contain business concepts rather than infrastructure concerns.

For example:

public class Order
{
    public decimal Total { get; private set; }

    public void ApplyDiscount(decimal percentage)
    {
        Total -= Total * percentage;
    }
}

This is different from:

public class Order
{
    public async Task SaveAsync(DbContext db)
    {
        await db.SaveChangesAsync();
    }
}

The second class mixes domain behavior with persistence.

Architecture checks can detect references to infrastructure types.

Enforce Application Service Boundaries

The application layer often coordinates use cases.

For example:

Controller
    |
    v
CreateOrderHandler
    |
    +---- Domain
    |
    +---- Repository
    |
    v
Result

An AI-generated controller that bypasses the application layer should fail the architecture check.

This creates a predictable rule:

Api -> Application

rather than:

Api -> Domain + Infrastructure + Database

Restrict External API Calls

Generated code may also introduce direct HTTP calls.

For example:

using var client = new HttpClient();

var response = await client.GetAsync(
    "https://example.internal/api/orders");

If the application architecture requires external calls to go through a dedicated integration layer, this should be prohibited.

A rule might be:

Api
  X
HttpClient

Api
  X
SocketsHttpHandler

Application
  X
External API implementation

Infrastructure
  -> Approved integration abstraction

This prevents infrastructure details from spreading through the application.

Enforce Dependency Injection

AI-generated code may instantiate dependencies directly.

For example:

var repository = new OrderRepository();

when the project expects dependency injection.

An architectural rule can flag direct construction of infrastructure services.

Preferred:

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

This is not merely a style preference.

It preserves dependency direction and makes testing easier.

Detect Service Locator Patterns

Generated code can also introduce:

var service = serviceProvider
    .GetRequiredService<IOrderService>();

inside business logic.

If the architecture forbids service locator usage, this should be detected automatically.

The rule can target:

IServiceProvider.GetService
IServiceProvider.GetRequiredService

outside approved composition-root locations.

Enforce EF Core Boundaries

If EF Core is isolated inside Infrastructure, architecture rules should prevent accidental references elsewhere.

For example:

Allowed:
Infrastructure.Persistence

Forbidden:
Domain
Application
Api

The rule should cover:

DbContext
DbSet
EntityFrameworkCore
EF query extensions
Migration APIs

This protects the persistence boundary.

Detect Raw SQL in the Wrong Layer

AI-generated code may introduce raw SQL:

await db.Database.ExecuteSqlRawAsync(
    "DELETE FROM Orders WHERE Id = {0}",
    orderId);

Even if parameterized correctly, this may violate the application's architecture if raw database operations belong exclusively in Infrastructure.

Architecture rules can therefore distinguish:

Infrastructure
   -> Raw SQL allowed

Application
   -> Raw SQL forbidden

Domain
   -> Raw SQL forbidden

Enforce Async Boundaries

Generated code can accidentally introduce synchronous database calls:

var orders = db.Orders.ToList();

when the application's architecture requires asynchronous I/O.

Rules can detect patterns such as:

.ToList()
.First()
.Single()
.SaveChanges()

on known database abstractions.

The preferred forms may be:

.ToListAsync()
.FirstAsync()
.SingleAsync()
.SaveChangesAsync()

This is particularly useful for server applications where blocking operations can reduce scalability.

Enforce CancellationToken Usage

Long-running application operations should often propagate cancellation.

For example:

public async Task<Order> GetOrderAsync(
    int id,
    CancellationToken cancellationToken)
{
    return await db.Orders
        .FirstAsync(
            x => x.Id == id,
            cancellationToken);
}

An AI-generated method may omit the token:

await db.Orders.FirstAsync(x => x.Id == id);

Architecture or code-quality rules can identify important asynchronous boundaries where cancellation should be propagated.

Test Business Logic Placement

Not every architectural rule can be detected through namespaces.

Consider:

if (order.Total > 1000)
{
    discount = 0.15m;
}

This may be business logic.

If the project's architecture requires pricing rules to live in the domain layer, a controller or repository containing this logic should trigger review.

These rules are harder to automate perfectly.

Use a combination of:

Static rules
+
Architecture tests
+
Code review

rather than expecting one mechanism to understand every architectural decision.

Add AI-Specific Rules

AI-generated pull requests introduce another concern: generated code can create large changes very quickly.

A useful pipeline should therefore identify:

AI-generated changes
       |
       v
Architecture validation
       |
       v
Security validation
       |
       v
Tests

The goal is not to reject AI-generated code automatically.

The goal is to apply the same or stronger validation to code that may have been produced at high speed.

Use a Layered Validation Pipeline

A practical CI pipeline might look like:

Pull Request
     |
     v
Build
     |
     v
Unit Tests
     |
     v
Architecture Tests
     |
     v
Static Analysis
     |
     v
Security Checks
     |
     v
Integration Tests
     |
     v
Review

Architecture tests should run early because dependency violations are often cheap to detect.

Return Actionable Errors

A failed architecture rule should explain what happened.

Bad:

Architecture test failed.

Better:

Architecture violation

File:
src/Api/OrdersController.cs

Rule:
API controllers must not reference Infrastructure.Persistence.

Detected:
MyApp.Infrastructure.Persistence.AppDbContext

Expected:
Use an application-layer service.

This is particularly important for AI coding workflows.

An agent can use the error to correct its implementation.

Make Rules Machine-Readable

Instead of only returning prose, expose structured findings.

{
  "rule": "API_NO_INFRASTRUCTURE",
  "severity": "error",
  "file": "OrdersController.cs",
  "line": 14,
  "message": "API layer cannot reference Infrastructure.Persistence."
}

This makes the output easier for:

  • CI systems

  • Pull-request bots

  • AI coding agents

  • IDE extensions

  • Reporting dashboards

Distinguish Errors From Warnings

Not every architectural issue should block a pull request.

For example:

Error:
Domain references Infrastructure

Warning:
Service contains more than 300 lines

Info:
Class has multiple dependencies

Use severity levels.

Critical architectural boundary
        |
        v
Block merge

Potential design smell
        |
        v
Review

This reduces false-positive fatigue.

Avoid Overly Strict Rules

Architecture rules can become counterproductive if they encode every style preference.

Suppose a team creates 200 rules.

Eventually developers may see:

Rule failed
Rule failed
Rule failed
Rule failed

even when the code is functionally correct.

AI agents can also struggle to make progress when constraints are contradictory.

Start with high-value boundaries:

No forbidden dependencies
No direct database access from API
No domain infrastructure references
No unauthorized external calls
No unsafe architectural shortcuts

Then expand gradually.

Measure False Positives

Every architecture system should measure false positives.

For example:

100 reported violations
80 genuine violations
20 false positives

Then:

Precision = 80%

If developers constantly override architecture warnings, the rules may be too broad.

Track:

Violation count
False-positive count
Override count
Fix rate
Time to resolution

Measure Architecture Drift

Architecture quality can be measured over time.

For example:

Month 1:
12 forbidden dependencies

Month 2:
7

Month 3:
3

Month 4:
1

This provides evidence that automated rules are actually helping.

Another useful metric is:

Architectural violations per 1,000 changed lines

The absolute number alone can be misleading as repositories grow.

Benchmark AI-Generated Pull Requests

To determine whether architecture rules are effective, create a benchmark.

Generate or collect pull requests containing:

Correct architecture
Layer violation
Database leakage
External API leakage
Dependency injection violation
Domain contamination
Improper async usage
Security boundary violation

Run the architecture pipeline.

Measure:

Detection Rate
False Positive Rate
Time to Detection
Time to Fix

This turns architectural validation into an engineering experiment.

Example Evaluation

Suppose 100 intentionally flawed pull requests are tested.

80 violations detected
10 false positives
10 missed

Then:

Detection Rate = 80%
False Positive Rate = 10%

After improving the rules:

92 violations detected
4 false positives
4 missed

The second rule set is clearly more useful.

Use Mutation Testing for Architecture Rules

A powerful technique is architectural mutation testing.

Start with valid code.

Then intentionally introduce violations:

Domain -> EF Core
Api -> DbContext
Application -> ASP.NET
Controller -> Raw SQL
Domain -> Infrastructure

Run the architecture suite.

Every mutation should be detected.

Valid Architecture
       |
       v
Mutation
       |
       v
Architecture Test
       |
       +---- Detected
       |
       +---- Missed

This tests whether the rules themselves are effective.

Protect Against Rule Bypass

AI-generated code may find alternative ways around simple rules.

For example, banning:

new AppDbContext()

does not prevent:

CreateDbContext();

or:

_serviceProvider.GetRequiredService<AppDbContext>();

The rule should focus on the architectural dependency rather than one syntax pattern.

This is why assembly and namespace analysis are often stronger than simple text matching.

Use Multiple Validation Layers

A robust system combines:

Text-level checks
      +
Syntax analysis
      +
Semantic analysis
      +
Assembly dependency analysis
      +
Runtime tests

Each catches a different class of problem.

Text
  -> Simple patterns

Syntax
  -> Code structures

Semantic
  -> Types and symbols

Assembly
  -> Dependency boundaries

Runtime
  -> Actual behavior

Architecture Rules for AI Agent Feedback

When an AI coding agent generates a pull request, the CI result can become feedback.

For example:

Architecture Check

FAIL

Rule:
API_NO_INFRASTRUCTURE

Found:
OrdersController -> AppDbContext

Suggested correction:
Move database access behind an application service.

The agent can then modify the implementation and submit another change.

This creates a feedback loop:

AI Generates Code
       |
       v
Architecture Tests
       |
       v
Violation
       |
       v
AI Corrects Code
       |
       v
Tests
       |
       v
Pass

Human review remains important, but deterministic architecture rules can handle repetitive checks.

Don't Let the AI Define Its Own Architecture

An AI coding agent should not be the final authority on architectural constraints.

The repository should contain explicit rules.

For example:

Architecture
- API depends on Application
- Application depends on Domain
- Domain cannot depend on Infrastructure
- Infrastructure implements persistence
- Controllers cannot access DbContext

The validation pipeline enforces those rules.

This makes architecture a property of the repository rather than a suggestion inside a prompt.

Store Architecture Rules With the Code

Architecture rules should live close to the repository.

For example:

architecture/
|
+-- dependencies.md
+-- rules.json
+-- forbidden-dependencies.json
+-- architecture-tests/

This makes the rules:

  • Version controlled

  • Reviewable

  • Testable

  • Reproducible

  • Available to AI coding agents

Architecture changes then become normal code changes.

Version Architecture Rules

Architecture evolves.

A project may initially use:

Repository Pattern

and later move toward:

Application Queries

Do not silently change the validation system.

Treat architecture changes as versioned decisions.

Architecture v1
     |
     v
Migration
     |
     v
Architecture v2

This prevents old rules from blocking legitimate modernization.

Common Mistakes

Relying Only on AI Instructions

Prompts can explain architecture, but they do not enforce it.

Checking Only Compilation

Code can compile while violating architectural boundaries.

Using Only Text Search

Simple string rules are easy to bypass.

Creating Too Many Rules

An excessive number of warnings creates noise.

Ignoring False Positives

Developers eventually stop trusting the system.

Blocking Every Violation

Some findings require human judgment rather than an automatic merge block.

Testing Only Existing Code

Architecture rules should also be tested against intentionally broken examples.

Ignoring Project References

A forbidden dependency may exist at the project level even if source files look clean.

Treating Architecture as Static

Architecture changes as systems evolve.

Best Practices

Define Boundaries Before Writing Rules

You cannot automate an architecture that has not been clearly defined.

Prefer Dependency Rules

Dependency direction is usually more reliable than stylistic rules.

Use Multiple Detection Layers

Combine syntax, semantic, assembly, and runtime validation.

Keep Errors Actionable

Tell developers exactly what dependency is forbidden and where it was found.

Use Severity Levels

Block critical architectural violations while allowing lower-risk findings to receive review.

Measure Precision and Recall

Architecture tooling should be evaluated like any other automated system.

Use Mutation Testing

Intentionally break architecture and verify that the tests catch it.

Keep Rules Version Controlled

Architecture constraints should evolve with the repository.

Give AI Agents Structured Feedback

Machine-readable findings make automated correction easier.

Keep Humans in the Loop

Automated rules enforce known boundaries; architects and developers still handle decisions that require context.

A Practical Architecture Validation Pipeline

A production-oriented setup can look like this:

AI-Generated Pull Request
          |
          v
       Build
          |
          v
    Unit Tests
          |
          v
 Architecture Tests
          |
          v
 Static Analysis
          |
          v
 Security Checks
          |
          v
 Integration Tests
          |
          v
     Human Review
          |
          v
        Merge

The important change is that architecture is no longer something reviewers discover only by reading the diff.

It becomes a continuously tested property of the application.

Conclusion

AI-generated pull requests can accelerate .NET development, but they also increase the speed at which architectural mistakes can enter a codebase. A generated controller can bypass application services, a domain class can accidentally reference Entity Framework Core, or an infrastructure dependency can spread into projects that were designed to remain independent.

The solution is not to rely on better prompts alone.

Architecture needs executable rules.

By defining dependency direction, enforcing namespace and assembly boundaries, restricting database and external-service access, validating project references, testing architectural mutations, and integrating the checks into CI, teams can turn architectural principles into measurable engineering constraints.

The most effective approach is layered. Static analysis catches simple violations, architecture tests enforce dependency boundaries, integration tests validate behavior, and human review handles decisions that require context.

For AI-assisted development, this creates an important safety mechanism: the AI can generate code quickly, but the repository still decides what kind of code is allowed to become part of the system.