Project · Architecture
Order acceptance and read-view lab
Check immutable domain input, local identity rejection, event intent, and read snapshots in six console scenarios. The memory adapter is not durable transactional storage.
All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.
Program.cs
var checks = OrderChecks.Run();
foreach (var check in checks) Console.WriteLine($"{check.Name}:{(check.Passed ? "PASS" : "FAIL")}");
Console.WriteLine($"passed:{checks.Count(check => check.Passed)}/{checks.Count}");
return checks.All(check => check.Passed) ? 0 : 1;
Domain/Order.cs
public sealed record Order
{
public string Id { get; }
public int Units { get; }
public Order(string id, int units)
{
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Id required", nameof(id));
if (units < 1) throw new ArgumentOutOfRangeException(nameof(units));
Id = id; Units = units;
}
}
public record Accepted(string Id, int Units);
Application/Orders.cs
public interface IOrderWrite { bool TrySave(Order order, Accepted message); }
public interface IOrderRead { IReadOnlyList<Order> Read(); }
public sealed class AcceptOrder(IOrderWrite store) { public bool Execute(Order order) => store.TrySave(order, new Accepted(order.Id, order.Units)); }
public record OrderView(int Count, int Units);
public sealed class ReadOrders(IOrderRead store)
{
public OrderView Execute() { var snapshot = store.Read(); return new(snapshot.Count, snapshot.Sum(order => order.Units)); }
}
Adapters/MemoryOrders.cs
public sealed class MemoryOrders : IOrderRead, IOrderWrite
{
private readonly Dictionary<string, Order> orders = new(StringComparer.Ordinal);
private readonly List<Accepted> outbox = new();
public IReadOnlyList<Accepted> Outbox => outbox.ToArray();
public IReadOnlyList<Order> Read() => orders.Values.ToArray();
public bool TrySave(Order order, Accepted message)
{
if (order.Id != message.Id || order.Units != message.Units) throw new ArgumentException("Mismatched event", nameof(message));
if (orders.ContainsKey(order.Id)) return false;
orders.Add(order.Id, order); outbox.Add(message); return true;
}
}
Checks/OrderChecks.cs
public static class OrderChecks
{
public static List<(string Name, bool Passed)> Run()
{
List<(string Name, bool Passed)> checks = [];
var first = new MemoryOrders();
bool accepted = new AcceptOrder(first).Execute(new Order("A1", 2));
checks.Add(("valid", accepted && new ReadOrders(first).Execute() == new OrderView(1, 2) && first.Outbox.Count == 1));
var duplicate = new MemoryOrders();
var writeDuplicate = new AcceptOrder(duplicate);
bool initial = writeDuplicate.Execute(new Order("A1", 2));
bool repeated = writeDuplicate.Execute(new Order("A1", 2));
checks.Add(("duplicate", initial && !repeated && duplicate.Read().Count == 1 && duplicate.Outbox.Count == 1));
var separate = new MemoryOrders();
var writeSeparate = new AcceptOrder(separate);
writeSeparate.Execute(new Order("A1", 2)); writeSeparate.Execute(new Order("B1", 3));
checks.Add(("separate-ids", new ReadOrders(separate).Execute() == new OrderView(2, 5) && separate.Outbox.Count == 2));
var snapshots = new MemoryOrders();
var writer = new AcceptOrder(snapshots);
writer.Execute(new Order("A1", 2));
var captured = snapshots.Read();
writer.Execute(new Order("B1", 1));
checks.Add(("snapshot", captured.Count == 1 && snapshots.Read().Count == 2));
var query = new ReadOrders(separate);
checks.Add(("query-no-effects", query.Execute() == new OrderView(2, 5) && query.Execute() == new OrderView(2, 5) && separate.Outbox.Count == 2));
bool rejected = false;
try { _ = new Order("bad", 0); } catch (ArgumentOutOfRangeException) { rejected = true; }
checks.Add(("invalid-domain", rejected));
return checks;
}
}
ConsoleLab.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>