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."); } }