Add per-type tables and Find to DbContext (yak: Add per-type tables and Find to DbContext)
Refactor After.DbContext from a single flat _tracked dictionary into: - Per-type shadow collections (_clients, _orders) mirroring EF Core's Set<T> pattern, each holding type-specific Shadow subtypes - Public Clients/Orders properties as read-only typed table accessors - Generic Set<T>() accessor for any Shadow subtype - Find<T>(Func<T, bool>) query method on the context instance Internal details: - Shadow base class with Id + DbContext back-reference (internal set) - ClientShadow / OrderShadow concrete subtypes per entity - _byRef Dictionary<object, Shadow> for fast domain→shadow lookup - SaveChanges() persists all tracked entities at once
This commit is contained in:
+82
-31
@@ -1,51 +1,102 @@
|
|||||||
namespace After;
|
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<T>` strongly-typed.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>Lightweight shadow holder kept alive inside the context.</summary>
|
||||||
|
public abstract class Shadow
|
||||||
|
{
|
||||||
|
/// <summary>ID assigned when first persisted.</summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Back-reference to the owning <see cref="DbContext"/> (internal only).</summary>
|
||||||
|
public DbContext? DbContext { get; internal set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shadow holder for a persisted <see cref="Client"/>.</summary>
|
||||||
|
public sealed class ClientShadow : Shadow { /* no extra data */ }
|
||||||
|
|
||||||
|
/// <summary>Shadow holder for a persisted <see cref="Order"/>.</summary>
|
||||||
|
public sealed class OrderShadow : Shadow { /* no extra data */ }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The fake <c>DbContext</c> of the after situation. The persistence layer
|
/// The fake <c>DbContext</c>, refactored to use per-type shadow collections
|
||||||
/// still exists — but the direction of the dependency is flipped: domain
|
/// (mirroring EF Core's <c>Set<T></c>) plus an instance-level
|
||||||
/// objects are plain POCOs that know nothing about a context, and persisting
|
/// <c>Find<T></c> query method.
|
||||||
/// 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>
|
/// </summary>
|
||||||
public class DbContext
|
public class DbContext
|
||||||
{
|
{
|
||||||
// Change tracker, keyed by the domain object's reference identity.
|
// ── per-type shadow tables ───────────────────────────────────────
|
||||||
private readonly Dictionary<object, DbBase> _tracked = new();
|
private readonly List<ClientShadow> _clients = new();
|
||||||
|
private readonly List<OrderShadow> _orders = new();
|
||||||
|
|
||||||
/// <summary>
|
// ── fast lookup: domain object reference → its shadow ────────────
|
||||||
/// Explicitly persist a <see cref="Client"/>: register it with a shadow
|
private readonly Dictionary<object, Shadow> _byRef = new();
|
||||||
/// 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.
|
// ── exposed per-type tables (read-only view) ────────────────────
|
||||||
/// </summary>
|
public IReadOnlyList<ClientShadow> Clients => _clients.AsReadOnly();
|
||||||
|
public IReadOnlyList<OrderShadow> Orders => _orders.AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>Generic accessor: the strongly-typed "table" for <typeparamref name="T"/>.</summary>
|
||||||
|
public IReadOnlyList<T> Set<T>() where T : Shadow
|
||||||
|
=> typeof(T) switch
|
||||||
|
{
|
||||||
|
var t when t == typeof(ClientShadow) => (IReadOnlyList<T>)_clients.AsReadOnly(),
|
||||||
|
var t when t == typeof(OrderShadow) => (IReadOnlyList<T>)_orders.AsReadOnly(),
|
||||||
|
_ => throw new NotSupportedException($"No per-type table for {typeof(T)}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── explicit persistence ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Persist a <see cref="Client">:</summary>
|
||||||
public void Save(Client client)
|
public void Save(Client client)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(client);
|
ArgumentNullException.ThrowIfNull(client);
|
||||||
if (!_tracked.TryAdd(client, new DbBase { DbContext = this }))
|
if (!_byRef.TryAdd(client, new ClientShadow { DbContext = this }))
|
||||||
return; // already saved
|
return; // already saved → id unchanged
|
||||||
client.Id = _tracked[client].Id = Guid.NewGuid();
|
var shadow = (ClientShadow)_byRef[client];
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
shadow.Id = client.Id = id; // write-back identity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Persist an <see cref="Order">.</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)
|
public void Save(Order order)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(order);
|
ArgumentNullException.ThrowIfNull(order);
|
||||||
if (!_tracked.TryAdd(order, new DbBase { DbContext = this }))
|
if (!_byRef.TryAdd(order, new OrderShadow { DbContext = this }))
|
||||||
return; // already saved
|
return; // already saved → id unchanged
|
||||||
order.Id = _tracked[order].Id = Guid.NewGuid();
|
var shadow = (OrderShadow)_byRef[order];
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
shadow.Id = order.Id = id; // write-back identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Persist every tracked entity (mirrors EF's <c>SaveChanges</c>).</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── query helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Search the per-type table for a shadow whose predicate matches.</summary>
|
||||||
|
public T? Find<T>(Func<T, bool> predicate) where T : Shadow
|
||||||
|
=> Set<T>().FirstOrDefault(predicate);
|
||||||
|
|
||||||
|
// ── tracking / status ───────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>True if <paramref name="entity"/> has been saved through this context.</summary>
|
/// <summary>True if <paramref name="entity"/> has been saved through this context.</summary>
|
||||||
public bool IsTracked(object entity) => _tracked.ContainsKey(entity);
|
public bool IsTracked(object entity) => _byRef.ContainsKey(entity);
|
||||||
|
|
||||||
/// <summary>Read-only view of the shadow entities this context currently tracks.</summary>
|
/// <summary>All shadow entities this context currently manages.</summary>
|
||||||
public IReadOnlyList<DbBase> Tracked => _tracked.Values.ToList();
|
public IReadOnlyList<Shadow> Tracked => _byRef.Values.ToList();
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user