From 325925a592062f32744d0d1b377d2af6f0237d0c Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Mon, 14 Sep 2026 21:17:03 +0100 Subject: [PATCH] Find falls back to DbBase.Context singleton when no context passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DbBase.Find 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. --- src/Before.Console/Program.cs | 9 +++-- src/Before/Client.cs | 16 +++++---- src/Before/DbBase.cs | 11 ++++++ tests/BeforeAfter.Tests/BeforeTests.cs | 48 +++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/Before.Console/Program.cs b/src/Before.Console/Program.cs index ecec572..58b55d9 100644 --- a/src/Before.Console/Program.cs +++ b/src/Before.Console/Program.cs @@ -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()) // ----- 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}]"); diff --git a/src/Before/Client.cs b/src/Before/Client.cs index c485dbe..11341b3 100644 --- a/src/Before/Client.cs +++ b/src/Before/Client.cs @@ -25,16 +25,20 @@ public class Client : DbBase } /// - /// Convenience lookup: walks the 's tracked entities, - /// finds the first whose - /// equals . Returns null when no match. + /// Convenience lookup: finds the first whose + /// equals . Returns + /// null when no match. /// /// This mirrors how an ActiveRecord-style ORM might surface a static - /// finder on the domain class itself — it rides the DbContext leak - /// from the entity's back-reference. Delegates to + /// finder on the domain class itself — it rides the DbContext + /// leak from the entity's back-reference. Delegates to /// for the common traversal logic. + /// + /// The context is optional: when omitted, the + /// singleton is used, so call sites read like + /// Client.FindByName("Jane Doe") — no context threading required. /// - public static Client? FindByName(string name, DbContext? db) + public static Client? FindByName(string name, DbContext? db = null) => DbBase.Find(c => c.Name == name, db); } diff --git a/src/Before/DbBase.cs b/src/Before/DbBase.cs index af99ebd..6c80e16 100644 --- a/src/Before/DbBase.cs +++ b/src/Before/DbBase.cs @@ -57,11 +57,22 @@ public class DbBase /// Filter applied to candidates of type . /// /// The context whose tracked entities to search. + /// When null, falls back to the singleton — + /// so a caller only passes a context explicitly when it must differ from + /// the ambient one. /// /// The first matching entity, or null when no match. /// + /// + /// Note the (deliberate, ActiveRecord-style) ambiguity this still leaves: + /// a null 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. + /// public static T? Find(Predicate predicate, DbContext? db) where T : DbBase { + db ??= Context; + if (db is null) return default; diff --git a/tests/BeforeAfter.Tests/BeforeTests.cs b/tests/BeforeAfter.Tests/BeforeTests.cs index 24b23c2..e4ed1ca 100644 --- a/tests/BeforeAfter.Tests/BeforeTests.cs +++ b/tests/BeforeAfter.Tests/BeforeTests.cs @@ -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] public void DbBase_Find_with_null_context_returns_default() @@ -238,6 +239,29 @@ public class BeforeTests 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(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. // --------------------------------------------------------------------- @@ -268,6 +292,28 @@ public class BeforeTests 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 // implicit context source for Find operations — the classic