ActiveRecord pattern: expose a shared DbContext on DbBase so any entity subclass can reach it without passing context through parameters. The instance-level DbContext back-reference still works; the static one provides a class-level global fallback.
162 lines
5.8 KiB
C#
162 lines
5.8 KiB
C#
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 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
|
|
}
|
|
} |