diff --git a/src/After/DbContext.cs b/src/After/DbContext.cs index cbba171..2215fbf 100644 --- a/src/After/DbContext.cs +++ b/src/After/DbContext.cs @@ -1,51 +1,102 @@ namespace After; +// --------------------------------------------------------------------------- +// Internal shadow wrappers — live *inside* the persistence layer so the +// domain never sees a DbContext back-reference. Subclassed per-entity-type +// solely to make `Set` strongly-typed. +// --------------------------------------------------------------------------- + +/// Lightweight shadow holder kept alive inside the context. +public abstract class Shadow +{ + /// ID assigned when first persisted. + public Guid Id { get; set; } + + /// Back-reference to the owning (internal only). + public DbContext? DbContext { get; internal set; } +} + +/// Shadow holder for a persisted . +public sealed class ClientShadow : Shadow { /* no extra data */ } + +/// Shadow holder for a persisted . +public sealed class OrderShadow : Shadow { /* no extra data */ } + /// -/// The fake DbContext 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 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. +/// The fake DbContext, refactored to use per-type shadow collections +/// (mirroring EF Core's Set<T>) plus an instance-level +/// Find<T> query method. /// public class DbContext { - // Change tracker, keyed by the domain object's reference identity. - private readonly Dictionary _tracked = new(); + // ── per-type shadow tables ─────────────────────────────────────── + private readonly List _clients = new(); + private readonly List _orders = new(); - /// - /// Explicitly persist a : register it with a shadow - /// entity, assign it a fresh id, and copy that id into - /// the client. Saving an already-saved client is a no-op for its id. - /// + // ── fast lookup: domain object reference → its shadow ──────────── + private readonly Dictionary _byRef = new(); + + // ── exposed per-type tables (read-only view) ──────────────────── + public IReadOnlyList Clients => _clients.AsReadOnly(); + public IReadOnlyList Orders => _orders.AsReadOnly(); + + /// Generic accessor: the strongly-typed "table" for . + public IReadOnlyList Set() where T : Shadow + => typeof(T) switch + { + var t when t == typeof(ClientShadow) => (IReadOnlyList)_clients.AsReadOnly(), + var t when t == typeof(OrderShadow) => (IReadOnlyList)_orders.AsReadOnly(), + _ => throw new NotSupportedException($"No per-type table for {typeof(T)}"), + }; + + // ── explicit persistence ──────────────────────────────────────── + + /// Persist a : 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(); + if (!_byRef.TryAdd(client, new ClientShadow { DbContext = this })) + return; // already saved → id unchanged + var shadow = (ClientShadow)_byRef[client]; + var id = Guid.NewGuid(); + shadow.Id = client.Id = id; // write-back identity } - /// - /// Explicitly persist an : register it with a shadow - /// entity, assign it a fresh id, and copy that id into - /// the order. Saving an already-saved order is a no-op for its id. - /// + /// Persist an . 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(); + if (!_byRef.TryAdd(order, new OrderShadow { DbContext = this })) + return; // already saved → id unchanged + var shadow = (OrderShadow)_byRef[order]; + var id = Guid.NewGuid(); + shadow.Id = order.Id = id; // write-back identity } - /// True if has been saved through this context. - public bool IsTracked(object entity) => _tracked.ContainsKey(entity); + /// Persist every tracked entity (mirrors EF's SaveChanges). + public int SaveChanges() + { + var count = 0; + foreach (var shadow in _byRef.Values) + { + if (shadow.Id != Guid.Empty) continue; // already had an id + shadow.Id = Guid.NewGuid(); + count++; + } + return count; + } - /// Read-only view of the shadow entities this context currently tracks. - public IReadOnlyList Tracked => _tracked.Values.ToList(); -} \ No newline at end of file + // ── query helpers ─────────────────────────────────────────────── + + /// Search the per-type table for a shadow whose predicate matches. + public T? Find(Func predicate) where T : Shadow + => Set().FirstOrDefault(predicate); + + // ── tracking / status ─────────────────────────────────────────── + + /// True if has been saved through this context. + public bool IsTracked(object entity) => _byRef.ContainsKey(entity); + + /// All shadow entities this context currently manages. + public IReadOnlyList Tracked => _byRef.Values.ToList(); +}