Architecture problems rarely appear in a single pull request.
A developer adds a new dependency between two projects. Another feature introduces a database call into an application service. Later, a utility project starts referencing infrastructure code. Each change may look reasonable in isolation, but over time the architecture becomes different from what the team originally designed.
This is architecture drift.
For large .NET applications, architecture drift can be particularly difficult to detect because the solution may contain dozens or hundreds of projects, shared libraries, APIs, background workers, infrastructure components, and test projects.
Code reviews can catch some of these problems, but relying entirely on reviewers is not scalable.
A better approach is to turn important architectural rules into automated checks and run them as part of pull requests.
The goal is not to prevent developers from changing the architecture. The goal is to make architectural changes explicit, reviewable, and intentional.
What Is Architecture Drift?
Architecture drift occurs when the implementation gradually moves away from the intended architectural boundaries.
Consider a simple .NET solution:
MyCompany.Application
|
v
MyCompany.Domain
MyCompany.Infrastructure
|
v
MyCompany.Application
|
v
MyCompany.Domain
MyCompany.Api
|
v
MyCompany.Application
The intended dependency direction might be:
API
|
v
Application
|
v
Domain
Infrastructure ---> Application
Infrastructure ---> Domain
Now imagine a developer adds:
MyCompany.Domain
|
v
MyCompany.Infrastructure
The project still compiles.
Tests may still pass.
The application may still work.
But the architecture has changed.
That is exactly the type of problem automated architecture validation should catch.
Why Pull Requests Are a Good Enforcement Point
Architecture checks are particularly useful in pull requests because that is where changes are reviewed.
A typical workflow becomes:
Developer changes code
|
v
Pull Request
|
+--> Build
|
+--> Unit Tests
|
+--> Static Analysis
|
+--> Architecture Tests
|
v
Review
|
v
Merge
If an architectural rule fails, the pull request can be blocked before the violation reaches the main branch.
This is much cheaper than discovering the problem months later.
Architecture Rules Should Be Explicit
Before implementing automation, define the rules.
For example:
Domain must not reference Infrastructure
Domain must not reference Web
Application must not reference Web
API may reference Application
Infrastructure may reference Application
Controllers should not directly access DbContext
Not every rule needs to be automated.
Start with boundaries that are:
Important
Stable
Objective
Easy to verify
A good automated rule should produce a clear answer:
Allowed
or:
Violation
Project References Are a Strong Starting Point
In .NET, project references provide a useful architectural signal.
Suppose the solution contains:
src/
MyApp.Api/
MyApp.Application/
MyApp.Domain/
MyApp.Infrastructure/
The project graph can be represented as:
MyApp.Api
|
v
MyApp.Application
|
v
MyApp.Domain
MyApp.Infrastructure
|
+----> MyApp.Application
|
+----> MyApp.Domain
A dependency from Domain to Infrastructure should therefore be rejected.
Inspecting Project References
A simple approach is to inspect .csproj files.
For example:
<ProjectReference Include="..\MyApp.Domain\MyApp.Domain.csproj" />
The important information is:
Source Project
|
v
Referenced Project
You can build a dependency graph from those relationships.
For larger repositories, it is useful to normalize project names before evaluating rules.
Define Architecture Rules as Data
Avoid scattering architectural rules throughout scripts.
Instead, represent them explicitly.
For example:
{
"rules": [
{
"source": "MyApp.Domain",
"forbidden": [
"MyApp.Infrastructure",
"MyApp.Api"
]
},
{
"source": "MyApp.Application",
"forbidden": [
"MyApp.Api"
]
}
]
}
This has an important advantage.
Developers can review architecture rules independently from the validation engine.
Build a Dependency Graph
The validator can convert project references into a graph:
Project A
|
+----> Project B
|
+----> Project C
For example:
Api
|
+--> Application
|
+--> Infrastructure
Application
|
+--> Domain
Infrastructure
|
+--> Application
+--> Domain
Then compare the actual graph against the allowed dependency model.
A violation becomes a graph problem rather than a subjective code-review discussion.
A Simple C# Dependency Model
A small internal representation is enough for many repositories:
public sealed record ProjectDependency(
string Source,
string Target);
You can then build a collection:
var dependencies = new List<ProjectDependency>
{
new("MyApp.Api", "MyApp.Application"),
new("MyApp.Application", "MyApp.Domain"),
new("MyApp.Infrastructure", "MyApp.Application")
};
The architecture validator can inspect this graph and identify forbidden edges.
Detecting Forbidden Dependencies
A simple validator could look like this:
public sealed record ArchitectureRule(
string Source,
IReadOnlySet<string> ForbiddenTargets);
public static IReadOnlyList<string> Validate(
IEnumerable<ProjectDependency> dependencies,
IEnumerable<ArchitectureRule> rules)
{
var violations = new List<string>();
foreach (var dependency in dependencies)
{
var rule = rules.FirstOrDefault(
x => x.Source.Equals(
dependency.Source,
StringComparison.OrdinalIgnoreCase));
if (rule is null)
continue;
if (rule.ForbiddenTargets.Contains(dependency.Target))
{
violations.Add(
$"{dependency.Source} must not reference " +
$"{dependency.Target}.");
}
}
return violations;
}
The important part is not the implementation itself.
The important part is that the architecture rule becomes executable.
Fail the Pull Request on Violations
The validator should return a non-zero exit code when architecture rules fail.
For example:
var violations = Validate(dependencies, rules);
foreach (var violation in violations)
{
Console.Error.WriteLine(
$"ARCHITECTURE VIOLATION: {violation}");
}
return violations.Count == 0 ? 0 : 1;
The CI pipeline can then treat the result as a failed check.
This turns architecture from documentation into an enforceable engineering constraint.
Architecture Tests Inside the .NET Solution
Another approach is to implement architecture rules as automated tests.
For example:
[Fact]
public void Domain_should_not_reference_infrastructure()
{
var result = ArchitectureAnalyzer
.Analyze("MyApp.Domain");
Assert.DoesNotContain(
result.Dependencies,
dependency =>
dependency.Contains("MyApp.Infrastructure"));
}
The exact architecture-testing mechanism can vary, but the principle is the same:
Architecture rule
|
v
Executable test
|
v
CI
This approach works particularly well when architecture rules need more than simple project-reference checks.
Namespace-Level Architecture Rules
Project dependencies are only one layer.
A project can have internal architectural boundaries based on namespaces.
For example:
MyApp.Application.Commands
MyApp.Application.Queries
MyApp.Application.Services
MyApp.Application.Infrastructure
You may want to prevent:
Commands
|
X
Infrastructure
even though everything exists inside the same project.
Namespace-level rules require deeper analysis of source code and symbol references.
This is where static analysis becomes more useful than simply parsing project files.
API Boundary Rules
Another useful rule is preventing controllers from directly accessing persistence infrastructure.
For example, this may be considered a violation:
public class OrdersController : ControllerBase
{
private readonly AppDbContext _db;
public OrdersController(AppDbContext db)
{
_db = db;
}
}
The preferred architecture might require:
Controller
|
v
Application Service
|
v
Repository / Persistence
The validator can detect references to restricted types or namespaces.
For example:
MyApp.Api
X MyApp.Infrastructure.Persistence.AppDbContext
This is a more precise architectural rule than simply checking project references.
Dependency Direction Is More Important Than Project Count
Teams sometimes create many projects and assume that means the application has a clean architecture.
It does not.
A solution with:
30 projects
can still have poor boundaries.
A better architecture check focuses on dependency direction.
For example:
Domain
^
|
Application
^
|
API
The exact structure can vary, but dependencies should reflect the intended design.
Detect Circular Dependencies
Architecture validation should also detect cycles.
For example:
Project A
|
v
Project B
|
v
Project C
|
v
Project A
This creates:
A -> B -> C -> A
Circular dependencies make systems harder to understand and evolve.
A simple depth-first search can identify cycles.
Conceptually:
Visit A
|
v
Visit B
|
v
Visit C
|
v
A already in current path
|
v
Cycle detected
The validator should report the complete path:
Circular dependency detected:
MyApp.A
-> MyApp.B
-> MyApp.C
-> MyApp.A
That is much more useful than simply reporting:
Architecture failed.
Detect Dependency Growth
Architecture drift is not always an outright violation.
Sometimes the problem is uncontrolled dependency growth.
For example:
Application
|
+--> Domain
+--> Contracts
+--> Logging
+--> Infrastructure
+--> Messaging
+--> Storage
+--> ExternalApi
+--> Reporting
The architecture may still technically work, but the Application project is becoming tightly coupled.
Track dependency count over time.
For example:
| Pull Request | Application Dependencies |
|---|
| PR #101 | 4 |
| PR #125 | 5 |
| PR #170 | 7 |
| PR #220 | 11 |
A sudden increase should trigger architectural discussion even if no hard rule is violated.
Architecture Drift Can Be Measured
You can define a simple drift score based on violations and dependency changes.
For example:
Drift Score =
Critical Violations × 5
+ Normal Violations × 2
+ New Dependencies
+ Circular Dependencies × 5
The exact formula is less important than consistency.
Avoid presenting such a score as an objective measure of architecture quality. It is a signal for engineering review.
Pull Request Diff Analysis
A powerful improvement is to analyze only architectural changes introduced by the pull request.
Instead of scanning everything:
Entire Repository
|
v
Architecture Analyzer
you can compare:
Base Branch
|
v
Current Branch
|
v
Dependency Difference
For example:
Before:
Api -> Application
Application -> Domain
After:
Api -> Application
Application -> Domain
Domain -> Infrastructure
The validator can report:
New forbidden dependency:
MyApp.Domain
->
MyApp.Infrastructure
This makes the feedback highly relevant to the developer who opened the pull request.
CI Pipeline Integration
A generic CI pipeline might look like:
steps:
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build
- name: Architecture Validation
run: dotnet run --project tools/ArchitectureValidator
The architecture validator becomes another quality gate.
The exact CI syntax will depend on the platform being used.
Keep Architecture Failures Actionable
A bad error message:
Architecture validation failed.
A useful error message:
Architecture violation
Source:
MyApp.Domain
Forbidden dependency:
MyApp.Infrastructure
Detected in:
MyApp.Domain.csproj
Rule:
Domain projects must not depend on Infrastructure.
Suggested action:
Move the dependency behind an abstraction or
relocate the implementation to Infrastructure.
Good diagnostics reduce the time required to fix violations.
Allow Explicit Exceptions
Not every architectural exception is wrong.
A team may intentionally allow a dependency for a specific reason.
Do not encourage developers to bypass the validator globally.
Instead, support controlled exceptions.
For example:
{
"exceptions": [
{
"source": "MyApp.Application",
"target": "MyApp.Legacy",
"expires": "2026-12-31",
"reason": "Temporary migration dependency"
}
]
}
The expiration date is important.
Without expiration, temporary exceptions tend to become permanent architecture.
Treat Architecture Rules as Code
Architecture rules should be version controlled.
A change to:
Allowed dependencies
Forbidden namespaces
Exceptions
Thresholds
should go through the same pull-request process as application code.
This creates an important feedback loop:
Architecture changes
|
v
Pull Request
|
v
Architecture rules reviewed
|
v
Validator updated
|
v
New architecture becomes enforceable
The architecture therefore evolves intentionally.
Common Mistakes
Automating Every Rule
Not every architectural decision can be expressed as a static rule.
Automate stable and objective boundaries first.
Blocking All Exceptions
Real systems sometimes require transitional architecture.
Use explicit, documented exceptions instead.
Ignoring Legacy Code
If an existing repository has hundreds of violations, immediately blocking every pull request may make the system unusable.
A better migration strategy is:
Existing violations
|
v
Baseline
|
v
Block new violations
|
v
Reduce baseline gradually
Reporting Only the Number of Violations
Developers need the source, target, rule, and location.
Checking Only Project References
Some important architecture violations happen at namespace, type, or API levels.
Making Architecture Validation Too Slow
Pull-request checks should be fast enough to run frequently.
Cache where appropriate and avoid unnecessarily analyzing unrelated projects.
A Practical Migration Strategy
For an existing .NET repository, start with project-level dependencies.
Phase 1: Discover
Generate the current dependency graph.
Projects
|
v
References
|
v
Dependency Graph
Phase 2: Identify
Find:
Circular dependencies
Unexpected dependencies
High-dependency projects
Layer violations
Phase 3: Baseline
Document existing violations without immediately blocking the team.
Phase 4: Enforce New Changes
Fail pull requests only when they introduce new violations.
Phase 5: Reduce Existing Drift
Create separate engineering work to remove baseline violations.
This approach avoids turning architecture enforcement into a large migration project.
Best Practices
Keep architectural rules small and understandable.
Prefer:
Domain -> must not reference Infrastructure
over a complicated rule that nobody can explain.
Run architecture validation on every pull request.
Keep rules in source control.
Make failures actionable.
Use temporary exceptions with expiration dates.
Track dependency growth.
Detect circular dependencies.
Separate architectural enforcement from subjective architectural review.
Most importantly, treat architecture validation as a guardrail rather than a replacement for engineering judgment.
Frequently Asked Questions
Can architecture drift be detected without analyzing source code?
Yes. Project references alone can detect many important dependency violations. More advanced rules require namespace or symbol-level analysis.
Should architecture tests run on every pull request?
For stable rules, yes. Pull-request execution provides fast feedback before architectural changes reach the main branch.
What should happen with existing violations?
Create a baseline and prevent new violations first. Then gradually reduce the existing debt.
Can architecture rules change?
Absolutely. Architecture evolves as systems evolve. Changes to architectural rules should themselves be reviewed carefully because they modify the boundaries enforced by the development process.
Should architecture validation replace code review?
No. Automated checks handle objective constraints. Human review is still required for architectural trade-offs, design quality, maintainability, and business context.
Conclusion
Architecture drift is usually gradual. A single unwanted dependency rarely breaks a production application, which is why these problems can remain invisible for a long time. The real cost appears later when boundaries become unclear, components become tightly coupled, and changes require touching more parts of the system.
.NET projects provide enough structural information to automate many of these checks. Project references, namespaces, type dependencies, circular-reference detection, and dependency growth can all become useful architecture signals.
The most effective approach is to move architectural rules into the pull-request workflow. Start with a small set of stable rules, establish a baseline for existing problems, block new violations, and provide clear diagnostics when something fails.
When architecture becomes an automated quality gate, developers do not have to rely on memory or documentation alone. The repository itself helps enforce the design decisions the team has agreed to maintain.