Yak: csharp after situation (Yak 02) — plain domain + DTOs + mappers

After/: plain Client/Order (no DbBase), ClientDto/OrderDto records
carrying the relations both ways, hand-written Mappers with a
reference-identity cache for the back-reference cycle, DbContext with
explicit Save(domain) via shadow entities (flipped dependency), and a
WebApp that consumes only DTOs. AfterTests cover the 7 required
behaviours; full solution builds with 0 warnings, 26/26 tests pass.
This commit is contained in:
2026-09-11 15:02:35 +01:00
parent 463245288a
commit c22a1e2ffa
10 changed files with 644 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
namespace After;
/// <summary>
/// The fake <c>DbContext</c> of the after situation. The persistence layer
/// still exists — but the direction of the dependency is flipped: domain
/// objects are plain POCOs that know nothing about a context, and persisting
/// one requires an *explicit* call into this context.
///
/// Saving a domain object registers it with a private shadow entity
/// (a <see cref="DbBase"/> owned by this context only), fakes identity
/// generation, and copies the new id back into the domain object. The domain
/// object therefore ends up with a saved id, but with no back-reference to
/// the context — the leak from the before situation is gone.
/// </summary>
public class DbContext
{
// Change tracker, keyed by the domain object's reference identity.
private readonly Dictionary<object, DbBase> _tracked = new();
/// <summary>
/// Explicitly persist a <see cref="Client"/>: register it with a shadow
/// entity, assign it a fresh <see cref="Guid"/> id, and copy that id into
/// the client. Saving an already-saved client is a no-op for its id.
/// </summary>
public void Save(Client client)
{
ArgumentNullException.ThrowIfNull(client);
if (!_tracked.TryAdd(client, new DbBase { DbContext = this }))
return; // already saved
client.Id = _tracked[client].Id = Guid.NewGuid();
}
/// <summary>
/// Explicitly persist an <see cref="Order"/>: register it with a shadow
/// entity, assign it a fresh <see cref="Guid"/> id, and copy that id into
/// the order. Saving an already-saved order is a no-op for its id.
/// </summary>
public void Save(Order order)
{
ArgumentNullException.ThrowIfNull(order);
if (!_tracked.TryAdd(order, new DbBase { DbContext = this }))
return; // already saved
order.Id = _tracked[order].Id = Guid.NewGuid();
}
/// <summary>True if <paramref name="entity"/> has been saved through this context.</summary>
public bool IsTracked(object entity) => _tracked.ContainsKey(entity);
/// <summary>Read-only view of the shadow entities this context currently tracks.</summary>
public IReadOnlyList<DbBase> Tracked => _tracked.Values.ToList();
}