Yak: csharp before situation (Yak 01) — DB-backed domain classes
Add the C# BEFORE situation: a fake-EF DbContext + DbBase that Client/Order inherit, the active-record DbContext back-reference (the dependency leak), navigation fix-up on Client.Orders, and a WebApp that consumes the domain classes directly. Adds an empty After project so the solution shape is final, and an xUnit BeforeTests suite covering the required assertions. - db-subclass-to-dto.sln (classic .sln; SDK 10 defaults to .slnx which breaks run-tests.sh) - src/Before: DbBase, DbContext, Client (+ClientOrders fix-up), Order, WebApp - src/After: empty placeholder (filled in Yak 02) - tests/BeforeAfter.Tests/BeforeTests.cs: 8 tests (7 required + 1 companion)
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// A client, backed by the database: it derives from <see cref="DbBase"/> and
|
||||
/// holds its orders as a navigation collection. In the "after" situation this
|
||||
/// becomes a plain POCO (no <see cref="DbBase"/>) with a <c>ClientDto</c>
|
||||
/// carrying its data to the WebApp.
|
||||
/// </summary>
|
||||
public class Client : DbBase
|
||||
{
|
||||
/// <summary>Display name of the client.</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Navigation collection of this client's orders. Adding to it performs
|
||||
/// EF-style navigation fix-up (see <see cref="ClientOrders"/>).
|
||||
/// </summary>
|
||||
public ClientOrders Orders { get; }
|
||||
|
||||
public Client()
|
||||
{
|
||||
Orders = new ClientOrders(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="Order"/> collection that fakes EF's navigation fix-up: when an
|
||||
/// order is added, its <see cref="Order.Client"/> back-reference is set to the
|
||||
/// owning client, exactly as EF would wire up the two ends of the relation.
|
||||
/// </summary>
|
||||
public sealed class ClientOrders : Collection<Order>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for every DB-backed domain object. Fakes the active-record part
|
||||
/// of EF: each entity carries its own <see cref="Id"/> and, once it has been
|
||||
/// created/saved through a context, a back-reference to that context.
|
||||
///
|
||||
/// <see cref="DbContext"/> 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.
|
||||
/// </summary>
|
||||
public class DbBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Primary key. Fresh (unsaved) entities have <see cref="Guid.Empty"/>;
|
||||
/// <see cref="DbContext.Save"/> assigns a real id, faking EF's identity
|
||||
/// generation.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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. <c>internal set</c> because
|
||||
/// only the owning <see cref="DbContext"/> may (re)assign it; consumers
|
||||
/// (including the WebApp, and the tests) can only read it.
|
||||
/// </summary>
|
||||
public DbContext? DbContext { get; internal set; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// A tiny hand-rolled fake of EF's <c>DbContext</c>: it keeps a change-tracker
|
||||
/// (a registration collection of the <see cref="DbBase"/> entities it knows
|
||||
/// about) and a <see cref="Save"/> that fakes <c>SaveChangesAsync</c>.
|
||||
///
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<DbBase> _tracked = new();
|
||||
|
||||
/// <summary>
|
||||
/// Attach an entity to this context (EF's <c>Add</c>). Sets the
|
||||
/// active-record back-reference so the entity knows its owner.
|
||||
/// </summary>
|
||||
public void Attach(DbBase entity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entity);
|
||||
entity.DbContext = this;
|
||||
if (!_tracked.Contains(entity))
|
||||
_tracked.Add(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake <c>SaveChangesAsync</c>: walk the tracked entities and assign a
|
||||
/// fresh <see cref="Guid"/> to any whose id is still <see cref="Guid.Empty"/>.
|
||||
/// Returns the number of entities that were (re)saved — i.e. newly
|
||||
/// identified — mirroring EF's "rows written" return value.
|
||||
/// </summary>
|
||||
public int Save()
|
||||
{
|
||||
var saved = 0;
|
||||
foreach (var entity in _tracked)
|
||||
{
|
||||
if (entity.Id == Guid.Empty)
|
||||
{
|
||||
entity.Id = Guid.NewGuid();
|
||||
saved++;
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
/// <summary>True if <paramref name="entity"/> is in this context's tracker.</summary>
|
||||
public bool IsTracked(DbBase entity) => _tracked.Contains(entity);
|
||||
|
||||
/// <summary>Read-only view of the entities this context currently tracks.</summary>
|
||||
public IReadOnlyList<DbBase> Tracked => _tracked;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// An order, backed by the database: it derives from <see cref="DbBase"/> and
|
||||
/// holds a back-reference to its <see cref="Client"/> (the other end of the
|
||||
/// relation). In the "after" situation this becomes a plain POCO with an
|
||||
/// <c>OrderDto</c> carrying its data to the WebApp.
|
||||
/// </summary>
|
||||
public class Order : DbBase
|
||||
{
|
||||
/// <summary>Free-text description of what this order is for.</summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The client that owns this order. Set automatically by
|
||||
/// <see cref="ClientOrders"/> when the order is added to
|
||||
/// <see cref="Client.Orders"/> (navigation fix-up).
|
||||
/// </summary>
|
||||
public Client? Client { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Before;
|
||||
|
||||
/// <summary>
|
||||
/// The consuming application. In the "before" situation it talks to the
|
||||
/// DB-backed domain model directly: its methods take <see cref="Client"/> and
|
||||
/// <see cref="Order"/> (which inherit <see cref="DbBase"/>) as parameters.
|
||||
///
|
||||
/// One of its methods reaches the <see cref="DbContext"/> *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.
|
||||
/// </summary>
|
||||
public class WebApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Render a client together with all of its orders, consuming the
|
||||
/// <see cref="Client"/>/ <see cref="Order"/> domain objects directly.
|
||||
/// </summary>
|
||||
public string ShowClient(Client client)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
|
||||
var lines = new List<string> { $"{client.Name} [{client.Id}]" };
|
||||
foreach (var order in client.Orders)
|
||||
lines.Add($" - order [{order.Id}]: {order.Description}");
|
||||
return string.Join(Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The leak, made concrete: from a plain domain object the WebApp can reach
|
||||
/// the <see cref="DbContext"/> (<see cref="DbBase.DbContext"/>) and thus
|
||||
/// touch the persistence layer — here just to ask whether the client has
|
||||
/// been saved. The "after" situation removes <c>client.DbContext</c>
|
||||
/// entirely, so no DTO-consumer can do this.
|
||||
/// </summary>
|
||||
public bool IsPersisted(Client client)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
return client.DbContext is not null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Before\Before.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,145 @@
|
||||
using Before;
|
||||
|
||||
namespace BeforeAfter.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the BEFORE situation (Yak 01): DB-backed domain classes that
|
||||
/// inherit <see cref="DbBase"/> and leak their <see cref="DbContext"/>.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user