using Before; namespace BeforeAfter.Tests; /// /// Tests for the BEFORE situation (Yak 01): DB-backed domain classes that /// inherit and leak their . /// public class BeforeTests { // --------------------------------------------------------------------- // (1) Inheritance: the domain classes ARE DB-backed (they derive from // DbBase). This is the "before" shape Yak 02 will undo. // --------------------------------------------------------------------- [Fact] public void Client_and_Order_derive_from_DbBase() { Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Client))); Assert.True(typeof(DbBase).IsAssignableFrom(typeof(Order))); } // --------------------------------------------------------------------- // (2) Fresh entities are unsaved: their Id is still Guid.Empty. // --------------------------------------------------------------------- [Theory] [InlineData(typeof(Client))] [InlineData(typeof(Order))] public void New_entities_have_an_empty_id(Type type) { var entity = (DbBase)Activator.CreateInstance(type)!; 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. // --------------------------------------------------------------------- [Fact] public void Save_assigns_fresh_distinct_ids() { var db = new DbContext(); var acme = new Client { Name = "Acme" }; var globex = new Client { Name = "Globex" }; db.Attach(acme); db.Attach(globex); Assert.Equal(2, db.Save()); Assert.NotEqual(Guid.Empty, acme.Id); Assert.NotEqual(Guid.Empty, globex.Id); Assert.NotEqual(acme.Id, globex.Id); } // --------------------------------------------------------------------- // (4) A saved entity is registered with its context, reachable via the // active-record back-reference. // --------------------------------------------------------------------- [Fact] public void Saved_entity_is_registered_with_its_context() { var db = new DbContext(); var client = new Client { Name = "Acme" }; db.Attach(client); db.Save(); Assert.Same(db, client.DbContext); Assert.True(db.IsTracked(client)); } // --------------------------------------------------------------------- // (5) Navigation fix-up: adding an order to client.Orders wires the // order.Client back-reference. // --------------------------------------------------------------------- [Fact] public void Adding_an_order_sets_the_client_back_reference() { var client = new Client { Name = "Acme" }; var order = new Order { Description = "first order" }; client.Orders.Add(order); Assert.Same(client, order.Client); Assert.Same(order, client.Orders[0]); 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. // --------------------------------------------------------------------- [Fact] public void A_client_can_reach_its_DbContext__the_leak() { 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. // --------------------------------------------------------------------- [Fact] public void WebApp_works_on_the_domain_classes_directly() { var app = new WebApp(); var db = new DbContext(); var client = new Client { Name = "Acme" }; var order = new Order { Description = "order one" }; client.Orders.Add(order); db.Attach(client); 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. [Fact] public void A_bare_client_has_no_context_until_saved() { var app = new WebApp(); var client = new Client { Name = "Nobody" }; Assert.Null(client.DbContext); 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. // ----------------------------------------------------------------- [Fact] public void DbBase_has_a_static_Context_singleton() { Assert.Null(DbBase.Context); // fresh — not set yet var db = new DbContext(); DbBase.Context = db; Assert.Same(db, DbBase.Context); 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. // --------------------------------------------------------------------- [Fact] public void DbBase_Find_finds_matching_entity() { var db = new DbContext(); var acme = new Client { Name = "Acme" }; var globex = new Client { Name = "Globex" }; db.Attach(acme); db.Attach(globex); var result = DbBase.Find(c => c.Name == "Globex", db); Assert.NotNull(result); Assert.Same(globex, result); } // --------------------------------------------------------------------- // (10) DbBase.Find returns null when no entity of type T satisfies // the predicate. // --------------------------------------------------------------------- [Fact] public void DbBase_Find_returns_null_when_no_match() { var db = new DbContext(); db.Attach(new Client { Name = "Acme" }); var result = DbBase.Find(c => c.Name == "Nobody", db); Assert.Null(result); } // --------------------------------------------------------------------- // (11) DbBase.Find filters by runtime type — an Order attached to the // context does NOT match a Client predicate. // --------------------------------------------------------------------- [Fact] public void DbBase_Find_ignores_non_matching_types() { var db = new DbContext(); db.Attach(new Order { Description = "test" }); var result = DbBase.Find(_ => true, db); Assert.Null(result); } // --------------------------------------------------------------------- // (12) DbBase.Find returns the FIRST matching entity only. // --------------------------------------------------------------------- [Fact] public void DbBase_Find_returns_first_match() { var db = new DbContext(); var first = new Client { Name = "SameName" }; var second = new Client { Name = "SameName" }; db.Attach(first); db.Attach(second); var result = DbBase.Find(c => c.Name == "SameName", db); Assert.Same(first, result); // first one inserted wins Assert.NotSame(second, result); } // --------------------------------------------------------------------- // (13) DbBase.Find handles a null context gracefully. // --------------------------------------------------------------------- [Fact] public void DbBase_Find_with_null_context_returns_default() { var result = DbBase.Find(_ => true, null); Assert.Null(result); } // --------------------------------------------------------------------- // (14) Client.FindByName delegates to DbBase.Find and works correctly. // --------------------------------------------------------------------- [Fact] public void Client_FindByName_finds_by_name() { var db = new DbContext(); var acme = new Client { Name = "Acme Corp" }; db.Attach(acme); var result = Client.FindByName("Acme Corp", db); Assert.NotNull(result); Assert.Same(acme, result); } // --------------------------------------------------------------------- // (15) Client.FindByName returns null when the name does not match. // --------------------------------------------------------------------- [Fact] public void Client_FindByName_returns_null_when_not_found() { var db = new DbContext(); db.Attach(new Client { Name = "Acme Corp" }); var result = Client.FindByName("Nobody", db); Assert.Null(result); } // --------------------------------------------------------------------- // (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. // --------------------------------------------------------------------- [Fact] public void DbBase_Context_singleton_is_used_in_Find() { var db = new DbContext(); var targeted = new Client { Name = "TargetCo" }; db.Attach(targeted); 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); Assert.Same(targeted, queryResult); DbBase.Context = null; // cleanup } }