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<Type, Collection>)
instead of a single flat List<DbBase>, mirroring EF Core's DbSet<T> pattern.
Changes:
- Replace _tracked List<DbBase> with _tables Dictionary<Type, Collection<DbBase>>
- Add Table<T>() generic helper for compile-time known types (Find<T>)
- Add TableFor(Type) non-generic helper for Attach where type is only known at runtime
- Add instance-level Find<T>(Predicate<T>) method on DbContext that searches only
the per-type table for T (mirroring DbSet<T>.Find behavior)
- Keep Tracked { get } as a flattened view across all per-type tables (API compatible)
- IsTracked now walks all per-type tables
This commit is contained in:
+97
-22
@@ -1,9 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// A tiny hand-rolled fake of EF's <c>DbContext</c>: it keeps a change-tracker
|
||||
/// (a registration collection of the <see cref="DbBase"/> entities it knows
|
||||
/// about) and a <see cref="Save"/> that fakes <c>SaveChangesAsync</c>.
|
||||
/// A tiny hand-rolled fake of EF's <c>DbContext</c>: it keeps per-type
|
||||
/// change-tracker tables (mirroring EF's <see cref="DbSet{T}"/> pattern) and
|
||||
/// a <see cref="Save"/> that fakes <c>SaveChangesAsync</c>.
|
||||
///
|
||||
/// 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;
|
||||
/// </summary>
|
||||
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<DbBase> _tracked = new();
|
||||
// Per-type tables: each entity type has its own typed collection,
|
||||
// mimicking EF Core's <see cref="DbSet{T}"/> model where the DbContext
|
||||
// maintains a separate set per entity type.
|
||||
private readonly Dictionary<Type, Collection<DbBase>> _tables = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get or create the per-type table for <typeparamref name="T"/>.
|
||||
/// This mirrors EF's <see cref="DbSet{T}"/> / <c>Set<T></c> accessor.
|
||||
/// </summary>
|
||||
private Collection<DbBase> Table<T>() where T : DbBase
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (!_tables.TryGetValue(type, out var table))
|
||||
{
|
||||
table = new Collection<DbBase>();
|
||||
_tables[type] = table;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or create the per-type table for the given runtime <paramref name="type"/>.
|
||||
/// Called from <see cref="Attach"/> where we only know the type at runtime.
|
||||
/// </summary>
|
||||
private Collection<DbBase> TableFor(Type type)
|
||||
{
|
||||
if (!_tables.TryGetValue(type, out var table))
|
||||
{
|
||||
table = new Collection<DbBase>();
|
||||
_tables[type] = table;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attach an entity to this context (EF's <c>Add</c>). 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake <c>SaveChangesAsync</c>: walk the tracked entities and assign a
|
||||
/// Fake <c>SaveChangesAsync</c>: walk all per-type tables and assign a
|
||||
/// fresh <see cref="Guid"/> to any whose id is still <see cref="Guid.Empty"/>.
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>True if <paramref name="entity"/> is in this context's tracker.</summary>
|
||||
public bool IsTracked(DbBase entity) => _tracked.Contains(entity);
|
||||
/// <summary>
|
||||
/// Find the first entity of type <typeparamref name="T"/> in this
|
||||
/// context's per-type table that satisfies <paramref name="predicate"/>.
|
||||
/// Only checks entities whose runtime type exactly matches <typeparamref name="T"/>
|
||||
/// — just as EF's <c>DbSet{T}.Find</c> operates on a single table.
|
||||
///
|
||||
/// This instance-level finder complements the static
|
||||
/// <see cref="DbBase.Find{T}(Predicate{T},DbContext?)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Entity type to find (must derive from <see cref="DbBase"/>).</typeparam>
|
||||
/// <param name="predicate">Filter applied to candidates of type <typeparamref name="T"/>.</param>
|
||||
/// <returns>The first matching entity, or <c>null</c> when no match.</returns>
|
||||
public T? Find<T>(Predicate<T> predicate) where T : DbBase
|
||||
{
|
||||
var table = Table<T>();
|
||||
foreach (var e in table)
|
||||
if (e is T candidate && predicate(candidate))
|
||||
return candidate;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>Read-only view of the entities this context currently tracks.</summary>
|
||||
public IReadOnlyList<DbBase> Tracked => _tracked;
|
||||
/// <summary>
|
||||
/// True if <paramref name="entity"/> is in this context's tracker
|
||||
/// (walks all per-type tables).
|
||||
/// </summary>
|
||||
public bool IsTracked(DbBase entity)
|
||||
{
|
||||
foreach (var table in _tables.Values)
|
||||
if (table.Contains(entity))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only view of all entities this context currently tracks
|
||||
/// (flattened across all per-type tables).
|
||||
/// </summary>
|
||||
public IReadOnlyList<DbBase> Tracked
|
||||
{
|
||||
get
|
||||
{
|
||||
var all = new List<DbBase>();
|
||||
foreach (var table in _tables.Values)
|
||||
all.AddRange(table);
|
||||
return all.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user