Common Errors¶
By diagnostic code (compile-time)¶
Every CMP0001–CMP0013 code is a compile-time error from Compono.Generators — see Reference: Diagnostics for the full message/cause/fix for each code. The one you'll hit most often in practice:
CMP0001(ambiguous construction path) — the type you're composing has more than one accessible constructor. Most common real-world trigger: composing a BCL type you don't own directly (e.g.HttpClient, which has 3 accessible constructors). Fix: compose an interface/wrapper around it instead — see Migrating from AutoFixture for a real worked example of exactly this.
CMP0020–CMP0032 are a separate, informational family — they only apply if ComponoGeneratedTestDoubles=true is set (see Compono.TestDoubles) and never fail the build. Most of them mean one interface leaf's generated double couldn't be emitted and fell back to the ordinary runtime-provider path — but CMP0022, CMP0029, and CMP0030 are narrower: they withhold a Configure()/Verify() surface for just one overload or colliding identity while the rest of the double still generates normally. See Reference: Diagnostics for each code.
A missing provider for an interface, abstract class, or delegate is not a CMP code at all — it always surfaces as a runtime CompositionException instead, covered next.
By symptom (runtime)¶
Runtime composition failures¶
This is a runtime failure — no compiled code was rejected, but nothing in the resolution pipeline could satisfy a request at composition time. Unlike the CMP codes above, there's no diagnostic code here — only a path-annotated message and a reproducible seed, via exception.Diagnostic (a CompositionDiagnostic):
Unable to compose CreateOrderHandler.
CreateOrderHandler
└── IOrderProcessor processor
└── OrderValidator validator
└── IRuleProvider rules
No registration, semantic provider, test-double provider, built-in
provider, or generated plan could satisfy IRuleProvider.
Seed: 8451203967726193045
Read the tree from the root down — it shows exactly which nested dependency failed, not just the top-level type you asked for.
Reproducing the failure depends on where the seed came from. An xUnit theory row's seed (from a [Compose]/[Compose<TProfile>] row) is always in the pasteable int range — copy it straight into [Compose(Seed = ...)] to get the same row again. A plain, unseeded Composer.Create().Create<T>() call outside a test framework generates a full 64-bit seed on its own, which won't fit [Compose(Seed = ...)]/builder.WithSeed(int)'s int parameter — for a programmatic composition you want to reproduce later, call builder.WithSeed(...) with an int you choose yourself up front, rather than trying to replay a printed value after the fact. See Determinism and Seeding.
The most common cause is a missing provider for an interface, abstract class, or delegate — this is always a runtime failure, never a CMP code (interfaces/abstract classes/delegates are always provider-resolved, not routed through constructor selection), and can also be discovered at a call site the generator couldn't statically prove would fail (e.g. a registration that only conditionally applies). Add the missing registration, or install Compono.NSubstitute and call UseNSubstitute() if you want an automatic substitute. Compono.Bogus doesn't supply interface providers — it only matches string-typed members by name, so it won't resolve this.
A genuine construction cycle always fails this way too, immediately — Compono has no OmitOnRecursionBehavior-style opt-out; a self-referencing object graph fails fast with the same path-annotated message rather than silently omitting the cycling member. See Migrating from AutoFixture if you're coming from AutoFixture's recursion-behavior configuration — there's nothing to configure here, by design.
The type looks composable, but the generated plan is unexpectedly missing¶
If composition looks valid — the type has an accessible constructor, no CMP diagnostic fired, nothing about the code changed — but you still hit a runtime "no generated plan"-style failure (the tree message above naming "generated plan" among the stages that couldn't satisfy the request), the generator may not have run at all for this compilation, rather than having run and correctly declined. This is a secondary symptom, not the root cause — check your build output for an analyzer warning before assuming the composition itself is wrong.
The .NET SDK's Roslyn compiler host refuses to load an analyzer (Compono.Generators.dll) built against a newer compiler than the host's own — silently, as a build warning (CS9057), never a build error, so a generator that failed to load produces no obvious signal unless you go looking at the build's warning output. docs/getting-started/installation.md's "Minimum .NET SDK version" section and ADR-0003 Amendment 1 record Compono's own actual minimum (.NET SDK 8.0.400 or later for a net8.0 build — net9.0/net10.0/net11.0 have no equivalent floor, every released SDK for those TFMs already bundles a new-enough Roslyn) — an older host SDK than that is a real, sourced, non-hypothetical way for this to happen. It is not, however, the only conceivable cause of a generator failing to run — an unusual or non-standard build toolchain could, in principle, produce the same symptom for a different reason.
Don't over-generalize this: a "no generated plan" failure is far more often a genuine, correctly-reported composition gap (a type the generator correctly never intended to compose, or a real missing registration) than a generator-didn't-run incident — most runtime composition failures are exactly what the message says, not this. Treat this as one specific thing to rule out, not the default explanation: if the failing type looks like it should have a generated plan and nothing else in this page's "Runtime composition failures" section explains it, check your build output for CS9057 (or any other analyzer-loading warning) before looking further at the composition itself.
"My test throws CompositionConfigurationException"¶
This is a configuration error, thrown when Composer.Create(...) returns — not a composition failure for a specific value. It means your configuration callback itself is contradictory: two Register<T> calls for the same type, two WithSeed calls, or a similar conflict. Every conflict found is collected and reported together, not just the first one. Fix: remove the duplicate/contradictory configuration call — there is no last-write-wins fallback to rely on instead.
"ComposeAttribute<TProfile, TConfig> throws before my test even runs"¶
Five distinct, deterministic, pre-composition failures — all plain-message CompositionExceptions, computed once per attribute instance and cached, never re-checked per theory row (see Compono.XunitV3's Package Guide and ADR-0036 for the full design). Every one of these ends with the same "\n\nSeed: {value}" convention every other Compono.XunitV3-owned pre-composition failure uses — even though a constructor-shape/argument mismatch fails identically regardless of seed, the seed printed is either the one you configured via Seed = ... or a freshly generated one, for consistency with every other failure category:
'{TConfig}' must have exactly one public constructor...— the config type you passed asTConfighas zero or more than one public constructor. There's no "best match" heuristic here by design — reduceTConfigto exactly one public constructor.'{TConfig}' is abstract and cannot be used as profile configuration...—TConfigis an abstract class. Even if it has exactly one public constructor (abstract types can declare one; only a derived type can actually call it), it can't be instantiated directly — use a concrete type.'{TProfile}' must have exactly one public constructor accepting a single '{TConfig}' parameter...— the profile type has no constructor accepting exactly oneTConfig-typed parameter (or has more than one, which normal C# overload resolution can't actually produce for an identical single-parameter shape, so this case is effectively unreachable in practice). Add a public constructor to your profile that takes a singleTConfigparameter.'{TProfile}' is abstract and cannot be used as a profile...— same reasoning asTConfig's abstract-rejection case above, applied to the profile type.'{TConfig}' requires {N} profile configuration argument(s), but {M} were supplied./ a null-for-non-nullable or type-mismatch message — the attribute's own constructor arguments don't matchTConfig's constructor positionally. Unlike this attribute family's ordinary inline values (which may supply fewer than the test method has parameters, leaving the rest composed), profile configuration arguments must matchTConfig's constructor exactly — there's no "leave the rest to composition" fallback for a config type's own constructor parameters.
Note the compile-time-vs-runtime tradeoff this form makes deliberately: [Compose<TProfile>] (no TConfig) rejects an invalid profile type at compile time, via its TProfile : ICompositionProfile, new() constraint — [Compose<TProfile, TConfig>] can't offer that, since "has a constructor accepting exactly this type" isn't expressible as a C# generic constraint. All three failures above are deterministic runtime checks instead, computed once and cached, but not compile errors.
"My [ComposeData] theory can't find or read its rows"¶
Two failure classes, both CompositionExceptions: the member-level D1–D5 (found while reading the row source) and the row-level R0 wrapper (one row failed to build). Under Compono.XunitV3 these surface at test-execution time; under Compono.XunitV3.Aot the same member/shape conditions are compile-time CMP diagnostics instead (see the AOT package guide). The full contract is in Composed theory rows ([ComposeData]).
Member-level failures (each with no seed line):
- D1 — member not found:
ComposeData member '{M}' for '{T}' was not found. Expected a public static field, property, or parameterless method.No matching public static member onMemberType(or the test class). Check the spelling andMemberType; an ambiguous inherited/hidden match reads as...is ambiguous: more than one inherited or hidden static member matches...— setMemberTypeor rename the member. - D2 — not public static:
ComposeData member '{M}' for '{T}' must be public and static. Make the field/property/method bothpublicandstatic. - D3 — requires arguments:
ComposeData member '{M}' for '{T}' requires arguments, which ComposeData does not support. The member matched is a method taking parameters; ComposeData supplies none, so use a parameterless method, field, or property. - D4 — unsupported return:
ComposeData member '{M}' for '{T}' returned unsupported type '{type}'. Expected IEnumerable, Task/ValueTask of IEnumerable, or IAsyncEnumerable of ITheoryDataRow, object?[], or ITuple rows.Astringis not a collection, and a single row is not itself a theory — return a collection of rows. - D5 — null result:
ComposeData member '{M}' for '{T}' returned null.The member (or its awaitedTask/ValueTaskresult) wasnull.
A row-level failure is wrapped as ComposeData row {i}[ [label]] from '{M}' for '{T}' failed: {cause} — i is the zero-based row index. The {cause} covers a null row, an unsupported row shape, too many values, a null value for a non-nullable parameter, a value that can't be assigned to its parameter, a missing/malformed TConfig in element 0 of the arity-two form, or a profile/Configure that threw. Row values are validated before anything is composed, so a binding mistake is reported as an exact row error, not a downstream null.
The seed line follows the effective-seed rule (attribute Seed, else a profile WithSeed, else a fresh per-row seed; setting both is a configuration error): an R0 failure before the row's CreateRow — row shape, TConfig selection, profile construction, Configure, or the composer build — prints Seed: {n} when the attribute's Seed was set, otherwise Seed: not assigned (the failure occurred before the row's composition started); from CreateRow on (including row-value validation and too many values) it prints Seed: {row.Seed}. Paste that value into the attribute's Seed to reproduce the row.
Profile-construction failure (ADR-0070 Amendment 6). When the arity-one or arity-two form's profile constructor itself throws, the R0 cause is the constructor's own message and the original exception is kept as InnerException — Compono no longer leaks System.Reflection.TargetInvocationException. This fix lives in AddProfile<TProfile>() and applies to every framework's [Compose<TProfile>] and [ComposeData<TProfile>], so those failures read identically.
"A composed value doesn't look realistic" (looks like an anonymous string)¶
Compono.Bogus isn't installed, or UseBogus() wasn't called, or the member's name isn't on the built-in convention list — see Compono.Bogus's Package Guide for the exact list of matched names and how to extend it.
"My substitute doesn't return what I expected"¶
Compono.NSubstitute never auto-configures a substitute's members — this is deliberate, not a bug. See Compono.NSubstitute's Package Guide for why, and how to stub the member explicitly.
"RequireFixture<TFixture>() throws"¶
TestContext.Current.RequireFixture<TFixture>() (see Compono.XunitV3's Package Guide) throws a plain InvalidOperationException — never a CompositionException and never a CMP code — for four distinct situations:
RequireFixture<...>() requires a running xUnit test class, but none is active (pipeline stage: ...)— the message fires only when there is genuinely no active test class: you called it outside a running xUnit test (for example from a[Fact]-less helper, a static initializer, or after the test finished).TestContext.Currentis ambient and available during profile construction andConfiguretoo, so move the read into aRegisterfactory not because construction lacks a context, but for lifecycle correctness: the factory runs per row and reads the fixture for the test actually executing, whereas a profile instance can outlive a single row (under JIT,Configureruns once per attribute instance and reuses any captured value across its rows).Fixture '...' for test class '...' has not finished initializing.— the fixture exists but itsInitializeAsynchasn't completed.RequireFixturenever waits for it. Declare the fixture concretely so xUnit initializes it before the test runs; don't read a lazily- or partially-initialized fixture.Test class '...' has no fixture of exact type '...'.— the class has no fixture of that concrete type. Fixture lookup matches the exact type only: a base type or interface request never matches a concrete fixture. Declare it withIClassFixture<T>,ICollectionFixture<T>on the test's collection definition, or[assembly: AssemblyFixture(typeof(T))], and request the same concrete type.Fixture lookup for '...' in test class '...' returned an instance of '...', which is not a '...'.— the lookup returned a value of the wrong type (only possible with a non-xUnitITestContextimplementation).
"Verify() on a host ILogger<T> throws"¶
A logger resolved from a host's IServiceProvider is the host's own ILogger<T> implementation, not a Compono capturing logger. Verify() and the other LoggerTestingExtensions members require a logger produced by UseLogging() or a directly constructed CapturingLogger/ CapturingLogger<T>, so they throw:
Host-wide capture is a separate destination from per-logger capture in v1: register a LogCollector on the host's logging pipeline with AddComponoCapture(), then call Verify()/GetCapturedEntries() on the collector instead of on the host logger. See Host-wide capture and How Do I Assert Nothing Logged at Error or Above Across a Generic Host?.
"Debug entries are missing from LogCollector"¶
The collector does not override the host's filter rules. A Generic Host's default minimum level is Information, so Debug and Trace entries are filtered out before they reach any provider, including Compono's. Lower the host minimum when the test needs them:
The same happens when a configured category rule or AddFilter excludes the category. To target only the Compono provider, use its Compono alias, for example Logging:Compono:LogLevel:Default — this works on every supported target framework.
"Never() passes but the host clearly logged an error"¶
Never() passes whenever no captured entry matches, including when the filter matches nothing or the collector captured nothing at all:
InCategorywas given a prefix or short name. It is an exact match on the full category (normally theILogger<T>type's full name), unlikeAddFilter/Logging:LogLevel:*rules, which match by prefix. Use the full name, or.Matching(entry => entry.Category.StartsWith("MyApp.Payments.", StringComparison.Ordinal)).- The host replaced
ILoggerFactory. A logging library that swaps in its own factory and ignores registered providers — for example Serilog'sUseSerilog()withoutwriteToProviders: true— never calls the Compono provider. - The provider was removed after registration. Application code that calls
ClearProviders()after the test'sAddComponoCaptureremoves it. Register the collector after that code runs (forWebApplicationFactory, inConfigureWebHost).
Check that the collector captured something a host of this kind logs. On a Generic Host with its default lifetime, logs.Verify().InCategory("Microsoft.Hosting.Lifetime").AtLeast(1) after StartAsync confirms capture. Under a WebApplicationFactory/ASP.NET Core TestHost, assert a deterministic entry your own code emits during the operation under test instead — TestHost runs a different host lifetime, and a shared fixture may clear startup entries between tests.
"Two composed parameters of the same type aren't the same instance"¶
That's the default — composition is independent per parameter unless you opt in with [Shared]. See Shared Values.
"Verify metrics on a non-Compono IMeterFactory throws"¶
Every method on MeterFactoryTestingExtensions (GetMeasurements<T>(), Verify<T>(), RecordObservableInstruments(), ClearMeasurements()) requires the IMeterFactory to be Compono's CapturingMeterFactory, and throws InvalidOperationException otherwise. This usually means one of:
UseMetrics()isn't registered. Add it to the profile orComposer.Createcall. (WithUseGeneratedTestDoubles()and noUseMetrics(), composition fails earlier with aCompositionExceptionnamingIMeterFactory, becauseCompono.Metricsowns that type and no generated double exists for it.)UseMetrics()is registered afterUseNSubstitute()(or another catch-all stage-6 provider). An interface request resolves at whichever stage-6 provider is registered first, soNSubstituteProviderwins and you get a substitute. CallUseMetrics()first, especially in a shared base profile. See Registration order and misuse. Order againstUseGeneratedTestDoubles()doesn't matter unless the project setsComponoMetricsOwnsMeterFactorytofalse.- The Generic Host resolved its own default factory. The builder already registers a default
IMeterFactory. Register the test's factory withbuilder.Services.AddSingleton<IMeterFactory>(meters); the container resolves a singleIMeterFactoryto its last registration, so the later one wins. Don't useTryAddSingleton— it keeps the first registration and would discard yours. See Verify Metrics Across a Host.
"Verify<long> throws instrument "x" records int, not long"¶
The instrument records a different numeric type than the one you asked for. Pass the real type to GetMeasurements<T>/Verify<T>, or read the actual type from CapturedMeasurement<T>.Instrument. There is no numeric coercion — int and long are distinct instruments.
"Verify throws about one selection merging measurements of different types"¶
Two instruments share a name but record different value types — either two instruments in one meter (CreateCounter<int>("x") and CreateCounter<long>("x") are distinct), or same-named meters with different versions or tags. A single selection can't merge different types. Rename one instrument in the SUT, or narrow with the two-argument overload when the conflicting instruments live in differently named meters — see selection rule 5 in the Compono.Metrics Package Guide.
"Verify<long> throws about meter ambiguity"¶
The one-argument overload matched instruments from more than one meter name. Use the two-argument overload (GetMeasurements<T>(meterName, instrumentName) or Verify<T>(meterName, instrumentName)) to pick the meter.
"GetMeasurements<Guid> throws ArgumentException"¶
Only the seven BCL measurement types are supported: byte, short, int, long, float, double, decimal. No instrument can record any other type, so the mistake is caught deterministically.
"Never() passes but the counter was clearly incremented"¶
Never() passes whenever no captured measurement matches, including when the instrument name matched nothing at all. A failing Once()/Exactly(n) lists the instruments that do exist, so check that message for a typo or an unexpected meter name.
"An undisposed factory leaks for the process lifetime"¶
A factory that created a meter stays referenced by the BCL's static meter list until it's disposed, keeping its meters, listener, and captured measurements alive — the same cost as an undisposed DefaultMeterFactory. Compono never disposes it. Who does depends on the test engine (ADR-0072 Amendment 6):
Compono.XunitV3(xUnit v3, JIT): xUnit disposes everyIDisposabletest-method argument after the test case, including a[Shared] IMeterFactoryparameter.using var _ = meters;is optional.Compono.XunitV3.Aot: xUnit's AOT core doesn't dispose arguments. Dispose it in the test body withusing var _ = meters;.Compono.TUnit: TUnit disposes a composed root method argument itself, which is documented inCompono.TUnit— Disposal and ADR-0040. That's documented behavior, not something ADR-0072 Amendment 6 re-tested. Theusingis optional there, and keeping it is harmless.Compono.NUnit,Compono.MSTest: not verified. Keepusing var _ = meters;. It's harmless, becauseDispose()is idempotent.- A factory you construct (
Composer.Createor host tests): you own it, so useusing var meters = new CapturingMeterFactory();.
See Disposal patterns. A factory that never created a meter costs nothing.
Next¶
- FAQ — design-decision questions that aren't a specific error.
- Reference: Diagnostics — full detail for every
CMPcode.