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 } }