From 68b9657d620c9b6dbdac06379004ced7af39e5cb Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Mon, 14 Sep 2026 20:56:27 +0100 Subject: [PATCH] Add per-type tables and Find to DbContext (yak: add-per-type-tables-and-find-to-dbcontext-engc) Refactor DbContext to use per-type internal storage (Dictionary) instead of a single flat List, mirroring EF Core's DbSet pattern. Changes: - Replace _tracked List with _tables Dictionary> - Add Table() generic helper for compile-time known types (Find) - Add TableFor(Type) non-generic helper for Attach where type is only known at runtime - Add instance-level Find(Predicate) method on DbContext that searches only the per-type table for T (mirroring DbSet.Find behavior) - Keep Tracked { get } as a flattened view across all per-type tables (API compatible) - IsTracked now walks all per-type tables --- src/Before/DbContext.cs | 119 ++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 22 deletions(-) diff --git a/src/Before/DbContext.cs b/src/Before/DbContext.cs index e11a3a3..225ad42 100644 --- a/src/Before/DbContext.cs +++ b/src/Before/DbContext.cs @@ -1,9 +1,11 @@ +using System.Collections.ObjectModel; + namespace Before; /// -/// A tiny hand-rolled fake of EF's DbContext: it keeps a change-tracker -/// (a registration collection of the entities it knows -/// about) and a that fakes SaveChangesAsync. +/// A tiny hand-rolled fake of EF's DbContext: it keeps per-type +/// change-tracker tables (mirroring EF's pattern) and +/// a that fakes SaveChangesAsync. /// /// This is deliberately NOT real EF Core — no providers, no SQLite, no NuGet /// beyond xUnit. The point of the exercise is the *shape of the dependencies* @@ -11,25 +13,57 @@ namespace Before; /// public class DbContext { - // The change tracker: the set of entities this context is responsible for. - // (EF calls this its change tracker; a list stands in for the - // id -> entity registration dictionary.) - private readonly List _tracked = new(); + // Per-type tables: each entity type has its own typed collection, + // mimicking EF Core's model where the DbContext + // maintains a separate set per entity type. + private readonly Dictionary> _tables = new(); + + /// + /// Get or create the per-type table for . + /// This mirrors EF's / Set<T> accessor. + /// + private Collection Table() where T : DbBase + { + var type = typeof(T); + if (!_tables.TryGetValue(type, out var table)) + { + table = new Collection(); + _tables[type] = table; + } + return table; + } + + /// + /// Get or create the per-type table for the given runtime . + /// Called from where we only know the type at runtime. + /// + private Collection TableFor(Type type) + { + if (!_tables.TryGetValue(type, out var table)) + { + table = new Collection(); + _tables[type] = table; + } + return table; + } /// /// Attach an entity to this context (EF's Add). Sets the /// active-record back-reference so the entity knows its owner. + /// The entity is placed into the per-type table that matches its + /// runtime type — just as EF writes rows to the correct table. /// public void Attach(DbBase entity) { ArgumentNullException.ThrowIfNull(entity); entity.DbContext = this; - if (!_tracked.Contains(entity)) - _tracked.Add(entity); + var table = TableFor(entity.GetType()); + if (!table.Contains(entity)) + table.Add(entity); } /// - /// Fake SaveChangesAsync: walk the tracked entities and assign a + /// Fake SaveChangesAsync: walk all per-type tables and assign a /// fresh to any whose id is still . /// Returns the number of entities that were (re)saved — i.e. newly /// identified — mirroring EF's "rows written" return value. @@ -37,20 +71,61 @@ public class DbContext public int Save() { var saved = 0; - foreach (var entity in _tracked) - { - if (entity.Id == Guid.Empty) - { - entity.Id = Guid.NewGuid(); - saved++; - } - } + foreach (var table in _tables.Values) + foreach (var entity in table) + if (entity.Id == Guid.Empty) + { + entity.Id = Guid.NewGuid(); + saved++; + } return saved; } - /// True if is in this context's tracker. - public bool IsTracked(DbBase entity) => _tracked.Contains(entity); + /// + /// Find the first entity of type in this + /// context's per-type table that satisfies . + /// Only checks entities whose runtime type exactly matches + /// — just as EF's DbSet{T}.Find operates on a single table. + /// + /// This instance-level finder complements the static + /// . + /// + /// Entity type to find (must derive from ). + /// Filter applied to candidates of type . + /// The first matching entity, or null when no match. + public T? Find(Predicate predicate) where T : DbBase + { + var table = Table(); + foreach (var e in table) + if (e is T candidate && predicate(candidate)) + return candidate; + return default; + } - /// Read-only view of the entities this context currently tracks. - public IReadOnlyList Tracked => _tracked; + /// + /// True if is in this context's tracker + /// (walks all per-type tables). + /// + public bool IsTracked(DbBase entity) + { + foreach (var table in _tables.Values) + if (table.Contains(entity)) + return true; + return false; + } + + /// + /// Read-only view of all entities this context currently tracks + /// (flattened across all per-type tables). + /// + public IReadOnlyList Tracked + { + get + { + var all = new List(); + foreach (var table in _tables.Values) + all.AddRange(table); + return all.AsReadOnly(); + } + } }