Architecture & Boundaries · Lesson 14

Check actual project dependency direction

Compare a declared graph fixture with a checker that reads real project files externally.

External .NET 10 SDK; five real projects with explicit ProjectReference items. Not executed by the single-file .NET 9 playground.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The architectural boundary

An architecture rule becomes more useful when a concrete check can detect its violation. This lesson distinguishes a portable rule test over declared edge data from an external checker that reads real project files. DependencyRule states allowed directions for Domain, Application, Infrastructure, and Host. The prepared edge fixture deliberately includes one forbidden relationship. It is not a scan of the currently running assembly. The complete external solution additionally includes an independent ArchitectureCheck console tool that loads the actual csproj XML, checks its declared ProjectReference edges, and returns a failing exit when the policy is broken.

Walk through the portable program

The portable fixture contains Application to Domain, Infrastructure to Application, and Domain to Infrastructure. The first two directions are allowed by the stated policy, while Domain is allowed no outward business-project references. Counting those rejected edges produces the single line declared-violations:1. The data is intentionally invalid so the policy algorithm has a visible counterexample. This result says nothing by itself about the physical external solution, whose baseline references are valid. Keeping fixture evidence separate from actual project evidence prevents a hardcoded list from masquerading as a real architecture dependency scan.

Build real physical dependencies

Externally, build ArchitectureLab.slnx and run the tool from the solution directory with that directory as its argument. It loads each of four business project files, resolves the explicit referenced project paths, and compares them with the allowed set. The valid baseline prints projects:4 and violations:0 and exits zero. The tool's own project is outside that business-layer policy. A separate deliberate Domain-to-Infrastructure reference is rejected by this checker; in the supplied graph it also creates a cycle that the actual SDK build rejects. Those are distinct, observable failure mechanisms.

Pitfalls and unproved guarantees

This small checker reads the direct unconditional reference items used by these labs. It is not a full evaluated MSBuild analyzer for imported targets, conditional target-framework matrices, NuGet dependencies, reflection, or runtime service calls. Modern .NET project references can also be transitive, so absence of a direct edge does not prove arbitrary type invisibility through all reachable dependencies. Define the exact policy being checked and inspect its full graph when needed. Passing a reference rule cannot establish domain correctness, performance, fault isolation, or the suitability of a chosen architecture for production.

Practice one: isolate behavior

First remove the forbidden edge from the portable fixture and predict zero declared violations. Then add an unknown source layer and predict another rejection. Hint: the rule deliberately rejects unrecognized source names rather than silently trusting them. Keep those data tests separate from physical project experiments. Test the policy with independently prepared valid and invalid edges, since a checker that always reports zero can look successful if its only fixture is a valid project graph.

Practice two: test a boundary

For another exercise, copy the external solution, add a Domain reference to Infrastructure, and run the already built checker against that copy. Predict a named violation and nonzero exit; then try building the mutated solution and observe its cycle failure. Hint: restore the original files before continuing normal examples. Next describe an acyclic but still forbidden relationship under a stricter policy, showing why compilation success alone is not an architecture test. A build and a policy check establish related but different constraints.

Review the evidence

Review architecture checks by naming their actual input artifact, evaluation scope, allowed edges, unknown-input policy, and failure signal. Retain a deliberate negative fixture so a broken checker cannot silently bless everything. The portable program verifies one policy algorithm on known data; the external tool verifies these real project files. Neither is a universal architectural proof. Keep behavior tests, dependency checks, and operational evidence separate, then combine the relevant results when assessing whether a boundary is both technically enforced and useful for the application's real changes.

External .NET 10 SDK; five real projects with explicit ProjectReference items. Not executed by the single-file .NET 9 playground.

Check actual project dependency direction — physical dependency lab

Set up the SDK project

  1. Save every file at its exact relative path under a fresh directory.
  2. Use a .NET 10 SDK. Run dotnet build ArchitectureLab.slnx.
  3. Run dotnet run --project Host/Host.csproj --no-build.
  4. From the solution directory run dotnet run --project ArchitectureCheck/ArchitectureCheck.csproj --no-build -- .

Download complete SDK project ↗

Project files

ArchitectureLab.slnxDownload file ↗
<Solution>
  <Project Path="Domain/Domain.csproj" />
  <Project Path="Application/Application.csproj" />
  <Project Path="Infrastructure/Infrastructure.csproj" />
  <Project Path="Host/Host.csproj" />
  <Project Path="ArchitectureCheck/ArchitectureCheck.csproj" />
