// Exercise 2: detect captive dependencies (medium) // // The container below works, but lets a singleton capture a scoped service. Add validation like // ASP.NET Core's ValidateScopes: // - While a SINGLETON is being created, resolving a SCOPED service must throw InvalidOperationException // with a message containing "captive". // - Track this with a counter of singletons currently being constructed (_buildingSingleton) in the scope. // - Singleton → Singleton and Singleton → Transient remain allowed; Scoped → Singleton is allowed. // // Run with: dotnet run (inside a console project). using System; using System.Collections.Generic; enum Lifetime { Singleton, Scoped, Transient } class Container { public readonly Dictionary factory)> Registrations = new(); public readonly Dictionary Singletons = new(); public void Register(Lifetime life, Func factory) where T : class => Registrations[typeof(T)] = (life, s => factory(s)); public Scope CreateScope() => new Scope(this); } class Scope { readonly Container _c; readonly Dictionary _scoped = new(); int _buildingSingleton; public Scope(Container c) { _c = c; } public T Resolve() where T : class { var reg = _c.Registrations[typeof(T)]; if (reg.life == Lifetime.Singleton) { if (!_c.Singletons.TryGetValue(typeof(T), out var s)) { s = reg.factory(this); // TODO: track that a singleton is being built _c.Singletons[typeof(T)] = s; } return (T)s; } if (reg.life == Lifetime.Scoped) { // TODO: throw if a singleton is being built if (!_scoped.TryGetValue(typeof(T), out var sc)) { sc = reg.factory(this); _scoped[typeof(T)] = sc; } return (T)sc; } return (T)reg.factory(this); } } class Db { } class Clock { } class Helper { } class Cache { public Db Db; public Cache(Db db) { Db = db; } } class Repo { public Clock Clock; public Repo(Clock c) { Clock = c; } } class Stats { public Helper H; public Clock C; public Stats(Helper h, Clock c) { H = h; C = c; } } class Program { static void Main() { int pass = 0, total = 0; void Check(string name, bool ok) { total++; if (ok) pass++; Console.WriteLine($"{(ok ? "PASS" : "FAIL")}: {name}"); } var c = new Container(); c.Register(Lifetime.Scoped, s => new Db()); c.Register(Lifetime.Singleton, s => new Clock()); c.Register(Lifetime.Transient, s => new Helper()); c.Register(Lifetime.Singleton, s => new Cache(s.Resolve())); c.Register(Lifetime.Scoped, s => new Repo(s.Resolve())); c.Register(Lifetime.Singleton, s => new Stats(s.Resolve(), s.Resolve())); var scope = c.CreateScope(); string msg = null; try { scope.Resolve(); } catch (InvalidOperationException e) { msg = e.Message; } Check("singleton → scoped throws", msg != null && msg.Contains("captive")); Check("scoped → singleton allowed", scope.Resolve().Clock != null); Check("singleton → transient/singleton allowed", scope.Resolve().C != null); Check("scoped still resolvable afterwards", scope.Resolve() != null); Console.WriteLine($"--- {pass}/{total} checks passed ---"); } }