Architecture & Boundaries · Lesson 9

Validation results across boundaries

Return a typed decision and keep presentation outside domain policy.

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

Validation crosses several boundaries: an adapter may reject malformed representation, the application may reject an unsupported request, and the domain must preserve its own rules. This example begins with typed integers and evaluates one bounded rule: quantities one through five are accepted. EvaluateQuantity returns a named result instead of using an exception for this ordinary decision. ResultText formats its code outside the rule. The portable program demonstrates those real values, while the external solution compiles Domain, Application, and presentation separately without inventing a framework Result type or pretending a DTO supplies validation automatically.

Walk through the portable program

The first request uses zero, which is below the accepted range. UnitRule returns false and the application creates a QuantityResult with Accepted false and Code quantity. The second request uses three, so it creates Accepted true and Code accepted. The formatter labels those decisions and prints bad:quantity followed by good:accepted. It does not re-run the rule or change the result. A reader can independently predict both codes from the stated range. The boolean remains useful structured data even though this particular display helper prints only the stable code.

Build real physical dependencies

The external Domain project owns the quantity rule, Application owns its decision representation and operation, and Infrastructure supplies text formatting. These are actual project references checked by the separate tool. That arrangement keeps a wording change from becoming a domain-policy edit. A real HTTP or JSON adapter would additionally map representation failures and valid typed requests, with its own tests. The current source deliberately starts after parsing. It therefore establishes neither transport status codes nor a complete error protocol, and the external console host does not pretend to be a web framework integration.

Pitfalls and unproved guarantees

A public record containing a boolean and a string can be constructed inconsistently by arbitrary callers. The baseline operation creates only its two valid combinations, but the type itself does not enforce that relationship. If invalid combinations matter, use a representation with controlled creation or a discriminated design appropriate to the codebase. Also distinguish an expected invalid quantity from an unexpected storage failure. Returning quantity for every caught exception would hide defects. Typed results are a communication choice; they do not remove the need for clear failure categories and real exception handling at suitable boundaries.

Practice one: isolate behavior

First test one and five and predict accepted for each, then zero and six and predict quantity for each. Hint: use adjacent boundary cases to distinguish inclusive comparisons from a plausible strict-comparison defect. Inspect both Accepted and Code rather than asserting only the formatted string. Keep expected combinations literal and independent of EvaluateQuantity. Then change only the display label and verify that the structured decision remains unchanged, demonstrating which part of the behavior belongs to presentation rather than to acceptance policy.

Practice two: test a boundary

For another exercise, replace arbitrary public result construction with named accepted and rejected creation paths that cannot produce contradictory fields. Hint: define the valid states before choosing syntax; renaming the same unrestricted constructor would not enforce them. Add a parsing adapter with separate malformed-text and typed-invalid-quantity codes, keeping the domain range unchanged. Test one input for each stage. Do not catch all exceptions and translate them into the ordinary rejection code merely to make every failure resemble a known user mistake.

Review the evidence

Review a validation flow by identifying its input representation, typed rule, result states, stable error meaning, and presentation mapping. Choose exceptions or explicit results according to the actual contract rather than a universal slogan. The sample verifies a small range decision and a separate formatting boundary; its external graph verifies responsibility placement. Neither guarantees that every caller constructs valid results or that infrastructure cannot fail. Make those additional concerns explicit so a convenient result type communicates useful evidence instead of becoming a wrapper that conceals ambiguity.

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

Validation results across boundaries — 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 UnitRule { public static bool Valid(int units) => units >= 1 && units <= 5; }
Application/Model.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;

namespace BoundaryLab.Application;

public record QuantityResult(bool Accepted, string Code);
public sealed class EvaluateQuantity
{
    public QuantityResult Execute(int units) => UnitRule.Valid(units) ? new(true, "accepted") : new(false, "quantity");
}
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 ResultText { public static string Render(string label, QuantityResult result) => label + ":" + result.Code; }
Host/Program.csDownload file ↗
using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;

var service = new EvaluateQuantity();
Console.WriteLine(ResultText.Render("bad", service.Execute(0)));
Console.WriteLine(ResultText.Render("good", service.Execute(3)));
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: bad:quantity good:accepted 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 service = new EvaluateQuantity();
Console.WriteLine(ResultText.Render("bad", service.Execute(0)));
Console.WriteLine(ResultText.Render("good", service.Execute(3)));

public static class UnitRule { public static bool Valid(int units) => units >= 1 && units <= 5; }

public record QuantityResult(bool Accepted, string Code);
public sealed class EvaluateQuantity
{
    public QuantityResult Execute(int units) => UnitRule.Valid(units) ? new(true, "accepted") : new(false, "quantity");
}

public static class ResultText { public static string Render(string label, QuantityResult result) => label + ":" + result.Code; }
Open C# practice

C# practice output

bad:quantity
good:accepted
Try this
Test one and five as accepted boundaries, then zero and six as rejections. A typed result does not replace exception handling for unexpected infrastructure failure.

Further reading