[PLAN-0006] Milestone 6: Bogus Integration¶
Status: Done
Implements: ADR-0026 (core capability: ICompositionContext.DeriveSeed()), ADR-0027 (Compono.Bogus package: BogusMemberNameProvider, BogusOptions, member-level UseBogus(faker => ...) sugar, whole-object UseBogus<T>(...) sugar, coexistence with Compono.NSubstitute), ADR-0028 (configurable conventions: BogusConvention, BogusOptions.AddAlias/AddConvention, scoped to a single UseBogus(...) call — a new ADR, not an amendment to ADR-0027)
Note: all three ADRs are Accepted. ADR-0026/ADR-0027 were accepted 2026-07-31; ADR-0028 (configurable conventions) was accepted 2026-08-01, after its own design review — see that ADR for the alternatives considered and rejected (notably: cross-call/cross-profile conflict detection, and the core build-finalization hook it would have needed, both explicitly deferred).
Goal¶
public sealed class ApplicationTestProfile : ICompositionProfile
{
public void Configure(CompositionBuilder builder) =>
builder
.UseNSubstitute()
.UseBogus();
}
[Theory]
[Compose<ApplicationTestProfile>]
public async Task Saves_order(
[Shared] IOrderRepository repository,
CreateOrderHandler handler,
CreateOrder command,
Customer customer)
{
// customer.FirstName/LastName/Email etc. are realistic, deterministic Bogus values
// repository is a real NSubstitute substitute, reused by handler's own constructor parameter
await handler.Handle(command);
await repository.Received(1).SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
}
runs end-to-end, packaged (not ProjectReference), proving: fixed pipeline precedence (semantic before test-double) needs no special ordering between UseBogus()/UseNSubstitute(); Bogus never claims an interface/delegate/ abstract-class request and NSubstitute never claims a semantic scalar/member request; an explicit registration or configuration rule wins over both; the same seed reproduces the same customer values across runs; a [Shared] substitute composed by NSubstitute coexists in the same graph as Bogus-supplied scalar values with no interaction between the two packages' code.
Scope¶
Per ADR-0026/ADR-0027's Decision Outcomes:
- New core
Componosurface:ICompositionContext.DeriveSeed(), backed by ADR-0012's existing path-hash mechanism with a distinct salt. - New
Compono.Boguspackage:BogusOptions,BogusMemberNameProvider,CompositionBuilderExtensions.UseBogus()/UseBogus(Action<BogusOptions>)/UseBogus<T>(Action<Faker<T>>)/UseBogus<T>(string, Action<Faker<T>>),MemberRuleExtensions.UseBogus(Func<Faker, TMember>, string)on the existing member-rule builder. - Configurable member-name conventions (ADR-0028):
BogusConvention(public enum),BogusOptions.AddAlias(string, BogusConvention)/AddConvention(string, Func<Faker, string>), the internalBogusConventionsshared built-in lookup, andBogusMemberNameProvider's constructor gaining a merged-conventions parameter. Scoped to a singleUseBogus(...)call — no cross-call/cross-profile conflict detection (see ADR-0028's Non-Goals). - New test project:
test/Compono.Bogus.Tests. - A
test/Compono.XunitV3.SampleTestsextension provingCompono.BogusandCompono.NSubstitutecompose in one real, packaged xUnit v3 consumer (this plan's own Goal scenario). - Doc updates:
docs/mvp.md,docs/architecture.md,docs/public-api.md.
Explicitly deferred/non-goals — see ADR-0027's own Decision Outcome/Negative Consequences:
.DependsOn(...)— a Compono-native member-dependency mechanism. Correlated values are satisfied via whole-objectFaker<T>(UseBogus<T>()) instead.- Cross-call/cross-profile alias/custom-convention conflict detection or merging, and the generic
CompositionBuilderbuild-finalization capability it would need — evaluated and explicitly deferred by ADR-0028; eachUseBogus(...)call's conventions are validated independently. - Any change to
Compono.NSubstitute— this plan touchesCompono.Bogusand core only; coexistence is verified, not implemented, on the NSubstitute side. - A
Compono.Benchmarksentry for a Bogus-composed graph — nice-to-have, not required for this plan's exit criteria (mirrors PLAN-0005's own deferral of the equivalent NSubstitute benchmark).
Phases¶
Each phase ships as its own PR, per design-decisions.md's phase rule.
Phase 0: Core DeriveSeed() capability (ADR-0026)¶
Status: Done
-
ICompositionContext.DeriveSeed(): derives anintfrom the context's root seed, the request path currently being resolved, and a fixed salt (RandomSource.DeriveSeedTag) distinct from ADR-0012's own internal per-PathSegment-kind fork tags — reusing the existing FNV-1a path-hash (IRandomSource.DeriveSeed()/RandomSource.DeriveSeed(), combining the node's already-forked_forkStatewith the new tag), not a new algorithm. The 64-bit result is folded into anintby XORing its two halves (raw ^ (raw >> 32)), not truncated to the low 32 bits alone.CompositionRowforwards to the wrappedCompositionContext, matching its existingResolve<T>()/ResolveCollectionSize()forwarding shape. - Callable exactly where the descriptor-less
Resolve<T>()already is: mid-TryProvide(viaInvokeProvider, ADR-0024 Amendment 1) and mid-factory (viaInvokeFactory, stage ¾ —.For<T>().Use(...)/.Member(...).Use(...)rules compile intoTypeRuleProvider/MemberRuleProvider, both of which invoke their factory through this exact sameInvokeFactorymethod, soRegister<T>coverage below exercises the identical code path). ThrowsInvalidOperationExceptionwhen called outside an active request (_manualResolveFrames.Count == 0), matchingResolve<T>()'s existing guard exactly. - Idempotent within one active request (repeated calls return the same value, since it's a pure read of the current node's already-forked state via
IRandomSource.DeriveSeed()); never advances or mutatesNextUInt64()'s own value-state stream. -
Compono.Testscoverage in isolation, beforeCompono.Bogusexists (testing.md's "verify a new public entry point independently" rule) —DeriveSeedTests.cs, 7 tests × 2 TFMs: same seed + same path → same derived value; sibling requests inside one factory → independent values; renaming a constructor parameter without reordering doesn't change its derived value, reordering does (ADR-0012's guarantee, re-verified for this new entry point); repeated calls within one active request are idempotent; calling outside an active request throws; a call from inside a public provider'sTryProvideis deterministic for the same seed; concurrentCreate<T>()calls against the same sharedComposerall land on the same, independently-verified-correct value (no shared mutable state bleeding between concurrent calls).
Phase 1: Compono.Bogus package (ADR-0027)¶
Status: Done
- New
src/Compono.Bogus/Compono.Bogus.csproj(matchingCompono.NSubstitute.csproj's TFM/packaging shape —ProjectReferencetoComponowithPrivateAssets="none",PackageReferencetoBogus(version35.6.5, added toDirectory.Packages.props, alongside aCompono.BogusVersion="1.0.0"local-feed entry for Phase 3's future packaged-consumer test)). -
BogusOptions:Locale(string, default"en"),EnableMemberNameConventions(bool, defaulttrue). -
BogusMemberNameProvider : ICompositionValueProvider: exact-match, case-sensitive lookup against the documented allowlist (FirstName,LastName,FullName,Email,PhoneNumber,StreetAddress,City,State,PostalCode,CompanyName), backed by aFrozenDictionary(an immutable, built-once lookup table, not a mutableDictionary— this is fixed, read-only library data), gated toRequestedType == typeof(string),NotHandledfor anything else (includingNameitself, deliberately absent from the allowlist). Constructs a freshFaker/Randomizerper handled request, seeded viacontext.DeriveSeed()— never a shared instance across requests. -
CompositionBuilderExtensions.UseBogus()/UseBogus(Action<BogusOptions>): registersBogusMemberNameProviderviaAddSemanticProviderwhenEnableMemberNameConventionsistrue. -
CompositionBuilderExtensions.UseBogus<T>(Action<Faker<T>> configureFaker) where T : class/UseBogus<T>(string locale, Action<Faker<T>> configureFaker) where T : class(theclassconstraint matchesFaker<T>'s own; the parameter is namedconfigureFaker, notconfigure, since it's now anAction, not aFunc).configureFakerisAction<Faker<T>>, notFunc<Faker<T>, Faker<T>>— it configures the instance in place (faker.RuleFor(...)as a statement, discarding the fluent return), rather than requiring the caller to return the same instance back. Compiles to purely ergonomic sugar over the existingRegister<T>registration mechanism — no hidden pipeline stage, no special runtime behavior of its own:builder.Register<T>(context => { var faker = new Faker<T>(locale).UseSeed(context.DeriveSeed()); configureFaker(faker); return faker.Generate(); });UseSeed(...)runs beforeconfigureFaker, not after — corrected by ADR-0027 Amendment 1 (caught by PR #33 review): aconfigureFakercallback that eagerly reads randomness at configuration time (an already-evaluatedRuleFor(x => x.Id, faker.Random.Guid()), not a lazyf => f.Random.Guid()factory) must still see this request's deterministic seed, not Bogus's own default unseededRandomizerstate — seeding first covers both that eager read and every lazyRuleForfactoryGenerate()evaluates afterward, sinceUseSeed(...)setsRandomimmediately and it persists across both calls. A freshFaker<T>is constructed inside the factory, once perTresolution — the factory closure itself is captured once atBuild()time (same as any otherRegister<T>factory), but noFaker<T>instance is ever retained or reused across requests, so concurrentCreate<T>()calls for the sameTnever share one. Caching a configuredFaker<T>across requests was considered and rejected — see ADR-0027's Model 3 section for why (Faker<T>carries mutable generation state with no documented concurrent-Generate()safety guarantee). Fully independent ofUseBogus()/BogusOptions.Locale— no ordering dependency, defaults to"en"on its own. -
MemberRuleExtensions.UseBogus(Func<Faker, TMember>, string locale = "en")on the existing.For<T>().Member(x => x.Y)builder (via a generic C# 14 extension block,extension<TParent, TMember>(CompositionMemberRuleBuilder<TParent, TMember> builder)): compiles to.Use(context => configure(new Faker(locale) { Random = new Randomizer(context.DeriveSeed()) })). Nocontext.Semanticaccessor, no core change beyond Phase 0'sDeriveSeed().
Phase 2: Configurable member-name conventions (ADR-0028)¶
Status: Done
Renumbered from the original 3-phase plan (Phase 2 "Test suites and verification" → Phase 3, Phase 3 "Docs and cleanup" → Phase 4) so implementation phases stay grouped together before the test-suite phase, per ADR-0028's own design review (added 2026-08-01, after Phase 0 shipped and Phase 1 was in review). This phase builds directly on Phase 1's BogusMemberNameProvider/BogusOptions, so it has to land after Phase 1 merges, before Phase 3's test suite (which should cover the complete Compono.Bogus.Tests surface — base package and configurable conventions together — in one coherent pass, per ADR-0028's Links section).
-
BogusConvention(public enum):FirstName,LastName,FullName,Email,PhoneNumber,StreetAddress,City,State,PostalCode,CompanyName— one member per existing built-in convention, no behavior beyond identity. -
BogusConventions(new internal static class): the shared built-in source of truthBogusMemberNameProvider's hardcodedConventionsdictionary (Phase 1) moves to —ByName/ByConvention, both typedIReadOnlyDictionary<...>(collision checks/default lookup, and alias-target resolution, respectively), backed byprivate static readonly FrozenDictionary<...>fields percoding-standards.md's collection-surface rule (applies tointernalmembers too, not justpublicones — the concreteFrozenDictionarytype never crosses even this in-assembly boundary), both derived from one underlying(name, convention, generate)tuple array so the ten generator delegates aren't duplicated. -
BogusOptions.AddAlias(string aliasName, BogusConvention target)/AddConvention(string memberName, Func<Faker, string> generate): eager validation performed byAddAlias/AddConventionagainstBogusConventions.ByNameplus this instance's own private accumulator —ArgumentNullException.ThrowIfNullfor a null name/generate(matching this repo's own established guard convention,coding-standards.md),ArgumentExceptionfor an empty/whitespace name or any duplicate or collision (naming the conflicting member name and whether it collided with a built-in or an already-configured entry),ArgumentOutOfRangeExceptionfor an undefinedBogusConventionvalue. Both returnvoid— matchingLocale/EnableMemberNameConventions's plain-property-setter shape, no fluent chaining. Both share one privateAddCorehelper for the validation/accumulation logic. -
BogusMemberNameProvidergains a second,internalconstructor overload —(string locale, IReadOnlyDictionary<string, Func<Faker, string>> conventions), called only byCompositionBuilderExtensions.UseBogus(...), freezing its own copy internally (conventions.ToFrozenDictionary()unless already one). Preserves the existingArgumentNullException.ThrowIfNull(locale)guard the real, already-merged Phase 1 public constructor has (src/Compono.Bogus/BogusMemberNameProvider.cs) — the public constructor now delegates to this one via: this(locale, BogusConventions.ByName), so the guard has to live in the shared internal constructor for both paths to keep it; also guardsconventionsitself. The existingpublic BogusMemberNameProvider(string locale)(Phase 1, already merged via#33) is untouched — not a breaking change, nobreakinglabel needed. Deliberatelyinternal, notpublic: a public overload would let a caller construct the provider with an arbitrary dictionary that omits or remaps a built-in name, silently supporting the replace/remove-a-built-in capability this ADR declares a Non-Goal and bypassingAddAlias/AddConvention's own eager validation entirely. -
CompositionBuilderExtensions.UseBogus(Action<BogusOptions> configure): afterconfigure(options)returns, mergesBogusConventions.ByNamewithoptions's own validated accumulator into oneFrozenDictionary<string, Func<Faker, string>>(no further validation needed —AddAlias/AddConventionalready guaranteed no collisions), then constructsBogusMemberNameProviderfrom that snapshot. Still gated entirely byEnableMemberNameConventions—falsemeans no provider is registered at all, aliases/custom conventions included (ADR-0028's explicit all-or-nothing scope; no partial mode in this version). - Explicitly not in this phase (ADR-0028 Non-Goals): cross-call/ cross-profile conflict detection or merging across separate
UseBogus(...)calls; anyCompositionBuildercore change; replacing or removing a built-in convention; non-stringcustom conventions; any fuzzy/pattern/priority matching.
Phase 3: Test suites and verification¶
Status: Done
-
test/Compono.Bogus.Tests:BogusMemberNameProviderunit coverage (each allowlisted name againststring, each allowlisted name against a non-stringtype declines,Nameitself declines, an unlisted name declines);UseBogus()/UseBogus(configure)wiring a working provider into a realComposer; member-levelUseBogus(faker => ...)sugar overriding the convention provider for the same member; whole-objectUseBogus<T>(...)producing a fully Bogus-generated instance, including a correlatedRuleFor((f, x) => ...)rule; duplicateUseBogus<T>()registration for the sameThits the existingCompositionConfigurationException; ADR-0027 Amendment 1 regression coverage — aUseBogus<T>()configureFakercallback that eagerly draws from the seededFaker<T>at configuration time (callingfaker.Generate()itself before returning, rather than a lazyf => f.Name.FirstName()RuleForfactory) still produces a deterministic result for the same Compono seed, provingUseSeed(...)is applied beforeconfigureFakerruns, not after. (Faker<T>exposes no publicRandomaccessor — confirmed by inspection, not assumed — so the ADR's own illustrativeRuleFor(x => x.Id, faker.Random.Guid())snippet doesn't literally compile against it;faker.Generate()is the real, compilable way to force an eager draw from that same seeded instance's internal state.) - Determinism regression coverage (ADR-0026's contract, exercised through real Bogus usage): same seed reproduces the same convention-provider value and the same
UseBogus<T>()-generated object; adding an unrelated Bogus-backed member elsewhere in the graph doesn't perturb an existing one;CreateMany<T>(n)produces independently-seeded items for aUseBogus<T>()-registered type, matching ADR-0012's existingCreateManyseed-derivation contract. - Coexistence tests against a real
Composerwith bothUseBogus()andUseNSubstitute()registered, any call order: a string member resolves via Bogus, an interface/delegate/abstract-class request resolves via NSubstitute, neither provider ever claims/handles the other's claimed shape - each is still attempted at its own pipeline stage and correctly declines (asserted via diagnostics trace, not just outcome); an explicitRegister<T>/.For<T>().Use(...)for a type/member either package could otherwise touch wins over both; a[Shared]NSubstitute substitute and Bogus-supplied scalar values coexist correctly in one row's scope. -
UseBogus<T>()lifetime/concurrency coverage, proving the corrected per-request-Faker<T>design (ADR-0027) actually holds: theconfigurecallback runs once per resolved object, not once at registration time (assert an invocation counter increments once perCreate<T>()call); two separateCreate<T>()calls receive two distinctFaker<T>instances (no instance identity/state leaks between requests); a parallelCreate<T>()/CreateMany<T>()run for aUseBogus<T>()- registered type produces correct, non-corrupted results with no shared mutableFaker<T>state observable across threads (a positive determinism-under-concurrency test, not a race characterization); the same seed and request path reproduce the same generated object across separate runs. - Configurable-convention coverage (ADR-0028):
AddAlias(...)resolves to the same value a direct call to the aliasedBogusConvention's own built-in generator would produce, for the same seed/path;AddConvention(...)produces the custom callback's value, seeded viacontext.DeriveSeed()exactly like the built-in/alias path; an alias or custom name colliding with a built-in name, an existing alias, or an existing custom convention throwsArgumentExceptionimmediately from theAddAlias/AddConventioncall that introduced it (not deferred toUseBogus(...)returning); a null name or a nullgeneratethrowsArgumentNullException, an empty/whitespace name throwsArgumentException, an undefinedBogusConventionvalue throwsArgumentOutOfRangeException;EnableMemberNameConventions = falsemeans aliases and custom conventions configured in the same call are never registered, not just the built-in conventions; the documented cross-call limitation — two separateUseBogus(...)calls each defining the same alias/custom name for different values compose via ordinary registration-order/first-match-wins pipeline semantics, asserted directly so the behavior is explicit rather than accidental (ADR-0028's Negative Consequences); exact, case-sensitive matching, explicitly exercised, not just assumed from the built-in allowlist's own existing coverage — a request forskudoes not match anAddConvention("Sku", ...)entry (and vice versa);AddAlias("givenname", ...)andAddAlias("GivenName", ...)in the same call are treated as two distinct names, not a collision; a name differing only by case from a built-in convention name (e.g.firstnamevs.FirstName) is not rejected as a collision and does not match the built-in generator — proving the merged lookup and its collision checks both use ordinal, case-sensitive comparison throughout, not a comparer that was accidentally left case-insensitive. - An API-surface/approval test locking
Compono.Bogus's public shape (now includingBogusConventionandBogusOptions.AddAlias/AddConvention), matchingCompono.NSubstitute.Tests'/Compono.XunitV3.Tests' existing pattern. - A real end-to-end run through
test/Compono.XunitV3.SampleTests(or a new sibling sample) proving this plan's own Goal scenario —UseBogus()andUseNSubstitute()composing one graph under a real xUnit v3 theory, packaged (notProjectReference) — matching PLAN-0004 Phase 3/PLAN-0005 Phase 2's real-packaged-consumer strategy, which has twice caught real packaging/compile-time bugs aProjectReference-only build couldn't surface.
Phase 4: Docs and cleanup¶
Status: Done
-
docs/mvp.mdMilestone 6 section: links ADR-0026/ADR-0027/ADR-0028/PLAN-0006, states implementation status per phase, matching Milestone 5's own phase-by-phase doc-update pattern (update in the PR that actually ships each phase, not deferred wholesale to this final phase). Verified complete — already carried per-phase during Phases 0-3. -
docs/architecture.md:ICompositionContext's conceptual sketch gainsDeriveSeed(); stage 5's Resolution Pipeline row and the stages-⅘/6/7 summary paragraph stop describing stage 5 as unconditionally empty;Compono.BogusPackage Boundaries entry gains a realOwnslist (includingBogusConvention/BogusConventions), Design line, and implementation status, matchingCompono.NSubstitute's entry shape; the Open Architectural Decisions "public provider extensibility" entry notes both stage 5 and stage 6 now have real registrants. Verified complete — already carried per-phase during Phases 0-2. -
docs/public-api.md: Bogus Integration section replaced with the real three-model design (convention provider, member-levelUseBogus(faker => ...), whole-objectUseBogus<T>(...)) — thecontext.Semantic.Email()sketch and the.DependsOn(...)sketch both removed/reframed per ADR-0027 — plus ADR-0028's configurable-conventions sketch (AddAlias/AddConvention) and its documented cross-call limitation; Naming Vocabulary gainsBogusMemberNameProvider/BogusOptions/BogusConventionif warranted; Diagnostics/Deterministic Reproduction sections cross-referenceDeriveSeed(). Verified complete — already carried per-phase during Phases 0-2. -
docs/adr/README.md/docs/plans/README.mdindex rows (already added during the design phase).
Critical Files¶
src/Compono/ICompositionContext.cs,src/Compono/CompositionContext.cs(DeriveSeed()public surface/implementation),src/Compono/IRandomSource.cs,src/Compono/RandomSource.cs(DeriveSeed()on the fork-state layer, the newDeriveSeedTag),src/Compono/CompositionRow.cs(forwarding implementation) — Phase 0.test/Compono.Tests/DeriveSeedTests.cs(new),test/Compono.Tests/UniqueValueResolverTests.cs(its hand-writtenICompositionContexttest fake updated for the new interface member) — Phase 0.src/Compono.Bogus/(new project) —BogusOptions.cs,BogusMemberNameProvider.cs,CompositionBuilderExtensions.cs,MemberRuleExtensions.cs— Phase 1.Directory.Packages.props(Bogus,Compono.BogusPackageVersionentries),Compono.slnx(new project entry) — Phase 1.src/Compono.Bogus/BogusConvention.cs(new),BogusConventions.cs(new, internal),BogusOptions.cs/BogusMemberNameProvider.cs/CompositionBuilderExtensions.cs(modified —AddAlias/AddConvention, the merged-conventions constructor parameter) — Phase 2.test/Compono.Bogus.Tests/(new project) — Phase 3.test/Compono.XunitV3.SampleTests/— new coexistence test(s) — Phase 3.docs/mvp.md,docs/architecture.md,docs/public-api.md— Phase 4.
Test Plan¶
Matches testing.md's existing conventions (xUnit v3 on MTP v2, Arrange-Act-Assert, fixed-seed determinism assertions, one test project per src project). Per testing.md's "verify a new public entry point in isolation before the package that will really use it exists" rule, DeriveSeed() gets its own Compono.Tests coverage (Phase 0) independent of Compono.Bogus, mirroring PLAN-0005 Phase 0's treatment of ICompositionValueProvider. Compono.Bogus.Tests (Phase 3) then covers the package's own real behavior — the base package (Phase 1) and configurable conventions (Phase 2) together, in one coherent pass — its coexistence with Compono.NSubstitute in the same Composer, UseBogus<T>()'s per-request Faker<T> lifetime under concurrent composition, and ADR-0028's own eager validation/collision contract, plus one real-runner proof (a packaged test/Compono.XunitV3.SampleTests run) since that specific shape has twice caught real bugs a ProjectReference-only build couldn't (PLAN-0004 Phase 3, PLAN-0005 Phase 2).
Open Items¶
- No
Compono.Benchmarksentry for a Bogus-composed graph is planned as part of this plan's own exit criteria — worth adding onceCompono.Bogusships, to characterizeFaker/Faker<T>generation cost againstdocs/performance.md's existing baselines, but not required to call this milestone done. .DependsOn(...)(ADR-0027's deferred member-dependency mechanism) is not designed here. Revisit only if Milestone 7 dogfooding surfaces a real needFaker<T>'s whole-object correlation doesn't already cover.- Cross-call/cross-profile alias/custom-convention conflict detection, and the generic
CompositionBuilderbuild-finalization capability it would need, are not designed here (ADR-0028 Non-Goals). Revisit only if a second, real integration-configuration need (beyond this one) justifies the cost of a genuine core capability with at least two real consumers.
Notes¶
Design addition (2026-08-01): ADR-0028 (configurable member-name conventions — BogusConvention, BogusOptions.AddAlias/ AddConvention) accepted after its own design review, proposed and confirmed after Phase 0 shipped and while Phase 1 was in review. A new ADR, not an amendment to ADR-0027 — ADR-0027's own accepted Decision Outcome is unchanged. Added as a new Phase 2, renumbering the original Phase 2 ("Test suites and verification") to Phase 3 and Phase 3 ("Docs and cleanup") to Phase 4, so implementation phases stay grouped before the single, comprehensive test-suite phase. The design review's most consequential moment was a considered-and-reversed decision: cross-call/cross-profile conflict detection was initially requested, investigated in depth (it would require either a new generic CompositionBuilder build-finalization capability in its own core-extension ADR, or a ConditionalWeakTable-keyed accumulator with weaker first-use-not-Build()-time validation timing), then explicitly declined once that cost was weighed against a single, milestone-scoped need — see ADR-0028's own Considered Options/Decision Outcome for the full account.
Phase 0 (Done):
- Implemented in the same branch/PR as the design docs (ADR-0026, ADR-0027, this plan), per explicit user direction — mirrors PLAN-0005 Phase 0's same choice. Phases 1-4 remain separate PRs, per
design-decisions.md's phase rule. - Implemented exactly per ADR-0026's Decision Outcome:
IRandomSource/RandomSourcegainedDeriveSeed()(a pure read of the node's own_forkStatecombined with a new, distinctDeriveSeedTag, via the sameFnv1a.CombineRandomSource.Forkalready uses — no new hashing algorithm);ICompositionContext/CompositionContextgained the publicint DeriveSeed(), guarded by the exact same_manualResolveFrames.Count == 0check the descriptor-lessResolve<T>()overload already uses, so both share one notion of "is a factory/provider invocation currently active."CompositionRowneeded a one-line forwarding addition to stay a completeICompositionContextimplementation. -
test/Compono.Tests/UniqueValueResolverTests.cs's hand-written `StubContext- ICompositionContext
test fake needed aDeriveSeed()member (throwingNotSupportedException, matching its existingResolve() /ResolveCollectionSize()stubs) to keep compiling against the now-larger interface — the only otherICompositionContextimplementation in the codebase besidesCompositionContext/CompositionRow` themselves.
DeriveSeedTests.cs(7 tests × 2 TFMs = 14) covers the full contract entirely through the publicComposer/Register<T>/AddTestDoubleProvidersurface — no new internal test seam was needed. The "callable from inside a factory" and "callable from inside a provider" cases are exercised viaRegister<T>and a hand-writtenICompositionValueProvider, respectively; a separate.For<T>().Use(...)test was judged unnecessary since that rule compiles intoTypeRuleProvider/MemberRuleProvider, both of which invoke their factory through the exact sameCompositionContext.InvokeFactorymethodRegister<T>already exercises — testing it a second time would cover the identical code path, not a new one.- Full suite green:
Compono.Tests426/426 (213 × 2 TFMs — 206 pre-existing + 7 new), whole-solutiondotnet build/dotnet test734/734, no warnings.
Phase 1 (Done):
- Implemented exactly per ADR-0027's Decision Outcome — no deviation from the ADR's own code sketches for
BogusOptions,BogusMemberNameProvider,CompositionBuilderExtensions, orMemberRuleExtensions. Compono.Bogus.csprojmirrorsCompono.NSubstitute.csproj's shape:net10.0;net11.0TFMs,ProjectReferencetoComponowithPrivateAssets="none"(PLAN-0004 Phase 3's packaging lesson, applied proactively),PackageReferencetoBogus(version centrally managed,35.6.5— the latest stable release at the time of this phase), andInternalsVisibleTofor the not-yet-createdCompono.Bogus.Tests(now Phase 3, after ADR-0028's Phase 2 insertion — see the design-addition note above).Directory.Packages.propsalso gained aCompono.BogusVersion="1.0.0"local-feed entry, matchingCompono.XunitV3/Compono.NSubstitute's existing pattern, ahead of that phase's own packaged-consumer test needing it.MemberRuleExtensions.UseBogus(...)is a generic C# 14 extension block (extension<TParent, TMember>(CompositionMemberRuleBuilder<TParent, TMember> builder) where TMember : notnull) — the first generic extension block in this codebase;CompositionBuilderExtensions' ownUseBogus<T>(...)overloads are ordinary generic methods inside a non-genericextension(CompositionBuilder builder)block, which is a different (already-established) shape.- XML-doc
<see cref="...">pointing at a sibling method inside the sameextension(...)block doesn't resolve (CS1574) — the compiler can't look up another extension member by simple name from inside its own block yet. Fixed by followingCompono.NSubstitute's own existing precedent exactly: a plain<c>UseBogus()</c>-style code-formatted reference instead of<see cref>for that one cross-reference case, not a suppression. - Added to
Compono.slnx. Whole-solutiondotnet buildgreen, 0 warnings. - No test project yet (now Phase 3, after renumbering) —
BogusOptions/BogusMemberNameProvider/UseBogus()/UseBogus<T>()/the member-ruleUseBogus(...)sugar are implemented but only build-verified in this phase, not test-verified — matching PLAN-0005 Phase 1's own explicit precedent for the identical package-skeleton-then-tests split. - PR #33 review (Codex, one P2 finding) caught a real determinism defect in
UseBogus<T>()'s own implementation, fixed before merge — see ADR-0027 Amendment 1 for the full account.configureFaker(faker)ran beforefaker.UseSeed(context.DeriveSeed()), so aconfigureFakercallback that eagerly reads randomness at configuration time (rather than through a lazyRuleForfactory) drew from Bogus's own default, unseededRandomizerstate instead of this request's deterministic seed. Fixed by constructingFaker<T>and applyingUseSeed(...)in the same statement, beforeconfigureFakerruns.BogusMemberNameProvider/the member-ruleUseBogus(...)sugar (Models ½) were already correct — both applyRandomvia an object initializer before calling into user code, so this defect was scoped to Model 3 only. Regression coverage added to this plan's own Phase 3 task list above (not written yet — Phase 1 stays build-verified only, per this phase's own scope). - A second stale-doc finding (
docs/mvp.md's Milestone 6 Exit Criteria still said "implementation has not started" after this phase's own earlier fix already saidCompono.Boguswas implemented) — same doc-staleness pattern PLAN-0005's review rounds caught repeatedly; fixed in the same PR.
Phase 2 (Done):
- Implemented exactly per ADR-0028's final Decision Outcome — the version that survived six rounds of design-PR review (
#34), not the original sketch. Concretely:BogusMemberNameProvider's public one-arg constructor is untouched and delegates to a newinternaltwo-arg overload;BogusConventions.ByName/ByConventionareIReadOnlyDictionary-typed properties backed byprivateFrozenDictionaryfields, not internalFrozenDictionaryfields directly;AddAlias/AddConventionthrowArgumentNullExceptionfor a null name (neverArgumentException); the internal constructor re-guardslocale/conventionssince the public one now delegates through it. BogusConventions'ByNameCore/ByConventionCoreare both derived from one canonical(string Name, BogusConvention Convention, Func<Faker, string> Generate)[]array viaToFrozenDictionary— the ADR's own sketch showed two independently-written dictionaries; consolidated to one array so the ten generator lambdas exist exactly once, matching the ADR's own stated intent ("both derived from one underlying set") rather than its literal code sample.BogusOptions.AddAlias/AddConventionshare one privateAddCore(string name, string paramName, Func<Faker, string> generate)helper for the null/whitespace/collision validation and accumulation — the two public methods differ only in how they obtaingenerate(a direct parameter forAddConvention,BogusConventions.ByConvention[target]forAddAlias, after checkingEnum.IsDefined(target)).CompositionBuilderExtensions.UseBogus(configure)merges via a plainDictionary<string, Func<Faker, string>>seeded fromBogusConventions.ByName, then overwritten byoptions.CustomConventions(safe —AddAlias/AddConventionalready guarantee no key inCustomConventionscollides with a built-in name), frozen once via.ToFrozenDictionary()before constructingBogusMemberNameProvider.- Hit the same
CS1574(<see cref>to a sibling extension member doesn't resolve) Phase 1 already found — this time onBogusConventions' own doc comment referencingCompositionBuilderExtensions.UseBogus(). Same fix:<c>UseBogus()</c>instead of<see cref>. - No test project yet (Phase 3) — build-verified only: whole-solution
dotnet build/dotnet test734/734 (unchanged from Phase 1, since this phase adds no tests), 0 warnings. Matches Phase 1's own precedent. - Human-in-the-loop implementation, per explicit user direction: presented the concrete file-by-file plan (new files, exact modifications) before writing any code, rather than proceeding straight to a PR/review loop as the previous three phases did.
Phase 3 (Done):
test/Compono.Bogus.Tests(new project, mirroringCompono.NSubstitute.Tests' shape):BogusMemberNameProviderTests.cs,CompositionBuilderExtensionsTests.cs,MemberRuleExtensionsTests.cs,DeterminismTests.cs,CoexistenceTests.cs,UseBogusOfTLifetimeTests.cs,BogusConventionConfigurationTests.cs,PublicApiSurfaceTests.cs— 60 tests × 2 TFMs = 120. Added toCompono.slnx.- Since
Compono.Bogus.Testsdoesn't reference the source generator (testing.md's established "unit tests use hand-written fakes / manual descriptors, not the real generator" pattern), every member-name-convention and coexistence test resolves throughComposer.CreateRow(...).Resolve<TValue>(descriptor)with an explicitCompositionRequestDescriptor, neverComposer.Create<T>()against an arbitrary hand-shaped class —UseBogus<T>()tests are the one exception, since that registration compiles to an exactRegister<T>factory (stage 3), which needs no generated plan at all. - "Declines" coverage uses an equivalence technique, not pattern-matching on Bogus's own string output:
BogusMemberNameProvider.TryProvidenever touchescontext.DeriveSeed()/constructs aFakerbefore deciding to decline (confirmed by reading its own source), so composing the same descriptor with and without the provider registered, under the same fixed seed, produces byte-identical fallback values whenever the provider declines, and a different value whenever it handles the request — this proves "declines"/"handles" without hardcoding what Bogus's own generated string looks like (locale/version-fragile) or relying onPrimitiveValueProvider's internal alphabet. - ADR-0027 Amendment 1 regression coverage found the ADR's own illustrative snippet doesn't compile:
RuleFor(x => x.Id, faker.Random.Guid())impliesFaker<T>exposes a public.Random— reflecting over the realBogus35.6.5 assembly (Faker<T>fields/properties/interfaces) confirms it does not; only the member-rule sugar's plain, non-genericFakerdoes. The regression test instead hasconfigureFakercallfaker.Generate()itself before returning (beforeRuleFor-drivenGenerate()normally would), which genuinely draws from the sameUseSeed(...)-applied internal state eagerly — the real, compilable equivalent of the ADR's illustrative bug class. Not a plan deviation in substance, just in the literal repro mechanism; noted here rather than editing the ADR (which stays as originally written, perdesign-decisions.md). - Coexistence tests use the diagnostic-trace assertion this phase's own task wording called for (
ProviderAttempt/CompositionDiagnostic.Trace) to proveBogusMemberNameProvideris tried-and-declines for a non-stringshape rather than never invoked at all — the pipeline always tries every registered semantic provider in order, so "disjoint claims" means "always declines the other's shape," not "never attempted." test/Compono.XunitV3.SampleTests: newBogusTests.cs(CustomerwithFirstName/LastName/Email,BogusTestProfilecallingUseBogus().UseNSubstitute()— the opposite call order fromNSubstituteTestProfile'sUseNSubstitute()-only config, proving order doesn't matter) exercises this plan's own Goal scenario through the real packagedCompono.Bogus/Compono.NSubstitute→Componodependency chain.Compono.Bogus.csprojadded to thePackageReference/PackToLocalFeedwiring (.csproj,pack-to-local-feed.sh, now packing 4 projects instead of 3). Verified with a realdotnet test -f net10.0/-f net11.0run against this project directly (it's intentionally not inCompono.slnx, matching PLAN-0004/PLAN-0005's own precedent): 8/9 theories pass, the 9th beingFailingCompositionTests' pre-existing, deliberate failure (unrelated to this phase, used by a separate real-runner proof elsewhere).- Whole-solution
dotnet build/dotnet test: 854/854 (734 pre-Phase-3 + 120 new fromCompono.Bogus.Tests), 0 warnings, both TFMs.
Phase 4 (docs/cleanup) hasn't started yet. ADR-0026/ADR-0027 reached Accepted on 2026-07-31, after a design review that resolved (in order): how Bogus's randomness should relate to ADR-0012's path-independence guarantee (a new, narrow, on-demand DeriveSeed() capability, not an eager field and not exposing IRandomSource); how explicit member rules should access that determinism (sugar over the existing stage-4 .Use(context => ...) mechanism, replacing docs/public-api.md's stale context.Semantic sketch); whether the built-in convention provider is on by default (yes, matching Compono.NSubstitute's own precedent); whether correlated values need a new Compono mechanism (no — Faker<T> already solves it, .DependsOn(...) is explicitly deferred); whether whole-object Faker<T> generation needs a first-class API (yes, but implemented as purely ergonomic sugar over the existing Register<T> registration mechanism, not a new pipeline concept or special runtime behavior); and whether that whole-object API should share package-wide locale state with UseBogus() (no — kept fully independent, no hidden call-order coupling).