diff --git a/README.md b/README.md index 0159e34..2f07b8e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # AI Code Exploration Exercise -A small C# application in the Active Record style, for practicing using coding agents to explore unfamiliar codebases. +A small C# application for practicing code exploration with a coding agent: use it to produce diagrams that reveal the structure and behavior of an unfamiliar codebase. ## What this is -The `src/Before/` folder contains a small domain model (Client, Order) backed by a fake EF Core `DbContext`. The `src/Before.Console/` project is a demo console app that seeds data and queries it. +`src/Before/` contains a small domain model (Client, Order) with an in-memory `DbContext` and a `WebApp` consumer. `src/Before.Console/` is a demo console app that seeds data and queries it. `tests/Before.Tests/` covers the behavior. -**You do not need to read or understand the code before starting.** The point is to use a coding agent to produce diagrams that reveal the structure and behavior. +**You do not need to read or understand the code before starting.** The point is to use a coding agent to produce the diagrams. ## Prerequisites - A coding agent with access to this repo (Claude Code, Cursor, Copilot, etc.) -- .NET SDK 10.0 (to build and run) +- .NET SDK 10.0 - `mmdc` CLI for Mermaid rendering, or IDE Mermaid preview ## Setup @@ -25,30 +25,29 @@ The `src/Before/` folder contains a small domain model (Client, Order) backed by ## Tasks -Use a coding agent to create diagrams of this codebase. Start broad, then iterate with follow-up prompts to refine. +Start broad, then iterate with follow-up prompts to refine. -### Task 1: Class diagram (sanity check) +### Task 1: Class diagram -Prompt your agent to create a **class diagram** of the classes in `src/Before/` using Mermaid. Iterate until you're satisfied it captures the structure accurately. +Create a **class diagram** of the classes in `src/Before/` using Mermaid. Iterate until it captures the structure accurately. -**What to check:** Are the inheritance relationships correct? Are the associations (Client → Orders, Order → Client) visible? Is the DbContext leak (domain objects reaching persistence) shown? +**What to check:** Are the inheritance relationships correct? Are the associations (Client → Orders, Order → Client) visible? Is the dependency between the domain classes and `DbContext` shown — and in which direction does it point? ### Task 2: Sequence diagram — happy path -Prompt your agent to create a **sequence diagram** for the flow when `Client.FindByName("Jane Doe")` is called. +Create a **sequence diagram** for the flow when `Client.FindByName("Jane Doe")` is called. ### Task 3: Sequence diagram — exception path -Prompt your agent to create a **sequence diagram** for `Client.FindByNameRequired("Nobody")` — when the client is not found. What interactions differ from the happy path? Where is `ObjectNotFoundException` thrown? +Create a **sequence diagram** for `Client.FindByNameRequired("Nobody")` — when the client is not found. What interactions differ from the happy path? Where is `ObjectNotFoundException` thrown? ### Task 4 (stretch): Full initialization flow -From `Program.cs` (the Console entry point), trace how the database gets initialized and what interactions happen on `Client` and `Order` entities. Include the exception flow from Task 3. This will be a larger diagram — iterate and simplify as needed. +From `Program.cs` (the console entry point), trace how the `DbContext` is created and made available to entities, and what interactions happen on `Client` and `Order`. Include the exception flow from Task 3. This will be a larger diagram — iterate and simplify as needed. ## Tips - **Start broad, then refine:** "Draw me a class diagram of src/Before/" is enough to start. - **Iterate on the prompt:** If the first diagram is missing something, ask the agent to add it. You don't need to write Mermaid syntax. - **Validate:** Use `mmdc` to check syntax, visually check the diagram against the code. -- **The skill is directing the agent,** not writing diagrams by hand. - +- **Direct the agent;** don't write diagrams by hand. diff --git a/src/Before.Console/Program.cs b/src/Before.Console/Program.cs index b828ed1..ec72c2e 100644 --- a/src/Before.Console/Program.cs +++ b/src/Before.Console/Program.cs @@ -1,15 +1,7 @@ using Before; -// --------------------------------------------------------------------------- -// Before.Console — a straight-line demo of the ActiveRecord-style pattern. -// -// 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) — no context argument: rides the -// DbBase.Context singleton -// • listing via DbBase.Context!.Tracked -// --------------------------------------------------------------------------- +// Before.Console — seeds clients and orders, saves them through the +// DbContext, and queries them via the Client finders and DbBase.Context. var ctx = new DbContext(); DbBase.Context = ctx; @@ -23,7 +15,7 @@ jane.Orders.Add(new Order { Description = "Website redesign" }); bob.Orders.Add(new Order { Description = "Monthly retainer" }); -// Orders need to be attached too so they get IDs: +// Orders are attached separately so they get IDs: ctx.Attach(jane.Orders[0]); ctx.Attach(jane.Orders[1]); ctx.Attach(bob.Orders[0]); @@ -43,7 +35,6 @@ foreach (var client in DbBase.Context!.Tracked.OfType()) // ----- find by name ------------------------------------------------------- Console.WriteLine("\n=== Find by Name (null-returning) ==="); -// No context argument: FindByName falls back to the DbBase.Context singleton. var found = Client.FindByName("Jane Doe"); if (found is not null) { @@ -68,7 +59,7 @@ catch (ObjectNotFoundException ex) Console.WriteLine($"Not found: {ex.Message}"); } -// Show the contrast with a non-existent name: +// ----- a name that matches nothing ---------------------------------------- Console.WriteLine("\n=== Miss: null-returning vs throwing ==="); var missNull = Client.FindByName("Nobody Here"); Console.WriteLine($"FindByName(\"Nobody Here\"): {(missNull == null ? "null" : missNull.Name)}"); diff --git a/src/Before/Client.cs b/src/Before/Client.cs index 37a4ef0..d7abf0a 100644 --- a/src/Before/Client.cs +++ b/src/Before/Client.cs @@ -2,21 +2,13 @@ using System.Collections.ObjectModel; namespace Before; -/// -/// A client, backed by the database: it derives from and -/// holds its orders as a navigation collection. In the "after" situation this -/// becomes a plain POCO (no ) with a ClientDto -/// carrying its data to the WebApp. -/// +/// A client with a name and a collection of orders. public class Client : DbBase { - /// Display name of the client. + /// Display name. public string Name { get; set; } = string.Empty; - /// - /// Navigation collection of this client's orders. Adding to it performs - /// EF-style navigation fix-up (see ). - /// + /// The orders belonging to this client. public ClientOrders Orders { get; } public Client() @@ -25,49 +17,26 @@ public class Client : DbBase } /// - /// 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 - /// 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. + /// Returns the first client whose equals + /// , or null. Uses + /// when given, otherwise . /// public static Client? FindByName(string name, DbContext? db = null) => DbBase.Find(c => c.Name == name, db); /// - /// Throwing variant of . Returns - /// the matching or throws - /// with a message that includes the searched . - /// Throws instead when no context at all - /// is configured (a configuration error, not a lookup miss). - /// - /// Delegates to . + /// Returns the first client whose equals + /// . Throws + /// when there is no match, and + /// when no context is available. /// - /// The client name to search for. - /// The context whose tracked entities to search. When - /// null, falls back to the singleton. - /// The first client whose equals . - /// - /// Thrown when no matching client is found. - /// - /// - /// Thrown when no context is configured (neither passed explicitly nor set as the singleton). - /// public static Client FindByNameRequired(string name, DbContext? db = null) => DbBase.FindRequired(c => c.Name == name, db, $"name == \"{name}\""); } /// -/// An collection that fakes EF's navigation fix-up: when an -/// order is added, its back-reference is set to the -/// owning client, exactly as EF would wire up the two ends of the relation. +/// Order collection that sets to the owning +/// client when an order is added. /// public sealed class ClientOrders : Collection { @@ -78,12 +47,11 @@ public sealed class ClientOrders : Collection _owner = owner; } - // Both Add(...) and Insert(...) funnel through InsertItem, so overriding - // it covers every way an order can be added to the collection. + // Add and Insert both route through InsertItem. protected override void InsertItem(int index, Order item) { ArgumentNullException.ThrowIfNull(item); - item.Client = _owner; // navigation fix-up: order now knows its client + item.Client = _owner; base.InsertItem(index, item); } } diff --git a/src/Before/DbBase.cs b/src/Before/DbBase.cs index 4466f39..5f4f02a 100644 --- a/src/Before/DbBase.cs +++ b/src/Before/DbBase.cs @@ -1,74 +1,29 @@ namespace Before; /// -/// Base class for every DB-backed domain object. Fakes the active-record part -/// of EF: each entity carries its own and, once it has been -/// created/saved through a context, a back-reference to that context. -/// -/// is the dependency this base class leaks. The -/// "after" situation (Yak 02) removes this inheritance entirely — domain -/// objects become plain POCOs that know nothing about a DbContext. +/// Base class for entities stored in a : carries an +/// and, once attached, a reference to the owning context. /// public class DbBase { - // ----------------------------------------------------------------- - // Static singleton: an ActiveRecord-style shared context that any - // entity can reach without passing it through method parameters. - // ----------------------------------------------------------------- /// - /// Shared (singleton) accessible from every - /// entity via its base type. Set once at application startup so that - /// entity methods can call DbBase.Context! instead of carrying - /// a context reference. - /// - /// This is yet another leak: domain objects depend on the persistence - /// layer at the *type* level, not just the instance level. + /// Shared context used by static finders when no context is passed + /// explicitly. Set at application startup. /// public static DbContext? Context { get; set; } - /// - /// Primary key. Fresh (unsaved) entities have ; - /// assigns a real id, faking EF's identity - /// generation. - /// + /// Entity key; until saved. public Guid Id { get; set; } - /// - /// Active-record back-reference: "I know which context created me". This - /// is the leak the exercise exposes — from a plain domain object you can - /// reach straight into the persistence layer. internal set because - /// only the owning may (re)assign it; consumers - /// (including the WebApp, and the tests) can only read it. - /// + /// The context this entity is attached to, or null. public DbContext? DbContext { get; internal set; } /// - /// Generic ActiveRecord-style finder: walks the 's tracked entities, looks for the first whose runtime type matches - /// and satisfies - ///. - /// - /// This static generic rides the DbContext leak just like - /// , but works for any subtype without each entity needing its own hand-written finder. + /// Returns the first tracked entity of type that + /// satisfies , or null when there is no + /// match or no context. Uses when given, otherwise + /// . /// - /// Entity type to find (must derive from ). - /// - /// 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; @@ -84,27 +39,12 @@ public class DbBase } /// - /// Throwing variant of . Finds the first entity - /// whose runtime type matches and satisfies - /// . Throws - /// when no match is found, and when no - /// singleton is configured (a configuration error, not a lookup miss). - /// - /// The exception message includes so - /// callers can debug which lookup failed. + /// Returns the first tracked entity of type that + /// satisfies . Throws + /// when there is no match, and + /// when no context is available. + /// is included in the miss message. /// - /// Filter applied to candidates of type . - /// The context whose tracked entities to search. When null, falls back - /// to the singleton. - /// A human-readable description of the predicate, used in the - /// exception message when the search fails. - /// The first matching entity. - /// - /// Thrown when no entity matches . - /// - /// - /// Thrown when no context is available (neither passed explicitly nor set as the singleton). - /// public static T FindRequired(Predicate predicate, DbContext? db, string predicateToString) where T : DbBase { @@ -125,4 +65,4 @@ public class DbBase $"FindRequired<{typeof(T).Name}>({predicateToString}) — " + $"no matching {typeof(T).Name} found in context."); } -} \ No newline at end of file +} diff --git a/src/Before/DbContext.cs b/src/Before/DbContext.cs index 3a3a9ca..5201eca 100644 --- a/src/Before/DbContext.cs +++ b/src/Before/DbContext.cs @@ -3,40 +3,18 @@ using System.Collections.ObjectModel; namespace Before; /// -/// 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* -/// (domain object -> DbContext), not EF's behaviour. +/// In-memory data store: one collection per entity type, id assignment on +/// save, and a configurable delay on every operation. /// public class DbContext { - // 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(); - // ----------------------------------------------------------------- - // Simulated DB latency. Every "round trip" through this context — - // Attach, Save, Find, IsTracked, Tracked — sleeps for this long, the - // way a real database makes every call pay a network + query cost. - // Deliberately on by default: the console demo and the tests are - // supposed to feel sluggish, so the cost of persistence becomes - // visible. Set to TimeSpan.Zero for an instant fake. - // ----------------------------------------------------------------- - /// - /// Simulated latency per DB operation. Default 200 ms. - /// + /// Delay applied to every operation. Default: 200 ms. public static TimeSpan Latency { get; set; } = TimeSpan.FromMilliseconds(200); private static void SimulateLatency() => Thread.Sleep(Latency); - /// - /// 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); @@ -48,10 +26,6 @@ public class DbContext 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)) @@ -63,10 +37,8 @@ public class DbContext } /// - /// 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. + /// Adds the entity to the table for its runtime type and sets its + /// reference. /// public void Attach(DbBase entity) { @@ -79,10 +51,8 @@ public class DbContext } /// - /// 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. + /// Assigns a fresh to every attached entity whose id is + /// still . Returns the number of entities saved. /// public int Save() { @@ -99,17 +69,9 @@ public class DbContext } /// - /// 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 - /// . + /// Returns the first entity in the table for that + /// satisfies , or null. /// - /// 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 { SimulateLatency(); @@ -120,10 +82,7 @@ public class DbContext return default; } - /// - /// True if is in this context's tracker - /// (walks all per-type tables). - /// + /// Returns true if the entity is in any table. public bool IsTracked(DbBase entity) { SimulateLatency(); @@ -133,10 +92,7 @@ public class DbContext return false; } - /// - /// Read-only view of all entities this context currently tracks - /// (flattened across all per-type tables). - /// + /// All tracked entities, across all tables. public IReadOnlyList Tracked { get diff --git a/src/Before/ObjectNotFoundException.cs b/src/Before/ObjectNotFoundException.cs index b4bb35e..ace3972 100644 --- a/src/Before/ObjectNotFoundException.cs +++ b/src/Before/ObjectNotFoundException.cs @@ -1,16 +1,6 @@ namespace Before; -/// -/// Thrown when a "required" ActiveRecord-style lookup — -/// / — -/// finds no matching entity in the context. -/// -/// Named after NHibernate's ObjectNotFoundException (Rails' ActiveRecord -/// raises ActiveRecord::RecordNotFound for the same situation), so a -/// lookup miss is distinguishable from unrelated -/// s such as EF's Single() -/// "Sequence contains no elements". -/// +/// Thrown when a required lookup finds no matching entity. public class ObjectNotFoundException : Exception { public ObjectNotFoundException(string message) : base(message) { } diff --git a/src/Before/Order.cs b/src/Before/Order.cs index 721d994..b562985 100644 --- a/src/Before/Order.cs +++ b/src/Before/Order.cs @@ -1,20 +1,11 @@ namespace Before; -/// -/// An order, backed by the database: it derives from and -/// holds a back-reference to its (the other end of the -/// relation). In the "after" situation this becomes a plain POCO with an -/// OrderDto carrying its data to the WebApp. -/// +/// An order belonging to a . public class Order : DbBase { - /// Free-text description of what this order is for. + /// Free-text description. public string Description { get; set; } = string.Empty; - /// - /// The client that owns this order. Set automatically by - /// when the order is added to - /// (navigation fix-up). - /// + /// The owning client; set by . public Client? Client { get; set; } } diff --git a/src/Before/WebApp.cs b/src/Before/WebApp.cs index a325724..9affc8f 100644 --- a/src/Before/WebApp.cs +++ b/src/Before/WebApp.cs @@ -1,19 +1,13 @@ namespace Before; /// -/// The consuming application. In the "before" situation it talks to the -/// DB-backed domain model directly: its methods take and -/// (which inherit ) as parameters. -/// -/// One of its methods reaches the *through* a domain -/// object — the leak. In the "after" situation (Yak 02) the WebApp consumes -/// DTOs instead, and the domain objects no longer expose a DbContext. +/// Consumes and domain objects +/// directly. /// public class WebApp { /// - /// Render a client together with all of its orders, consuming the - /// / domain objects directly. + /// Returns the client's name, id, and orders formatted as text. /// public string ShowClient(Client client) { @@ -25,13 +19,7 @@ public class WebApp return string.Join(Environment.NewLine, lines); } - /// - /// The leak, made concrete: from a plain domain object the WebApp can reach - /// the () and thus - /// touch the persistence layer — here just to ask whether the client has - /// been saved. The "after" situation removes client.DbContext - /// entirely, so no DTO-consumer can do this. - /// + /// Returns true when the client is attached to a context. public bool IsPersisted(Client client) { ArgumentNullException.ThrowIfNull(client); diff --git a/tests/Before.Tests/BeforeTests.cs b/tests/Before.Tests/BeforeTests.cs index c41f2c5..76a8ffe 100644 --- a/tests/Before.Tests/BeforeTests.cs +++ b/tests/Before.Tests/BeforeTests.cs @@ -3,15 +3,11 @@ using Before; namespace BeforeAfter.Tests; /// -/// Tests for the BEFORE situation (Yak 01): DB-backed domain classes that -/// inherit and leak their . +/// Tests for the Client, Order, DbBase, DbContext, and WebApp classes. /// public class BeforeTests { - // --------------------------------------------------------------------- - // (1) Inheritance: the domain classes ARE DB-backed (they derive from - // DbBase). This is the "before" shape Yak 02 will undo. - // --------------------------------------------------------------------- + // Client and Order derive from DbBase. [Fact] public void Client_and_Order_derive_from_DbBase() { @@ -19,9 +15,7 @@ public class BeforeTests Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Order))); } - // --------------------------------------------------------------------- - // (2) Fresh entities are unsaved: their Id is still Guid.Empty. - // --------------------------------------------------------------------- + // New entities have Id == Guid.Empty. [Theory] [InlineData(typeof(Client))] [InlineData(typeof(Order))] @@ -31,10 +25,7 @@ public class BeforeTests Assert.Equal(Guid.Empty, entity.Id); } - // --------------------------------------------------------------------- - // (3) Save() fakes EF's SaveChanges: it assigns fresh, distinct Guids to - // entities that do not have one yet. - // --------------------------------------------------------------------- + // Save assigns fresh, distinct Guids to unsaved entities. [Fact] public void Save_assigns_fresh_distinct_ids() { @@ -51,10 +42,7 @@ public class BeforeTests Assert.NotEqual(acme.Id, globex.Id); } - // --------------------------------------------------------------------- - // (4) A saved entity is registered with its context, reachable via the - // active-record back-reference. - // --------------------------------------------------------------------- + // A saved entity references its context and is tracked by it. [Fact] public void Saved_entity_is_registered_with_its_context() { @@ -67,10 +55,7 @@ public class BeforeTests Assert.True(db.IsTracked(client)); } - // --------------------------------------------------------------------- - // (5) Navigation fix-up: adding an order to client.Orders wires the - // order.Client back-reference. - // --------------------------------------------------------------------- + // Adding an order to client.Orders sets order.Client. [Fact] public void Adding_an_order_sets_the_client_back_reference() { @@ -84,30 +69,21 @@ public class BeforeTests Assert.Single(client.Orders); } - // --------------------------------------------------------------------- - // (6) THE LEAK: from a plain Client you can reach its DbContext. - // This is exactly what the "after" situation removes: a ClientDto has - // no DbContext to reach, so a consumer can never touch the persistence - // layer through it. - // --------------------------------------------------------------------- + // A client exposes the context it was attached to. [Fact] - public void A_client_can_reach_its_DbContext__the_leak() + public void A_client_can_reach_its_DbContext() { var db = new DbContext(); var client = new Client { Name = "Acme" }; db.Attach(client); db.Save(); - // Reaching the persistence layer *through* the domain object: Assert.NotNull(client.DbContext); Assert.Same(db, client.DbContext); Assert.True(client.DbContext!.IsTracked(client)); } - // --------------------------------------------------------------------- - // (7) The WebApp consumes the domain classes directly — including a method - // that rides the leak. - // --------------------------------------------------------------------- + // WebApp works on domain classes directly. [Fact] public void WebApp_works_on_the_domain_classes_directly() { @@ -121,17 +97,14 @@ public class BeforeTests db.Attach(order); db.Save(); - // ShowClient consumes Client and its Orders directly: var view = app.ShowClient(client); Assert.Contains("Acme", view); Assert.Contains("order one", view); - // IsPersisted rides the leak (client.DbContext) — and it is true here - // because the client was saved through the context: Assert.True(app.IsPersisted(client)); } - // A companion check: a bare client has no context until saved. + // A bare client has no context until saved. [Fact] public void A_bare_client_has_no_context_until_saved() { @@ -142,29 +115,21 @@ public class BeforeTests Assert.False(app.IsPersisted(client)); } - // ----------------------------------------------------------------- - // (8) Static Context (Singleton): DbBase exposes a class-level - // shared DbContext. Setting it makes the context accessible - // from any entity via its base type. - // ----------------------------------------------------------------- + // DbBase exposes a static, shared Context reference. [Fact] public void DbBase_has_a_static_Context_singleton() { - Assert.Null(DbBase.Context); // fresh — not set yet + Assert.Null(DbBase.Context); var db = new DbContext(); DbBase.Context = db; Assert.Same(db, DbBase.Context); - DbBase.Context = null; // cleanup + DbBase.Context = null; // cleanup } - // --------------------------------------------------------------------- - // (9) ActiveRecord finder: DbBase.Find walks the context's tracked - // entities, looks for the first whose runtime type matches T, - // and returns it when matches. - // --------------------------------------------------------------------- + // DbBase.Find returns the first tracked entity of type T matching the predicate. [Fact] public void DbBase_Find_finds_matching_entity() { @@ -180,10 +145,7 @@ public class BeforeTests Assert.Same(globex, result); } - // --------------------------------------------------------------------- - // (10) DbBase.Find returns null when no entity of type T satisfies - // the predicate. - // --------------------------------------------------------------------- + // DbBase.Find returns null when there is no match. [Fact] public void DbBase_Find_returns_null_when_no_match() { @@ -195,10 +157,7 @@ public class BeforeTests Assert.Null(result); } - // --------------------------------------------------------------------- - // (11) DbBase.Find filters by runtime type — an Order attached to the - // context does NOT match a Client predicate. - // --------------------------------------------------------------------- + // DbBase.Find only matches entities of type T. [Fact] public void DbBase_Find_ignores_non_matching_types() { @@ -210,9 +169,7 @@ public class BeforeTests Assert.Null(result); } - // --------------------------------------------------------------------- - // (12) DbBase.Find returns the FIRST matching entity only. - // --------------------------------------------------------------------- + // DbBase.Find returns the first match only. [Fact] public void DbBase_Find_returns_first_match() { @@ -228,10 +185,7 @@ public class BeforeTests Assert.NotSame(second, result); } - // --------------------------------------------------------------------- - // (13) DbBase.Find handles a null context gracefully — with no explicit - // context AND no singleton configured, it returns default (null). - // --------------------------------------------------------------------- + // DbBase.Find returns null when no context is available. [Fact] public void DbBase_Find_with_null_context_returns_default() { @@ -239,8 +193,7 @@ 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. + // DbBase.Find falls back to DbBase.Context when db is null. [Fact] public void DbBase_Find_falls_back_to_Context_singleton_when_db_is_null() { @@ -262,9 +215,7 @@ public class BeforeTests } } - // --------------------------------------------------------------------- - // (14) Client.FindByName delegates to DbBase.Find and works correctly. - // --------------------------------------------------------------------- + // Client.FindByName delegates to DbBase.Find. [Fact] public void Client_FindByName_finds_by_name() { @@ -278,9 +229,7 @@ public class BeforeTests Assert.Same(acme, result); } - // --------------------------------------------------------------------- - // (15) Client.FindByName returns null when the name does not match. - // --------------------------------------------------------------------- + // Client.FindByName returns null when there is no match. [Fact] public void Client_FindByName_returns_null_when_not_found() { @@ -292,7 +241,7 @@ public class BeforeTests Assert.Null(result); } - // (15b) FindByName with no context argument uses the Context singleton. + // Client.FindByName uses DbBase.Context when db is omitted. [Fact] public void Client_FindByName_uses_Context_singleton_when_db_omitted() { @@ -314,12 +263,7 @@ public class BeforeTests } } - // --------------------------------------------------------------------- - // (16) The class-level DbBase.Context singleton can be used as the - // implicit context source for Find operations — the classic - // ActiveRecord pattern where any entity method reaches the shared - // context without parameter passing. - // --------------------------------------------------------------------- + // DbBase.Context can be passed as the context argument to Find. [Fact] public void DbBase_Context_singleton_is_used_in_Find() { @@ -329,9 +273,6 @@ public class BeforeTests DbBase.Context = db; - // Query using the singleton instead of passing the context: - // (In practice, callers often do this to avoid threading db through - // every call — the whole point of the ActiveRecord leak.) var queryResult = DbBase.Find(c => c.Name == "TargetCo", DbBase.Context); Assert.NotNull(queryResult); @@ -340,9 +281,7 @@ public class BeforeTests DbBase.Context = null; // cleanup } - // --------------------------------------------------------------------- - // (17) DbBase.FindRequired finds the matching entity — same result as Find. - // --------------------------------------------------------------------- + // DbBase.FindRequired returns the matching entity. [Fact] public void DbBase_FindRequired_T_finds_matching_entity() { @@ -357,10 +296,7 @@ public class BeforeTests Assert.Same(globex, result); } - // --------------------------------------------------------------------- - // (18) DbBase.FindRequired throws ObjectNotFoundException when no match - // is found (the domain-specific lookup miss, not a BCL exception). - // --------------------------------------------------------------------- + // DbBase.FindRequired throws ObjectNotFoundException when there is no match. [Fact] public void DbBase_FindRequired_T_throws_when_no_match() { @@ -374,13 +310,8 @@ public class BeforeTests Assert.Contains("no matching Client found", ex.Message); } - // --------------------------------------------------------------------- - // (19) DbBase.FindRequired throws with a useful message when no context is - // configured (both explicit null and singleton null). - // - // Deliberately InvalidOperationException, not ObjectNotFoundException: - // a missing context is a configuration error, not a lookup miss. - // --------------------------------------------------------------------- + // DbBase.FindRequired throws InvalidOperationException when no context is + // configured — InvalidOperationException, not ObjectNotFoundException. [Fact] public void DbBase_FindRequired_T_throws_when_no_context_configured() { @@ -393,9 +324,7 @@ public class BeforeTests Assert.Contains("DbBase.Context", ex.Message); } - // --------------------------------------------------------------------- - // (20) Client.FindByNameRequired finds the matching client by name. - // --------------------------------------------------------------------- + // Client.FindByNameRequired returns the matching client. [Fact] public void Client_FindByNameRequired_finds_by_name() { @@ -408,10 +337,8 @@ public class BeforeTests Assert.Same(acme, result); } - // --------------------------------------------------------------------- - // (21) Client.FindByNameRequired throws ObjectNotFoundException when no - // match — message includes name. - // --------------------------------------------------------------------- + // Client.FindByNameRequired throws ObjectNotFoundException when there is no + // match; the message includes the name. [Fact] public void Client_FindByNameRequired_throws_with_name_when_not_found() { @@ -425,9 +352,7 @@ public class BeforeTests Assert.Contains("no matching Client found", ex.Message); } - // --------------------------------------------------------------------- - // (22) Client.FindByNameRequired uses Context singleton when no context arg. - // --------------------------------------------------------------------- + // Client.FindByNameRequired uses DbBase.Context when no context is passed. [Fact] public void Client_FindByNameRequired_uses_Context_singleton() { @@ -447,11 +372,8 @@ public class BeforeTests } } - // --------------------------------------------------------------------- - // (23) Client.FindByNameRequired throws with a useful message when the - // singleton is not configured (no arg, no singleton). - // Stays InvalidOperationException: configuration error, not a miss. - // --------------------------------------------------------------------- + // Client.FindByNameRequired throws InvalidOperationException when no + // context is configured. [Fact] public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null() { @@ -463,4 +385,4 @@ public class BeforeTests Assert.Contains("no DbContext configured", ex.Message); Assert.Contains("DbBase.Context", ex.Message); } -} \ No newline at end of file +}