</Solution>
Domain/Domain.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
Application/Application.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="../Domain/Domain.csproj" />
  </ItemGroup>
</Project>
Infrastructure/Infrastructure.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="../Domain/Domain.csproj" />
    <ProjectReference Include="../Application/Application.csproj" />
  </ItemGroup>
</Project>
Host/Host.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="../Domain/Domain.csproj" />
    <ProjectReference Include="../Application/Application.csproj" />
    <ProjectReference Include="../Infrastructure/Infrastructure.csproj" />
  </ItemGroup>
</Project>
ArchitectureCheck/ArchitectureCheck.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
Domain/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;

namespace BoundaryLab.Domain;

public static class DependencyRule
{
    public static bool Allowed(string from, string to) => from switch
    {
        "Domain" => false,
        "Application" => to == "Domain",
        "Infrastructure" => to == "Domain" || to == "Application",
        "Host" => to == "Domain" || to == "Application" || to == "Infrastructure",
        _ => false
    };
}
Application/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;

namespace BoundaryLab.Application;

public static class DependencyAudit
{
    public static int CountInvalid(IEnumerable<(string From, string To)> edges) => edges.Count(edge => !DependencyRule.Allowed(edge.From, edge.To));
}
Infrastructure/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;

namespace BoundaryLab.Infrastructure;

public static class ExampleEdges
{
    public static (string From, string To)[] Read() => new[] { ("Application", "Domain"), ("Infrastructure", "Application"), ("Domain", "Infrastructure") };
}
Host/Program.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;

var edges = ExampleEdges.Read();
Console.WriteLine($"declared-violations:{DependencyAudit.CountInvalid(edges)}");
ArchitectureCheck/Program.csDownload file ↗
using System.Xml.Linq;
string root = Path.GetFullPath(args.Length == 0 ? "." : args[0]);
var allowed = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal)
{
    ["Domain"] = new(),
    ["Application"] = new() { "Domain" },
    ["Infrastructure"] = new() { "Domain", "Application" },
    ["Host"] = new() { "Domain", "Application", "Infrastructure" }
};
int violations = 0;
foreach (var item in allowed)
{
    string path = Path.Combine(root, item.Key, item.Key + ".csproj");
    var document = XDocument.Load(path);
    foreach (var reference in document.Descendants().Where(element => element.Name.LocalName == "ProjectReference"))
    {
        string include = (string?)reference.Attribute("Include") ?? throw new InvalidOperationException("Missing Include");
        string targetPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path)!, include));
        string target = Path.GetFileNameWithoutExtension(targetPath);
        string expectedPath = Path.Combine(root, target, target + ".csproj");
        if (!item.Value.Contains(target) || targetPath != expectedPath)
        {
            violations++;
            Console.WriteLine($"violation:{item.Key}->{target}");
        }
    }
}
Console.WriteLine($"projects:{allowed.Count}");
Console.WriteLine($"violations:{violations}");
return violations == 0 ? 0 : 1;

Expected result

Host output: declared-violations:1 Physical ProjectReference policy check output: projects:4 violations:0 Both commands exit zero. The checker inspects the four business-project files; its own tooling project is outside that policy. It checks declared project-reference direction, not all runtime behavior or package dependencies.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Collections.Generic;
using System.Linq;

var edges = ExampleEdges.Read();
Console.WriteLine($"declared-violations:{DependencyAudit.CountInvalid(edges)}");

public static class DependencyRule
{
    public static bool Allowed(string from, string to) => from switch
    {
        "Domain" => false,
        "Application" => to == "Domain",
        "Infrastructure" => to == "Domain" || to == "Application",
        "Host" => to == "Domain" || to == "Application" || to == "Infrastructure",
        _ => false
    };
}

public static class DependencyAudit
{
    public static int CountInvalid(IEnumerable<(string From, string To)> edges) => edges.Count(edge => !DependencyRule.Allowed(edge.From, edge.To));
}

public static class ExampleEdges
{
    public static (string From, string To)[] Read() => new[] { ("Application", "Domain"), ("Infrastructure", "Application"), ("Domain", "Infrastructure") };
}
Open C# practice

C# practice output

declared-violations:1
Try this
The portable fixture contains one intentionally illegal edge. Run the external ArchitectureCheck tool to read actual .csproj references, then mutate a project reference in a separate copy.

Further reading