Skip to content

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 as TConfig has zero or more than one public constructor. There's no "best match" heuristic here by design — reduce TConfig to exactly one public constructor.
  • '{TConfig}' is abstract and cannot be used as profile configuration... — TConfig is 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 one TConfig-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 single TConfig parameter.
  • '{TProfile}' is abstract and cannot be used as a profile... — same reasoning as TConfig'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 match TConfig'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 match TConfig'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 on MemberType (or the test class). Check the spelling and MemberType; an ambiguous inherited/hidden match reads as ...is ambiguous: more than one inherited or hidden static member matches... — set MemberType or rename the member.
  • D2 — not public static: ComposeData member '{M}' for '{T}' must be public and static. Make the field/property/method both public and static.
  • 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. A string is 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 awaited Task/ValueTask result) was null.

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.Current is ambient and available during profile construction and Configure too, so move the read into a Register factory 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, Configure runs once per attribute instance and reuses any captured value across its rows).
  • Fixture '...' for test class '...' has not finished initializing. — the fixture exists but its InitializeAsync hasn't completed. RequireFixture never 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 with IClassFixture<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-xUnit ITestContext implementation).

"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:

'Microsoft.Extensions.Logging.Logger<...>' is not a Compono.Logging capturing logger. ...

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:

builder.Logging.SetMinimumLevel(LogLevel.Trace);

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:

  • InCategory was given a prefix or short name. It is an exact match on the full category (normally the ILogger<T> type's full name), unlike AddFilter/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's UseSerilog() without writeToProviders: true — never calls the Compono provider.
  • The provider was removed after registration. Application code that calls ClearProviders() after the test's AddComponoCapture removes it. Register the collector after that code runs (for WebApplicationFactory, in ConfigureWebHost).

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 or Composer.Create call. (With UseGeneratedTestDoubles() and no UseMetrics(), composition fails earlier with a CompositionException naming IMeterFactory, because Compono.Metrics owns that type and no generated double exists for it.)
  • UseMetrics() is registered after UseNSubstitute() (or another catch-all stage-6 provider). An interface request resolves at whichever stage-6 provider is registered first, so NSubstituteProvider wins and you get a substitute. Call UseMetrics() first, especially in a shared base profile. See Registration order and misuse. Order against UseGeneratedTestDoubles() doesn't matter unless the project sets ComponoMetricsOwnsMeterFactory to false.
  • The Generic Host resolved its own default factory. The builder already registers a default IMeterFactory. Register the test's factory with builder.Services.AddSingleton<IMeterFactory>(meters); the container resolves a single IMeterFactory to its last registration, so the later one wins. Don't use TryAddSingleton — 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 every IDisposable test-method argument after the test case, including a [Shared] IMeterFactory parameter. using var _ = meters; is optional.
  • Compono.XunitV3.Aot: xUnit's AOT core doesn't dispose arguments. Dispose it in the test body with using var _ = meters;.
  • Compono.TUnit: TUnit disposes a composed root method argument itself, which is documented in Compono.TUnit — Disposal and ADR-0040. That's documented behavior, not something ADR-0072 Amendment 6 re-tested. The using is optional there, and keeping it is harmless.
  • Compono.NUnit, Compono.MSTest: not verified. Keep using var _ = meters;. It's harmless, because Dispose() is idempotent.
  • A factory you construct (Composer.Create or host tests): you own it, so use using var meters = new CapturingMeterFactory();.

See Disposal patterns. A factory that never created a meter costs nothing.

Next