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 — with no explicit
// context AND no singleton configured, it returns default (null).
// ---------------------------------------------------------------------
[Fact]
public void DbBase_Find_with_null_context_returns_default()
{
var result = DbBase.Find(_ => true, null);
Assert.Null(result);
}
// (13b) When no explicit context is passed, Find falls back to the
// DbBase.Context singleton instead of returning null.
[Fact]
public void DbBase_Find_falls_back_to_Context_singleton_when_db_is_null()
{
var db = new DbContext();
var acme = new Client { Name = "SingletonCo" };
db.Attach(acme);
DbBase.Context = db;
try
{
var result = DbBase.Find(c => c.Name == "SingletonCo", null);
Assert.NotNull(result);
Assert.Same(acme, result);
}
finally
{
DbBase.Context = null; // cleanup
}
}
// ---------------------------------------------------------------------
// (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);
}
// (15b) FindByName with no context argument uses the Context singleton.
[Fact]
public void Client_FindByName_uses_Context_singleton_when_db_omitted()
{
var db = new DbContext();
var acme = new Client { Name = "Acme Corp" };
db.Attach(acme);
DbBase.Context = db;
try
{
var result = Client.FindByName("Acme Corp");
Assert.NotNull(result);
Assert.Same(acme, result);
}
finally
{
DbBase.Context = null; // cleanup
}
}
// ---------------------------------------------------------------------
// (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
}
// ---------------------------------------------------------------------
// (17) DbBase.FindRequired finds the matching entity — same result as Find.
// ---------------------------------------------------------------------
[Fact]
public void DbBase_FindRequired_T_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.FindRequired(c => c.Name == "Globex", db, "name == Globex");
Assert.Same(globex, result);
}
// ---------------------------------------------------------------------
// (18) DbBase.FindRequired throws ObjectNotFoundException when no match
// is found (the domain-specific lookup miss, not a BCL exception).
// ---------------------------------------------------------------------
[Fact]
public void DbBase_FindRequired_T_throws_when_no_match()
{
var db = new DbContext();
db.Attach(new Client { Name = "Acme" });
var ex = Assert.Throws(() =>
DbBase.FindRequired(c => c.Name == "Nobody", db, "name == Nobody"));
Assert.Contains("Nobody", ex.Message);
Assert.Contains("no matching Client found", ex.Message);
}
// ---------------------------------------------------------------------
// (19) DbBase.FindRequired throws with a useful message when no context is
// configured (both explicit null and singleton null).
//
// Deliberately InvalidOperationException, not ObjectNotFoundException:
// a missing context is a configuration error, not a lookup miss.
// ---------------------------------------------------------------------
[Fact]
public void DbBase_FindRequired_T_throws_when_no_context_configured()
{
DbBase.Context = null;
var ex = Assert.Throws(() =>
DbBase.FindRequired(_ => true, null, "true"));
Assert.Contains("no DbContext configured", ex.Message);
Assert.Contains("DbBase.Context", ex.Message);
}
// ---------------------------------------------------------------------
// (20) Client.FindByNameRequired finds the matching client by name.
// ---------------------------------------------------------------------
[Fact]
public void Client_FindByNameRequired_finds_by_name()
{
var db = new DbContext();
var acme = new Client { Name = "Acme Corp" };
db.Attach(acme);
var result = Client.FindByNameRequired("Acme Corp", db);
Assert.Same(acme, result);
}
// ---------------------------------------------------------------------
// (21) Client.FindByNameRequired throws ObjectNotFoundException when no
// match — message includes name.
// ---------------------------------------------------------------------
[Fact]
public void Client_FindByNameRequired_throws_with_name_when_not_found()
{
var db = new DbContext();
db.Attach(new Client { Name = "Acme Corp" });
var ex = Assert.Throws(() =>
Client.FindByNameRequired("Nobody", db));
Assert.Contains("Nobody", ex.Message);
Assert.Contains("no matching Client found", ex.Message);
}
// ---------------------------------------------------------------------
// (22) Client.FindByNameRequired uses Context singleton when no context arg.
// ---------------------------------------------------------------------
[Fact]
public void Client_FindByNameRequired_uses_Context_singleton()
{
var db = new DbContext();
var acme = new Client { Name = "SingletonCo" };
db.Attach(acme);
DbBase.Context = db;
try
{
var result = Client.FindByNameRequired("SingletonCo");
Assert.Same(acme, result);
}
finally
{
DbBase.Context = null;
}
}
// ---------------------------------------------------------------------
// (23) Client.FindByNameRequired throws with a useful message when the
// singleton is not configured (no arg, no singleton).
// Stays InvalidOperationException: configuration error, not a miss.
// ---------------------------------------------------------------------
[Fact]
public void Client_FindByNameRequired_throws_with_context_hint_when_singleton_null()
{
DbBase.Context = null;
var ex = Assert.Throws(() =>
Client.FindByNameRequired("SomeBody"));
Assert.Contains("no DbContext configured", ex.Message);
Assert.Contains("DbBase.Context", ex.Message);
}
}