Enforcing Clean Architecture

Rule ND1412 Architecture

Ensures strict inward dependency flow between layers: only Presentation -> Application -> Domain. Flags outward or sideways dependencies (e.g. Domain -> Infrastructure).

How to fix

1) Move business rules used by multiple layers into Domain or Application. 2) Replace direct references to outer layers with abstractions defined in an inner layer. 3) Implement those abstractions in Infrastructure/Presentation, injected from the composition root. 4) Move DTOs/view models to their boundary layer; map at boundaries instead of sharing concrete outer-layer types.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Enforcing Clean Architecture</Name>
// <Id>ND1412:EnforcingCleanArchitecture</Id>
warnif count > 0
      
let getLayerTypes = new Func<string, HashSet<IType>>(
  names => names.Split('|').SelectMany(name =>
    Application.Namespaces.Where(n => n.Name.EndsWith("." + name) || n.Name.Contains("." + name + ".")).ChildTypes()
    .Concat(Application.Assemblies.Where(a => a.Name.EndsWith("." + name)).ChildTypes())
  ).ToHashSetEx()
)

// Define layers 
let layers = new[] {
    new { Name = "Domain", 
          Types = getLayerTypes("Domain|Entities") },
    new { Name = "Application", 
          Types = getLayerTypes("Application|UseCases") },
    new { Name = "Infrastructure", 
          Types = getLayerTypes("Infrastructure|Persistence") },
    new { Name = "Presentation", 
          Types = getLayerTypes("Presentation|UI|API|Web") }
}

// Define forbidden dependencies as anonymous types
let forbiddenDeps = new[] {
    new { From = "Domain",         To = "Application" },
    new { From = "Domain",         To = "Infrastructure" },
    new { From = "Domain",         To = "Presentation" },
    new { From = "Application",    To = "Infrastructure" },
    new { From = "Application",    To = "Presentation" },
    new { From = "Infrastructure", To = "Presentation" }
}

// Search for forbidden dependencies
from dep in forbiddenDeps
let sourceLayer = layers.First(l => l.Name == dep.From)
let targetLayer = layers.First(l => l.Name == dep.To)
from tUser  in sourceLayer.Types.UsingAny(targetLayer.Types)
let typesUsed = targetLayer.Types.Intersect(tUser.TypesUsed)
from tUsed in typesUsed
select new {
    tUser,
    tUsed,
    error = dep.From + " must not use " + dep.To,
    Debt = 7.ToMinutes().ToDebt(),
    Severity = Severity.High
}

// <Explanation>
// The {0} shouldn't use {1} because {2}
// </Explanation>     

// <Desc>
// Ensures strict inward dependency flow between layers: only
// Presentation -> Application -> Domain. Flags outward or sideways
// dependencies (e.g. Domain -> Infrastructure).
// </Desc>

// <HowToFix>
// 1) Move business rules used by multiple layers into Domain or Application.
// 2) Replace direct references to outer layers with abstractions defined in
//    an inner layer.
// 3) Implement those abstractions in Infrastructure/Presentation, injected
//    from the composition root.
// 4) Move DTOs/view models to their boundary layer; map at boundaries
//    instead of sharing concrete outer-layer types.
// </HowToFix>

Architecture: all 13 items