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:
@@ -6,7 +6,8 @@ using Before;
|
|||||||
// The console app sets DbBase.Context (the shared singleton) once at startup,
|
// The console app sets DbBase.Context (the shared singleton) once at startup,
|
||||||
// then creates client/order entities, saves them through the context, and
|
// then creates client/order entities, saves them through the context, and
|
||||||
// queries back using the ActiveRecord entry points:
|
// 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
|
// • listing via DbBase.Context!.Tracked
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -42,7 +43,11 @@ foreach (var client in DbBase.Context!.Tracked.OfType<Client>())
|
|||||||
|
|
||||||
// ----- find by name -------------------------------------------------------
|
// ----- find by name -------------------------------------------------------
|
||||||
Console.WriteLine("\n=== 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)
|
if (found is not null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Found: {found.Name} [{found.Id}]");
|
Console.WriteLine($"Found: {found.Name} [{found.Id}]");
|
||||||
|
|||||||
+10
-6
@@ -25,16 +25,20 @@ public class Client : DbBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convenience lookup: walks the <paramref name="db"/>'s tracked entities,
|
/// Convenience lookup: finds the first <see cref="Client"/> whose
|
||||||
/// finds the first <see cref="Client"/> whose <see cref="Name"/>
|
/// <see cref="Name"/> equals <paramref name="name"/>. Returns
|
||||||
/// equals <paramref name="name"/>. Returns <c>null</c> when no match.
|
/// <c>null</c> when no match.
|
||||||
///
|
///
|
||||||
/// This mirrors how an ActiveRecord-style ORM might surface a static
|
/// This mirrors how an ActiveRecord-style ORM might surface a static
|
||||||
/// finder on the domain class itself — it rides the <c>DbContext</c> leak
|
/// finder on the domain class itself — it rides the <c>DbContext</c>
|
||||||
/// from the entity's back-reference. Delegates to
|
/// leak from the entity's back-reference. Delegates to
|
||||||
/// <see cref="DbBase.Find{T}"/> for the common traversal logic.
|
/// <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>
|
/// </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);
|
=> DbBase.Find<Client>(c => c.Name == name, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,22 @@ public class DbBase
|
|||||||
/// <param name="predicate">Filter applied to candidates of type <typeparamref name="T"/>.
|
/// <param name="predicate">Filter applied to candidates of type <typeparamref name="T"/>.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="db">The context whose tracked entities to search.
|
/// <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>
|
/// </param>
|
||||||
/// <returns>The first matching entity, or <c>null</c> when no match.
|
/// <returns>The first matching entity, or <c>null</c> when no match.
|
||||||
/// </returns>
|
/// </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
|
public static T? Find<T>(Predicate<T> predicate, DbContext? db) where T : DbBase
|
||||||
{
|
{
|
||||||
|
db ??= Context;
|
||||||
|
|
||||||
if (db is null)
|
if (db is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ public class BeforeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (13) DbBase.Find handles a null context gracefully.
|
// (13) DbBase.Find handles a null context gracefully — with no explicit
|
||||||
|
// context AND no singleton configured, it returns default (null).
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DbBase_Find_with_null_context_returns_default()
|
public void DbBase_Find_with_null_context_returns_default()
|
||||||
@@ -238,6 +239,29 @@ public class BeforeTests
|
|||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (13b) When no explicit context is passed, Find falls back to the
|
||||||
|
// DbBase.Context singleton instead of returning null.
|
||||||
|
[Fact]
|
||||||
|
public void DbBase_Find_falls_back_to_Context_singleton_when_db_is_null()
|
||||||
|
{
|
||||||
|
var db = new DbContext();
|
||||||
|
var acme = new Client { Name = "SingletonCo" };
|
||||||
|
db.Attach(acme);
|
||||||
|
|
||||||
|
DbBase.Context = db;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = DbBase.Find<Client>(c => c.Name == "SingletonCo", null);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Same(acme, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DbBase.Context = null; // cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (14) Client.FindByName delegates to DbBase.Find and works correctly.
|
// (14) Client.FindByName delegates to DbBase.Find and works correctly.
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -268,6 +292,28 @@ public class BeforeTests
|
|||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (15b) FindByName with no context argument uses the Context singleton.
|
||||||
|
[Fact]
|
||||||
|
public void Client_FindByName_uses_Context_singleton_when_db_omitted()
|
||||||
|
{
|
||||||
|
var db = new DbContext();
|
||||||
|
var acme = new Client { Name = "Acme Corp" };
|
||||||
|
db.Attach(acme);
|
||||||
|
|
||||||
|
DbBase.Context = db;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = Client.FindByName("Acme Corp");
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Same(acme, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DbBase.Context = null; // cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// (16) The class-level DbBase.Context singleton can be used as the
|
// (16) The class-level DbBase.Context singleton can be used as the
|
||||||
// implicit context source for Find operations — the classic
|
// implicit context source for Find operations — the classic
|
||||||
|
|||||||
Reference in New Issue
Block a user