Find falls back to DbBase.Context singleton when no context passed

DbBase.Find<T> now uses db ?? Context, and Client.FindByName's context
parameter becomes optional — call sites read like Client.FindByName("Jane
Doe") without context threading. A null result now overwhelmingly means
"no match"; the residual "no context configured at all" ambiguity is
documented in remarks as part of the ActiveRecord-leak cost this exercise
illustrates.
This commit is contained in:
2026-09-14 21:17:03 +01:00
parent 68b9657d62
commit 325925a592
4 changed files with 75 additions and 9 deletions
+7 -2
View File
@@ -6,7 +6,8 @@ using Before;
// The console app sets DbBase.Context (the shared singleton) once at startup,
// then creates client/order entities, saves them through the context, and
// queries back using the ActiveRecord entry points:
// • Client.FindByName(name, context)
// • Client.FindByName(name) — no context argument: rides the
// DbBase.Context singleton
// • listing via DbBase.Context!.Tracked
// ---------------------------------------------------------------------------
@@ -42,7 +43,11 @@ foreach (var client in DbBase.Context!.Tracked.OfType<Client>())
// ----- find by name -------------------------------------------------------
Console.WriteLine("\n=== Find by Name ===");
var found = Client.FindByName("Jane Doe", DbBase.Context);
// No context argument: FindByName falls back to the DbBase.Context singleton.
// This is the ActiveRecord ergonomic — call sites don't carry the context,
// but the null that comes back on a miss can't tell "no such client" from
// "nobody configured the context".
var found = Client.FindByName("Jane Doe");
if (found is not null)
{
Console.WriteLine($"Found: {found.Name} [{found.Id}]");
+10 -6
View File
@@ -25,16 +25,20 @@ public class Client : DbBase
}
/// <summary>
/// Convenience lookup: walks the <paramref name="db"/>'s tracked entities,
/// finds the first <see cref="Client"/> whose <see cref="Name"/>
/// equals <paramref name="name"/>. Returns <c>null</c> when no match.
/// Convenience lookup: finds the first <see cref="Client"/> whose
/// <see cref="Name"/> equals <paramref name="name"/>. Returns
/// <c>null</c> when no match.
///
/// This mirrors how an ActiveRecord-style ORM might surface a static
/// finder on the domain class itself — it rides the <c>DbContext</c> leak
/// from the entity's back-reference. Delegates to
/// finder on the domain class itself — it rides the <c>DbContext</c>
/// leak from the entity's back-reference. Delegates to
/// <see cref="DbBase.Find{T}"/> for the common traversal logic.
///
/// The context is optional: when omitted, the <see cref="DbBase.Context"/>
/// singleton is used, so call sites read like
/// <c>Client.FindByName("Jane Doe")</c> — no context threading required.
/// </summary>
public static Client? FindByName(string name, DbContext? db)
public static Client? FindByName(string name, DbContext? db = null)
=> DbBase.Find<Client>(c => c.Name == name, db);
}
+11
View File
@@ -57,11 +57,22 @@ public class DbBase
/// <param name="predicate">Filter applied to candidates of type <typeparamref name="T"/>.
/// </param>
/// <param name="db">The context whose tracked entities to search.
/// When <c>null</c>, falls back to the <see cref="Context"/> singleton —
/// so a caller only passes a context explicitly when it must differ from
/// the ambient one.
/// </param>
/// <returns>The first matching entity, or <c>null</c> when no match.
/// </returns>
/// <remarks>
/// Note the (deliberate, ActiveRecord-style) ambiguity this still leaves:
/// a <c>null</c> result means "no match — or no context configured at
/// all". The caller cannot distinguish the two; that is part of the cost
/// of the static-singleton leak this exercise illustrates.
/// </remarks>
public static T? Find<T>(Predicate<T> predicate, DbContext? db) where T : DbBase
{
db ??= Context;
if (db is null)
return default;