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
{
// -----------------------------------------------------------------
// Static singleton: an ActiveRecord-style shared context that any
// entity can reach without passing it through method parameters.
// -----------------------------------------------------------------
///
/// Shared (singleton) accessible from every
/// entity via its base type. Set once at application startup so that
/// entity methods can call DbBase.Context! instead of carrying
/// a context reference.
///
/// This is yet another leak: domain objects depend on the persistence
/// layer at the *type* level, not just the instance level.
///
public static DbContext? Context { get; set; }
///
/// 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; }
///
/// Generic ActiveRecord-style finder: walks the 's tracked entities, looks for the first whose runtime type matches
/// and satisfies
///.
///
/// This static generic rides the DbContext leak just like
/// , but works for any subtype without each entity needing its own hand-written finder.
///
/// Entity type to find (must derive from ).
///
/// Filter applied to candidates of type .
///
/// The context whose tracked entities to search.
/// When null, falls back to the singleton —
/// so a caller only passes a context explicitly when it must differ from
/// the ambient one.
///
/// The first matching entity, or null when no match.
///
///
/// Note the (deliberate, ActiveRecord-style) ambiguity this still leaves:
/// a null result means "no match — or no context configured at
/// all". The caller cannot distinguish the two; that is part of the cost
/// of the static-singleton leak this exercise illustrates.
///
public static T? Find(Predicate predicate, DbContext? db) where T : DbBase
{
db ??= Context;
if (db is null)
return default;
foreach (var e in db.Tracked)
if (e is T candidate && predicate(candidate))
return candidate;
return default;
}
///
/// Throwing variant of . Finds the first entity
/// whose runtime type matches and satisfies
/// . Throws
/// when no match is found, and when no
/// singleton is configured (a configuration error, not a lookup miss).
///
/// The exception message includes so
/// callers can debug which lookup failed.
///
/// Filter applied to candidates of type .
/// The context whose tracked entities to search. When null, falls back
/// to the singleton.
/// A human-readable description of the predicate, used in the
/// exception message when the search fails.
/// The first matching entity.
///
/// Thrown when no entity matches .
///
///
/// Thrown when no context is available (neither passed explicitly nor set as the singleton).
///
public static T FindRequired(Predicate predicate, DbContext? db, string predicateToString)
where T : DbBase
{
db ??= Context;
if (db is null)
throw new InvalidOperationException(
$"Cannot perform FindRequired<{typeof(T).Name}>: " +
$"no DbContext configured (neither passed explicitly nor set as " +
$"DbBase.Context singleton). Set DbBase.Context before calling " +
$"FindRequired<{typeof(T).Name}>.");
foreach (var e in db.Tracked)
if (e is T candidate && predicate(candidate))
return candidate;
throw new ObjectNotFoundException(
$"FindRequired<{typeof(T).Name}>({predicateToString}) — " +
$"no matching {typeof(T).Name} found in context.");
}
}