Software Architecture/Engineering  

Building Architecture Gates for Copilot-Generated Pull Requests in .NET

Introduction

AI coding assistants can generate a surprising amount of code in a short time. A developer can describe a feature, ask an AI agent to implement it, and receive changes across controllers, services, repositories, tests, and configuration files.

That speed is useful, but it introduces a new engineering problem.

A pull request can compile, pass unit tests, and still violate the architecture of the application.

For example, a .NET application may follow this structure:

API
 |
 v
Application
 |
 v
Domain
 |
 v
Infrastructure

But an AI-generated change might accidentally create:

API
 |
 +------> Infrastructure
 |
 +------> Database

The code may work.

The tests may pass.

The architecture is still wrong.

This is why architecture gates are becoming increasingly useful for AI-assisted development.

Instead of asking only:

"Does the code work?"

the pull-request pipeline should also ask:

"Does the code follow the architecture?"

What Is an Architecture Gate?

An architecture gate is an automated validation step that checks whether a code change follows predefined architectural rules.

A traditional CI pipeline might look like:

Pull Request
     |
     v
Build
     |
     v
Unit Tests
     |
     v
Integration Tests
     |
     v
Merge

An architecture-aware pipeline adds another layer:

Pull Request
     |
     v
Build
     |
     v
Tests
     |
     v
Architecture Gate
     |
     v
Security / Quality Checks
     |
     v
Human Review
     |
     v
Merge

This becomes particularly useful when AI agents are generating or modifying large portions of the codebase.

Why AI-Generated Code Needs Architecture Checks

AI systems are very good at producing locally reasonable code.

The problem is that software architecture is a global concern.

Suppose the project has:

Domain
Application
Infrastructure
API

A developer may know that:

Domain

must not depend on:

Infrastructure

An AI agent may not always preserve that rule unless the repository provides enough context and the workflow enforces it.

This creates a useful distinction:

AI Instructions
       |
       v
"Please follow Clean Architecture."

versus:

Automated Rule
       |
       v
"Domain cannot reference Infrastructure."

The second is enforceable.

Architecture as an Executable Contract

Architecture documentation is useful, but documentation alone does not prevent violations.

Consider this rule:

The Domain layer must not depend on Infrastructure.

You can document it in:

README
Architecture Document
Developer Guide

But an automated architecture test can turn it into an executable rule.

Conceptually:

Architecture Rule
       |
       v
Automated Test
       |
       +---- Pass ---> Pull Request continues
       |
       +---- Fail ---> Pull Request blocked

This makes architecture part of the software delivery process.

Example .NET Architecture

Consider a typical ASP.NET Core solution:

src/
├── Store.Api/
├── Store.Application/
├── Store.Domain/
├── Store.Infrastructure/
└── Store.Contracts/

A reasonable dependency model might be:

Store.Api
    |
    v
Store.Application
    |
    v
Store.Domain

Store.Infrastructure
    |
    +----> Store.Application
    |
    +----> Store.Domain

The important restriction is:

Store.Domain
    X
Store.Infrastructure

and potentially:

Store.Application
    X
Store.Api

The exact dependency direction depends on the architecture, but it should be explicit.

Enforcing Architecture With NetArchTest

One practical approach for .NET applications is using an architecture-testing library such as NetArchTest.

The idea is simple: inspect assemblies and assert architectural rules.

For example:

