diff --git a/db-subclass-to-dto.sln b/db-subclass-to-dto.sln
new file mode 100644
index 0000000..ed19c24
--- /dev/null
+++ b/db-subclass-to-dto.sln
@@ -0,0 +1,71 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Before", "src\Before\Before.csproj", "{2FF256B4-B085-449E-95AD-B9E6202DA94A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "After", "src\After\After.csproj", "{E65C0410-A863-44B0-88A4-6C74EF929419}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeforeAfter.Tests", "tests\BeforeAfter.Tests\BeforeAfter.Tests.csproj", "{F5E0C182-CBC3-4440-AF8F-32887F9B589C}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|x64.Build.0 = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Debug|x86.Build.0 = Debug|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|x64.ActiveCfg = Release|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|x64.Build.0 = Release|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|x86.ActiveCfg = Release|Any CPU
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A}.Release|x86.Build.0 = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|x64.Build.0 = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Debug|x86.Build.0 = Debug|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|x64.ActiveCfg = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|x64.Build.0 = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|x86.ActiveCfg = Release|Any CPU
+ {E65C0410-A863-44B0-88A4-6C74EF929419}.Release|x86.Build.0 = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|x64.Build.0 = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Debug|x86.Build.0 = Debug|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|x64.ActiveCfg = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|x64.Build.0 = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|x86.ActiveCfg = Release|Any CPU
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {2FF256B4-B085-449E-95AD-B9E6202DA94A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {E65C0410-A863-44B0-88A4-6C74EF929419} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {F5E0C182-CBC3-4440-AF8F-32887F9B589C} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ EndGlobalSection
+EndGlobal
diff --git a/src/After/After.csproj b/src/After/After.csproj
new file mode 100644
index 0000000..b760144
--- /dev/null
+++ b/src/After/After.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/src/Before/Before.csproj b/src/Before/Before.csproj
new file mode 100644
index 0000000..b760144
--- /dev/null
+++ b/src/Before/Before.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/src/Before/Client.cs b/src/Before/Client.cs
new file mode 100644
index 0000000..48c9e90
--- /dev/null
+++ b/src/Before/Client.cs
@@ -0,0 +1,50 @@
+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.
+///
+public class Client : DbBase
+{
+ /// Display name of the client.
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Navigation collection of this client's orders. Adding to it performs
+ /// EF-style navigation fix-up (see ).
+ ///
+ public ClientOrders Orders { get; }
+
+ public Client()
+ {
+ Orders = new ClientOrders(this);
+ }
+}
+
+///
+/// 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.
+///
+public sealed class ClientOrders : Collection
+{
+ private readonly Client _owner;
+
+ public ClientOrders(Client owner)
+ {
+ _owner = owner;
+ }
+
+ // Both Add(...) and Insert(...) funnel through InsertItem, so overriding
+ // it covers every way an order can be added to the collection.
+ protected override void InsertItem(int index, Order item)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+ item.Client = _owner; // navigation fix-up: order now knows its client
+ base.InsertItem(index, item);
+ }
+}
diff --git a/src/Before/DbBase.cs b/src/Before/DbBase.cs
new file mode 100644
index 0000000..d6b97b0
--- /dev/null
+++ b/src/Before/DbBase.cs
@@ -0,0 +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.
+///
+public class DbBase
+{
+ ///
+ /// Primary key. Fresh (unsaved) entities have ;
+ /// assigns a real id, faking EF's identity
+ /// generation.
+ ///
+ 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.
+ ///
+ public DbContext? DbContext { get; internal set; }
+}
\ No newline at end of file
diff --git a/src/Before/DbContext.cs b/src/Before/DbContext.cs
new file mode 100644
index 0000000..e11a3a3
--- /dev/null
+++ b/src/Before/DbContext.cs
@@ -0,0 +1,56 @@
+namespace Before;
+
+///
+/// A tiny hand-rolled fake of EF's DbContext: it keeps a change-tracker
+/// (a registration collection of the entities it knows
+/// about) 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.
+///
+public class DbContext
+{
+ // The change tracker: the set of entities this context is responsible for.
+ // (EF calls this its change tracker; a list stands in for the
+ // id -> entity registration dictionary.)
+ private readonly List _tracked = new();
+
+ ///
+ /// Attach an entity to this context (EF's Add). Sets the
+ /// active-record back-reference so the entity knows its owner.
+ ///
+ public void Attach(DbBase entity)
+ {
+ ArgumentNullException.ThrowIfNull(entity);
+ entity.DbContext = this;
+ if (!_tracked.Contains(entity))
+ _tracked.Add(entity);
+ }
+
+ ///
+ /// Fake SaveChangesAsync: walk the tracked entities 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.
+ ///
+ public int Save()
+ {
+ var saved = 0;
+ foreach (var entity in _tracked)
+ {
+ if (entity.Id == Guid.Empty)
+ {
+ entity.Id = Guid.NewGuid();
+ saved++;
+ }
+ }
+ return saved;
+ }
+
+ /// True if is in this context's tracker.
+ public bool IsTracked(DbBase entity) => _tracked.Contains(entity);
+
+ /// Read-only view of the entities this context currently tracks.
+ public IReadOnlyList Tracked => _tracked;
+}
diff --git a/src/Before/Order.cs b/src/Before/Order.cs
new file mode 100644
index 0000000..721d994
--- /dev/null
+++ b/src/Before/Order.cs
@@ -0,0 +1,20 @@
+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.
+///
+public class Order : DbBase
+{
+ /// Free-text description of what this order is for.
+ 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).
+ ///
+ public Client? Client { get; set; }
+}
diff --git a/src/Before/WebApp.cs b/src/Before/WebApp.cs
new file mode 100644
index 0000000..a325724
--- /dev/null
+++ b/src/Before/WebApp.cs
@@ -0,0 +1,40 @@
+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.
+///
+public class WebApp
+{
+ ///
+ /// Render a client together with all of its orders, consuming the
+ /// / domain objects directly.
+ ///
+ public string ShowClient(Client client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+
+ var lines = new List { $"{client.Name} [{client.Id}]" };
+ foreach (var order in client.Orders)
+ lines.Add($" - order [{order.Id}]: {order.Description}");
+ 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.
+ ///
+ public bool IsPersisted(Client client)
+ {
+ ArgumentNullException.ThrowIfNull(client);
+ return client.DbContext is not null;
+ }
+}
diff --git a/tests/BeforeAfter.Tests/BeforeAfter.Tests.csproj b/tests/BeforeAfter.Tests/BeforeAfter.Tests.csproj
new file mode 100644
index 0000000..c4ab542
--- /dev/null
+++ b/tests/BeforeAfter.Tests/BeforeAfter.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/BeforeAfter.Tests/BeforeTests.cs b/tests/BeforeAfter.Tests/BeforeTests.cs
new file mode 100644
index 0000000..0aace98
--- /dev/null
+++ b/tests/BeforeAfter.Tests/BeforeTests.cs
@@ -0,0 +1,145 @@
+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 client that was never attached/saved exposes no
+ // context yet, so the leak is dormant until the entity meets a context.
+ [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));
+ }
+}
\ No newline at end of file