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
CQRS separates write responsibilities from read representations and their use cases; it does not inherently require a mediator library, message broker, separate deployment, or separate database. This small shared-state example has an Increment command operating on a domain Counter and a ReadCount query returning a distinct immutable CountView. It demonstrates a minimal separation of command and query models without claiming a distributed read-model pipeline. The portable program makes those responsibilities visible, while the external solution shows their physical placement in Domain and Application with formatting left outside the core.
Walk through the portable program
The counter begins at zero. Increment accepts positive changes and applies two, then three, producing five. ReadCount creates a new CountView containing the current value without changing the counter. Calling the query twice therefore prints first:5 and second:5 through the outer formatting helper. The two equal observations do not prove every possible query is side-effect-free, but the actual implementation is simple enough to inspect. The example deliberately uses one underlying state object so database replication lag and eventual consistency cannot be silently claimed or invented from its output.
Build real physical dependencies
In the external solution, Counter is the write model in Domain. Increment and ReadCount express separate application operations, and CountView is the read representation. Infrastructure formats that representation for Host without importing display text into Counter. The physical checker verifies the same inward reference rules used by the course's other labs. If a future read model is maintained asynchronously, its freshness, update failures, replay, and duplicate delivery become additional contracts. Separating two responsibilities can be useful before any of those distributed mechanisms are justified by the application's real workload.
Pitfalls and unproved guarantees
Avoid naming two arbitrary methods command and query while allowing the query to mutate domain state or exposing a writeable aggregate as its result. Also avoid introducing a broker solely to satisfy a diagram when one process and a shared store meet the requirement. Conversely, a small shared-state example does not prove that a remote projection is immediately consistent. A mediator can route handlers, but routing is independent of whether command and read responsibilities are designed well. Measure actual access patterns and complexity before turning a lightweight separation into a larger operational architecture.
Practice one: isolate behavior
First query before either command and predict zero, then after the first command and predict two. Hint: use observations at deliberate points in the sequence rather than relying only on the final total. Add a rejected zero increment and confirm that the next query still reads the prior value. Keep the command's failure and the query's representation separate in your expectations. These cases establish useful behavior even without a framework runner, while leaving no reason to add message delivery to a purely synchronous scenario.
Practice two: test a boundary
For another exercise, add a read representation that classifies the current count into a documented band without returning the mutable Counter. Test its threshold independently from the increment arithmetic. Hint: the query should derive data for readers, not issue another command to force the model into a convenient display state. Then outline an asynchronously maintained projection and state exactly when it may lag behind accepted writes. Identify what an integration test must observe in that design instead of assuming the immediate behavior of this in-memory query still applies.
Review the evidence
Review a CQRS decision by identifying its write model, read representation, consistency needs, and actual reason to separate them. The sample stays within one process and one state model while giving the reader a distinct view. The external projects add enforceable dependency direction, not a mandatory messaging stack. If independent scaling or specialized query storage becomes worthwhile, introduce those changes with explicit operational contracts. Keep the simpler design when it satisfies the use case; a named pattern should help explain responsibilities rather than force infrastructure that the bounded example neither needs nor tests.
CQRS without a mandatory mediator — physical dependency lab
Set up the SDK project
- Save every file at its exact relative path under a fresh directory.
- Use a .NET 10 SDK. Run dotnet build ArchitectureLab.slnx.
- Run dotnet run --project Host/Host.csproj --no-build.
- From the solution directory run dotnet run --project ArchitectureCheck/ArchitectureCheck.csproj --no-build -- .
Download complete SDK project ↗
Project files
ArchitectureLab.slnx
Download 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.csproj
Download 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.csproj
Download 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.csproj
Download 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.csproj
Download 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.csproj
Download 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.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
namespace BoundaryLab.Domain;
public sealed class Counter
{
public int Value { get; private set; }
public void Add(int units) { if (units < 1) throw new ArgumentOutOfRangeException(nameof(units)); Value = checked(Value + units); }
}
Application/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
namespace BoundaryLab.Application;
public record CountView(int Value);
public sealed class Increment(Counter counter) { public void Execute(int units) => counter.Add(units); }
public sealed class ReadCount(Counter counter) { public CountView Execute() => new(counter.Value); }
Infrastructure/Model.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
namespace BoundaryLab.Infrastructure;
public static class CountText { public static string Render(string label, CountView view) => label + ":" + view.Value; }
Host/Program.cs
Download file ↗using System;
using System.Collections.Generic;
using System.Linq;
using BoundaryLab.Domain;
using BoundaryLab.Application;
using BoundaryLab.Infrastructure;
var counter = new Counter();
var command = new Increment(counter);
var query = new ReadCount(counter);
command.Execute(2); command.Execute(3);
Console.WriteLine(CountText.Render("first", query.Execute()));
Console.WriteLine(CountText.Render("second", query.Execute()));
ArchitectureCheck/Program.cs
Download 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: first:5 second:5 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 counter = new Counter();
var command = new Increment(counter);
var query = new ReadCount(counter);
command.Execute(2); command.Execute(3);
Console.WriteLine(CountText.Render("first", query.Execute()));
Console.WriteLine(CountText.Render("second", query.Execute()));
public sealed class Counter
{
public int Value { get; private set; }
public void Add(int units) { if (units < 1) throw new ArgumentOutOfRangeException(nameof(units)); Value = checked(Value + units); }
}
public record CountView(int Value);
public sealed class Increment(Counter counter) { public void Execute(int units) => counter.Add(units); }
public sealed class ReadCount(Counter counter) { public CountView Execute() => new(counter.Value); }
public static class CountText { public static string Render(string label, CountView view) => label + ":" + view.Value; }
C# practice output
first:5 second:5
Add a query that does not mutate the aggregate. Separate command/query responsibilities do not require separate databases, messaging, or a mediator package.