[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
    var result = Types
        .InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn("Store.Infrastructure")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

The exact namespaces and assembly structure will vary by application.

The important concept is that architecture becomes testable.

Testing Layer Dependencies

Suppose the application uses these layers:

API
Application
Domain
Infrastructure

You can define rules such as:

Domain
  -> No Infrastructure

Domain
  -> No API

Application
  -> No API

Infrastructure
  -> Can depend on Application and Domain

A test suite can represent those rules.

For example:

[Fact]
public void Domain_Should_Not_Depend_On_Api()
{
    var result = Types
        .InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn("Store.Api")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Now an accidental dependency can fail the pull request automatically.

Architecture Tests vs Unit Tests

These tests answer different questions.

Test TypeMain Question
Unit testDoes this component behave correctly?
Integration testDo components work together?
API testDoes the API behave correctly?
Architecture testDoes the code follow structural rules?
Security testDoes the code meet security expectations?

A project should not rely on one category alone.

For example:

Unit Tests       -> Correct behavior
Integration Tests -> Correct integration
Architecture Tests -> Correct structure
Security Checks  -> Safer implementation

Together they provide stronger validation.

AI Agents and Dependency Direction

Imagine an AI agent is asked:

Add persistence for Customer preferences.

The agent creates:

CustomerPreferencesService
       |
       v
DbContext

inside the Application layer.

The code may compile.

But suppose the architecture requires persistence to remain inside Infrastructure.

An architecture gate can detect the dependency.

Without the gate:

AI Change
   |
   v
Build Pass
   |
   v
Tests Pass
   |
   v
Merge

With the gate:

AI Change
   |
   v
Build Pass
   |
   v
Tests Pass
   |
   v
Architecture Gate
   |
   X
Violation

The pull request stops before the architectural problem reaches the main branch.

Architecture Rules Should Be Specific

Avoid vague rules such as:

"Keep the architecture clean."

That is difficult to automate.

Instead define concrete rules:

Domain cannot reference Infrastructure.

Controllers cannot directly access DbContext.

Application handlers cannot depend on ASP.NET Core controllers.

Infrastructure implementations cannot be referenced by Domain.

API projects cannot contain database queries.

These rules can be tested.

Preventing Controllers From Becoming Business Logic

A common architecture rule is keeping controllers thin.

Bad example:

[HttpPost]
public async Task<IActionResult> CreateOrder(
    CreateOrderRequest request)
{
    var customer = await db.Customers
        .FirstAsync(x => x.Id == request.CustomerId);

    var order = new Order();

    order.Total = request.Items.Sum(
        x => x.Price * x.Quantity);

    db.Orders.Add(order);

    await db.SaveChangesAsync();

    return Ok(order);
}

The controller is handling:

Database access
Business logic
Calculation
Persistence
HTTP response

A more structured design might be:

Controller
    |
    v
Application Service
    |
    v
Domain
    |
    v
Infrastructure

The controller becomes simpler:

[HttpPost]
public async Task<IActionResult> CreateOrder(
    CreateOrderRequest request,
    CancellationToken cancellationToken)
{
    var result = await service.CreateOrderAsync(
        request,
        cancellationToken);

    return Ok(result);
}

An architecture gate can help prevent developers, including AI agents, from bypassing the intended layers.

Restricting Direct DbContext Usage

Suppose the architecture says:

Only Infrastructure can directly use AppDbContext.

An architecture test can search for dependencies on the database assembly.

Conceptually:

API
  X
DbContext

Application
  X
DbContext

Infrastructure
  OK
DbContext

This prevents a common form of architectural drift.

Namespace Rules

Architecture does not only exist at the project level.

Namespaces can also express boundaries.

For example:

Store.Domain.Entities
Store.Domain.ValueObjects
Store.Domain.Services

A rule could prevent domain entities from depending on:

Microsoft.AspNetCore.*
Microsoft.EntityFrameworkCore.*

This helps keep the domain model independent from infrastructure frameworks.

Example Framework Dependency Test

A simplified test might look like:

[Fact]
public void Domain_Should_Not_Depend_On_EfCore()
{
    var result = Types
        .InAssembly(typeof(Order).Assembly)
        .ShouldNot()
        .HaveDependencyOn(
            "Microsoft.EntityFrameworkCore")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

Again, the correct rule depends on the project's architecture.

The purpose is not to ban frameworks universally.

The purpose is to enforce deliberate boundaries.

Architecture Gates in CI

An architecture test is most useful when it runs automatically.

A .NET CI pipeline can conceptually look like:

Pull Request
      |
      v
Restore
      |
      v
Build
      |
      v
Unit Tests
      |
      v
Integration Tests
      |
      v
Architecture Tests
      |
      v
Security Checks
      |
      v
Review
      |
      v
Merge

The architecture tests should run on every relevant pull request.

For example:

dotnet restore

dotnet build --no-restore

dotnet test --no-build

If architecture tests are included in the test project, they become part of the normal validation process.

Making the Gate Fail Clearly

A failed architecture test should explain what went wrong.

Bad output:

Test Failed.

Better:

Architecture violation:

Store.Domain has a dependency on
Store.Infrastructure.

Expected:
Domain -> Infrastructure = prohibited.

Clear failure messages reduce debugging time.

Architecture Gates and Pull Requests

A useful pull-request workflow is:

Developer or AI Agent
          |
          v
Code Changes
          |
          v
Pull Request
          |
          +--> Build
          |
          +--> Tests
          |
          +--> Architecture
          |
          +--> Security
          |
          v
Human Review

The human reviewer can then focus more on business correctness and design decisions instead of manually checking every dependency relationship.

AI Instructions Still Have Value

Architecture gates should not replace repository instructions.

They serve different purposes.

Instructions can explain:

Use the application service pattern.

Keep controllers thin.

Use domain services for business rules.

Architecture tests enforce:

Controller cannot reference Infrastructure.

A useful model is:

Instructions
    +
Tests
    +
CI Policies
    =
Stronger AI-Assisted Development

The AI receives guidance.

The automated system verifies the result.

Architecture Gates for Naming

Architecture tests can also enforce naming conventions.

For example:

Commands must end with Command.
Handlers must end with Handler.
Controllers must end with Controller.
Repositories must end with Repository.

A test could conceptually enforce:

[Fact]
public void Handlers_Should_End_With_Handler()
{
    var result = Types
        .InAssembly(typeof(CreateOrderHandler).Assembly)
        .That()
        .ResideInNamespace("Store.Application.Handlers")
        .Should()
        .HaveNameEndingWith("Handler")
        .GetResult();

    Assert.True(result.IsSuccessful);
}

This can be particularly useful when AI-generated code introduces inconsistent naming.

Architecture Gates for Interfaces

Another useful rule is ensuring implementations follow the expected abstraction.

For example:

IOrderRepository
       ^
       |
OrderRepository

An automated test can verify that classes in the repository namespace implement the expected interfaces.

This can catch incomplete AI-generated changes.

Architecture Gates for Dependency Injection

Suppose the project requires:

Application services
        |
        v
Interfaces
        |
        v
Infrastructure implementations

A generated class that directly constructs a dependency is a potential architectural problem.

For example:

var repository = new SqlOrderRepository();

could bypass dependency injection.

A better pattern is:

public class OrderService(
    IOrderRepository repository)
{
    private readonly IOrderRepository repository = repository;
}

Architecture rules can detect some of these patterns, while code-quality analyzers can handle others.

Architecture Gates for Project References

Project references provide another strong boundary.

Suppose:

Store.Domain.csproj

should never reference:

Store.Infrastructure.csproj

This can be checked before runtime.

For example, the project dependency graph should remain:

Store.Api
   |
   +--> Store.Application
   |
   +--> Store.Infrastructure

Store.Application
   |
   +--> Store.Domain

Store.Infrastructure
   |
   +--> Store.Application
   |
   +--> Store.Domain

A dependency graph makes architectural violations easier to understand.

Architecture Drift

Architecture tends to degrade gradually.

One developer adds a shortcut.

Another copies it.

An AI agent learns the existing pattern.

Soon the shortcut becomes normal.

The progression can look like:

Clean Architecture
       |
       v
One Exception
       |
       v
More Exceptions
       |
       v
Repeated Pattern
       |
       v
Architecture Drift

Architecture gates interrupt this process.

Instead of relying on developers to remember every rule, the system continuously checks them.

Common Mistakes

Mistake 1: Relying Only on Documentation

Documentation explains architecture but does not enforce it.

Mistake 2: Creating Too Many Rules

An architecture suite with hundreds of fragile rules becomes difficult to maintain.

Mistake 3: Blocking Legitimate Designs

Rules should represent actual architectural decisions, not personal preferences.

Mistake 4: Ignoring Existing Violations

If the current codebase already violates a rule, introducing the gate may immediately block every pull request.

Mistake 5: Writing Tests That Are Too Fragile

Architecture tests should survive reasonable refactoring.

Mistake 6: Treating Architecture Tests as Business Tests

They solve different problems.

Mistake 7: Assuming AI Will Follow Instructions Forever

Automated enforcement is more reliable than instructions alone.

Introducing Gates Into a Legacy Application

Large applications often already contain architectural violations.

Do not try to fix everything at once.

Instead, establish a baseline.

For example:

Current Violations
       |
       v
Document Baseline
       |
       v
Block New Violations
       |
       v
Reduce Existing Violations

This is sometimes called a ratcheting approach.

The key idea is:

Do not allow the architecture to become worse while gradually improving it.

Handling Existing Violations

Suppose the current codebase has:

15 Domain -> Infrastructure violations

A new architecture gate can initially identify those violations.

Then the team can establish a rule such as:

New violations = 0

The existing technical debt can be addressed separately.

This is often more practical than requiring a complete architectural rewrite before automation can be introduced.

Testing AI-Generated Pull Requests

A useful experiment is to compare ordinary and AI-generated pull requests.

For each pull request, record:

Build Result
Test Result
Architecture Result
Security Result
Review Comments
Rework Required

Then categorize architecture failures:

Layer Violation
Namespace Violation
Naming Violation
Dependency Violation
Pattern Violation

This helps the engineering team understand where AI-generated changes need additional guardrails.

Architecture Quality Score

Organizations can create an internal scorecard.

For example:

CheckResult
BuildPass
Unit TestsPass
Integration TestsPass
ArchitecturePass
SecurityPass
Code QualityPass
Human ReviewPending

The pull request becomes:

Ready for Merge

only after the required checks pass.

The exact rules should be determined by the organization.

Advantages

Prevents Architectural Drift

Rules are continuously checked instead of relying only on developer discipline.

Useful for AI-Generated Code

AI agents can generate many files quickly, making automated structural validation valuable.

Faster Code Review

Reviewers can spend less time checking basic dependency rules.

Repeatable

The same rules run on every pull request.

Works With Existing .NET Workflows

Architecture tests can run alongside normal build and test processes.

Makes Architecture Executable

Architectural decisions become machine-verifiable rules.

Disadvantages and Limitations

Initial Setup Takes Time

The team must define meaningful architectural rules.

Existing Technical Debt Can Cause Failures

Legacy projects may contain many violations.

Rules Require Maintenance

Architecture evolves, so tests must evolve too.

Not Everything Can Be Automated

Business architecture and design intent sometimes require human judgment.

Poor Rules Create Friction

Overly restrictive tests can block legitimate development.

Architecture Tests Do Not Guarantee Good Software

A project can follow dependency rules and still contain incorrect business logic.

Recommended Architecture Gate Strategy

For a typical .NET application, start with a small set of high-value rules:

1. Domain cannot depend on Infrastructure.

2. Domain cannot depend on API.

3. Application cannot depend on API.

4. Controllers cannot directly access DbContext.

5. Infrastructure dependencies remain behind defined abstractions.

6. Required naming conventions are enforced.

7. New projects must follow approved dependency direction.

Once these rules are stable, add more specialized checks.

A Complete AI-Assisted Pull Request Workflow

A mature workflow can look like this:

Developer
   |
   v
AI Agent
   |
   v
Code Changes
   |
   v
Pull Request
   |
   +-------------------+
   |                   |
   v                   v
Build              Unit Tests
   |                   |
   +---------+---------+
             |
             v
      Integration Tests
             |
             v
      Architecture Gates
             |
             v
       Security Checks
             |
             v
       Code Quality
             |
             v
       Human Review
             |
             v
          Merge

The important idea is that AI-generated code enters the same engineering control system as human-generated code.

The source of the code does not change the quality requirements.

Practical Implementation Checklist

Before introducing architecture gates, define:

[ ] What is the intended architecture?

[ ] Which project dependencies are allowed?

[ ] Which dependencies are prohibited?

[ ] Which namespaces have restrictions?

[ ] Which naming conventions matter?

[ ] Which framework dependencies are restricted?

[ ] Which rules should block a pull request?

[ ] Which existing violations are accepted temporarily?

[ ] How will architecture tests run in CI?

[ ] How will violations be reported?

[ ] Who owns the architecture rules?

[ ] How will the rules change when architecture evolves?

Once these questions are answered, implementation becomes much easier.

Conclusion

AI-generated code does not remove the need for architecture; if anything, it makes automated architectural validation more valuable. An AI agent can generate a working implementation very quickly, but a working implementation can still violate dependency boundaries, introduce unwanted framework coupling, bypass application layers, or create long-term architecture drift. Architecture gates turn important design rules into executable tests that can run automatically during every pull request. For .NET applications, this can include checking project dependencies, namespace boundaries, layer relationships, controller responsibilities, framework dependencies, naming conventions, and other structural rules. The best approach is to start with a small set of meaningful rules, run them alongside builds and tests, and gradually expand them as the team gains confidence. The goal is not to create more CI failures. The goal is to make the architecture easier to protect, especially when developers and AI agents are both making changes at a much faster pace.