Migrating from AutoFixture to Compono¶
This guide is based on migrating a real, multi-project .NET test suite from AutoFixture, AutoFixture.Xunit3, and AutoFixture.AutoNSubstitute to Compono. The before/after examples throughout are drawn from real patterns encountered during that migration, not synthetic ones — see Real-World Migration Evidence at the end for the full evidence record, if you want it. You don't need to read that record to complete your own migration; this guide is self-contained.
Who this guide is for¶
You have an existing AutoFixture-based test suite — [AutoData]/ [InlineAutoData], one or more ICustomization/ISpecimenBuilder implementations, possibly AutoNSubstituteCustomization — and you want to move it to Compono. This guide assumes you're already comfortable with AutoFixture and want the fastest path to idiomatic Compono, not an introduction to Compono from scratch (start with Getting Started for that).
Migration mindset¶
The goal isn't a mechanical, one-for-one API translation. AutoFixture is specimen-oriented — a pipeline of request-matching builders that can intercept and reshape almost any request. Compono is composition-oriented — a fixed, ordered pipeline of exact registrations, type/member rules, and providers, with generated construction underneath. Some AutoFixture infrastructure has no Compono counterpart because it solved a problem Compono's design doesn't have in the first place. Let obsolete fixture infrastructure disappear rather than recreating it under a new name.
A few principles to migrate by:
- Prefer profiles over custom data-attribute subclasses. A profile (
ICompositionProfile) replaces the pattern of subclassingAutoDataAttributeto bake in a fixture factory. - Prefer exact registrations for exact-type creation. If an AutoFixture customization only ever built one specific type, it's a
Register<T>call, not a general-purpose builder. - Prefer member/type rules for scoped customization. If a customization only overrode one member of one type, reach for
.For<T>().Member(...)instead of a full specimen builder. - Use providers only for genuinely pattern-based behavior. Reserve a custom
ICompositionValueProviderfor the rare case that really needs to match on request shape, not a fixed type — most AutoFixture specimen builders don't actually need this. - Use
[Shared]only when object identity actually matters. Don't reach for it just because AutoFixture's[Frozen]was on the parameter before — audit whether the test actually depends on the same instance. - Don't recreate hidden AutoFixture behavior unless the test genuinely needs it.
ConfigureMembers-style auto-configuration and recursion-omission are common examples — see below. - Prefer explicit substitute setup over recursive member auto-configuration. A test that depends on a substitute's return value should stub it, not rely on an implicit default.
Install Compono¶
See Package Guides for the full ecosystem map. Most AutoFixture users migrating xUnit tests need:
Compono— the core package, always required.Compono.XunitV3— if you use[AutoData]/[InlineAutoData]today.Compono.NSubstitute— if you useAutoNSubstituteCustomization.Compono.Bogus— if you want realistic fake data instead of anonymous values.
dotnet add package Compono --prerelease
dotnet add package Compono.XunitV3 --prerelease
dotnet add package Compono.NSubstitute --prerelease
dotnet add package Compono.Bogus --prerelease
Install matching versions of every Compono package you add — mixing versions across packages isn't supported; see Package Guides: Version Compatibility. See Installation for the full setup, including why --prerelease is required during public preview.
Quick concept map¶
An orientation aid, not a claim that the two frameworks are identical — each row is expanded into its own section below.
| AutoFixture usage | Compono approach |
|---|---|
fixture.Create<T>() | composer.Create<T>() |
[AutoData] | [Compose] |
Custom AutoDataAttribute subclass | [Compose<TProfile>] |
ICustomization | ICompositionProfile |
| Exact-type specimen customization | Register<T>() |
Exact-type ISpecimenBuilder | Register<T>() |
| Type/member customization | .For<T>() / .Member(...) |
| Pattern-based specimen builder | ICompositionValueProvider |
[Frozen] | [Shared], when identity is genuinely required |
AutoNSubstituteCustomization | UseNSubstitute() |
| Semantic/realistic data | UseBogus() / UseBogus<T>() |
OmitOnRecursionBehavior | No equivalent — Compono fails clearly instead |
Migrate object creation¶
The baseline call maps directly:
composer comes from Composer.Create(builder => ...), built once and reused — see The Composition Model if you haven't read it yet.
Migrate [AutoData] and [InlineAutoData]¶
A project-specific AutoDataAttribute subclass that bakes in an IFixture factory:
// Before
public sealed class ProjectAutoDataAttribute() : AutoDataAttribute(CreateFixture)
{
internal static IFixture CreateFixture() =>
new Fixture().Customize(new ProjectCustomization());
}
[Theory]
[ProjectAutoData]
public void Handles_Order(Order order) { }
becomes [Compose<TProfile>] applied directly — no wrapper attribute subclass, since the profile does what the custom subclass used to do implicitly:
See Migrate ICustomization below for what ProjectTestProfile looks like.
A row where every value is supplied inline needs no Compono attribute at all. If a [Theory] row supplies every parameter inline, plain xUnit [InlineData] already works and is simpler than routing it through a Compose-family attribute:
// Before
[Theory]
[InlineProjectAutoData(null!, "")]
[InlineProjectAutoData("Kaladin", "kaladin")]
public void Normalizes(string? input, string expected) { }
// After — no Compono attribute needed
[Theory]
[InlineData(null, "")]
[InlineData("Kaladin", "kaladin")]
public void Normalizes(string? input, string expected) { }
[Compose] is method-scoped, not parameter-scoped — a row with nothing left to compose doesn't need it. For a row that mixes inline values with composed ones, [Compose(...)] binds the inline values positionally and composes the rest — see How Do I Write a Composed Theory?.
Only one Compose-family attribute per test method is supported. AutoFixture's idiom of stacking multiple [InlineAutoData(...)] instances for several rows, each with its own composed parameters, has no direct Compono equivalent — pick one Compose-family attribute per method and cover the rest with a separate [Theory]/[InlineData] method instead. See Compono.XunitV3's Package Guide for the full mechanics of why stacking isn't supported.
Migrate ICustomization¶
// Before
public sealed class ProjectCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new OrderSpecimenBuilder());
}
}
// After
public sealed class ProjectTestProfile : ICompositionProfile
{
public void Configure(CompositionBuilder builder)
{
builder.Register(CreateOrder);
}
private static Order CreateOrder(ICompositionContext context) =>
new(context.Resolve<string>());
}
Simpler: an empty or commented-out ICustomization with no real customization logic doesn't need porting at all — delete it. A customization that did real work becomes a profile of equivalent Register<T>/rule calls, as above. A profile shared across projects can be composed into another via builder.AddProfile<TProfile>() — see Profiles for when a separate, composed-in profile is worth it versus configuring everything in one place.
Migrate [Frozen] and shared dependencies¶
By default, Compono composes each parameter independently — two parameters of the same type get two separate instances, same as unfrozen AutoFixture. [Shared] is the direct equivalent of [Frozen], but audit each real [Frozen] usage rather than translating it mechanically: many turn out not to need it at all.
Equivalent — real sharing. Where a dependency composed as a test parameter is also depended on by another composed parameter, and the test needs to assert against or configure that exact instance, [Shared] preserves the behavior directly:
// Before
public async Task Repository_UsesTheConfiguredClient(
[Frozen] IHttpClient client,
OrderRepository sut) { }
// After
public async Task Repository_UsesTheConfiguredClient(
[Shared] IHttpClient client,
OrderRepository sut) { }
Simpler — [Frozen] wasn't sharing anything. A very common pattern is [Frozen] used purely to obtain a substitute for an interface, with the substitute never reused elsewhere in the same test. This needs no annotation under Compono at all — composing an interface parameter already produces a substitute automatically once UseNSubstitute() is active:
// Before
public async Task Handle_WhenInvalid_DoesNotCallRepository(
[Frozen] IOrderRepository repository) { }
// After — [Frozen] wasn't sharing anything
public async Task Handle_WhenInvalid_DoesNotCallRepository(
IOrderRepository repository) { }
Auditing every [Frozen] usage this way — rather than converting each one to [Shared] by rote — is usually the single biggest simplification a migration finds. See Shared Values for the full model.
Migrate AutoNSubstitute¶
AutoNSubstituteCustomization becomes builder.UseNSubstitute():
Tradeoff: AutoNSubstituteCustomization { ConfigureMembers = true } auto-configures every generated substitute's members with sensible return values, including recursively-constructed objects for Task<T>-returning members. Compono.NSubstitute has no equivalent — every substitute is a bare Substitute.For<T>(). Most call sites that never depended on a specific return value need no change at all. But watch for a test that never stubs a substitute's member yet asserts the code under test doesn't throw — it may have been passing only because auto-configuration supplied a non-null value. Under Compono's bare substitute, the same unstubbed call returns NSubstitute's own default (null/default, or Task.FromResult<T>(default) for an async member); if your code dereferences that result, you'll see a NullReferenceException where the test previously passed silently:
client.SendAsync(Arg.Any<HttpRequestMessage>())
.Returns(new HttpResponseMessage(HttpStatusCode.OK));
This is a real, one-time migration cost for a suite that leaned on auto-configuration — but it also makes a previously-hidden dependency visible in the test body, which is the point: Compono favors explicit setup over implicit magic throughout. See Compono.NSubstitute's Package Guide for the full rationale.
Migrate specimen builders¶
ISpecimenBuilder served several distinct purposes in AutoFixture, and doesn't map to one single Compono extension point — which one you need depends on what the original builder actually did:
| What the specimen builder does | Compono mechanism |
|---|---|
| Creates one exact type | Register<T>() |
| Overrides one type or member | .For<T>() / .Member(...) |
| Matches open-ended request shapes | ICompositionValueProvider |
| Creates a complete, realistic object | UseBogus<T>() |
Most real specimen builders fall into the first case — dispatching on a fixed Type/ParameterInfo/NamedRequest to build one specific type:
// Before
public sealed class OrderSpecimenBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
return request switch
{
Type t when t == typeof(Order) => CreateOrder(context),
ParameterInfo p when p.ParameterType == typeof(Order) => CreateOrder(context),
NamedRequest { InnerRequest: Type t } nr when t == typeof(Order) => CreateOrder(context),
_ => new NoSpecimen(),
};
}
private static Order CreateOrder(ISpecimenContext context) =>
new(context.Create<string>());
}
// After — no request-shape pattern-matching needed
builder.Register<Order>(context => new Order(context.Resolve<string>()));
Compono's registration is keyed by exact type, so there's no Type/ParameterInfo/NamedRequest matching to write by hand for this case. Reach for a custom ICompositionValueProvider only for the rarer case that genuinely needs to match on request shape rather than a fixed type — see Providers.
Handle recursion behavior¶
Intentional difference: AutoFixture's default ThrowingRecursionBehavior can be swapped for OmitOnRecursionBehavior, which silently omits a member that would cause infinite recursion instead of throwing. Compono has no equivalent to opt into — a genuine construction cycle always fails fast with a path-annotated CompositionException (ADR-0011), the same way any other unsatisfiable composition does. If your object graph is genuinely self-referencing, break the cycle explicitly with a Register<T> factory that supplies the recursive member directly, rather than relying on generated default construction. See Troubleshooting: Common Errors if you hit this during migration.
Add realistic data with Bogus¶
Where AutoFixture only produces anonymous specimens, Compono.Bogus's UseBogus<T>(Action<Faker<T>>) builds a Faker<T> already seeded from the current composition's own deterministic seed before invoking your configuration callback — every RuleFor inside it is automatically seed-consistent with the rest of the composition, with no manual seeding required:
public void Configure(CompositionBuilder builder)
{
builder.UseBogus<Customer>(ConfigureCustomer);
}
private static void ConfigureCustomer(Faker<Customer> faker)
{
faker.RuleFor(c => c.FullName, f => f.Name.FullName());
faker.RuleFor(c => c.Email, f => f.Internet.Email());
}
For members that follow a common naming convention (FirstName, Email, PhoneNumber, and similar), plain UseBogus() matches them automatically with no per-type configuration — see Compono.Bogus's Package Guide for the full built-in list and its member-name-matching limits.
Concepts that disappear entirely¶
Removed entirely — no replacement concept exists:
| AutoFixture concept | Why nothing replaced it |
|---|---|
IFixture | Composition is per-test-method via [Compose<TProfile>] — there's no fixture object, configured or otherwise |
IRequestSpecification/NamedRequest | Registration is keyed by exact type; no separate request-matching type is needed |
AutoNSubstituteCustomization's member auto-configuration | Compono never auto-configures a substitute's members |
OmitOnRecursionBehavior | A construction cycle always fails fast |
Replaced one-for-one with a Compono equivalent:
| AutoFixture concept | Compono equivalent |
|---|---|
ICustomization | ICompositionProfile |
ISpecimenBuilder (exact-type case) | CompositionBuilder.Register<T>(...) |
Custom AutoDataAttribute/InlineAutoDataAttribute subclasses | [Compose]/[Compose<TProfile>], applied directly |
AutoNSubstituteCustomization's substitute creation itself | builder.UseNSubstitute() |
Freeze<T>()/[Frozen] | [Shared] |
Known differences and limitations¶
Composing an external or BCL type with ambiguous constructors fails. Compono's constructor-selection validation runs entirely at compile time, from a type's constructor count — it has no visibility into a runtime registration that might construct the type, so a type like HttpClient (three accessible constructors) always fails with CMP0001, even with an explicit registration for it. For an external or BCL type with ambiguous constructors, compose an application-owned abstraction or provider instead of the concrete type directly:
public interface IHttpClientProvider
{
HttpClient Create();
}
internal sealed class HttpClientProvider(HttpMessageHandler handler) : IHttpClientProvider
{
public HttpClient Create() => new(handler) { BaseAddress = new Uri("https://localhost/") };
}
public sealed class ClientTestProfile : ICompositionProfile
{
public void Configure(CompositionBuilder builder)
{
builder.Register<HttpMessageHandler>(_ => Substitute.For<HttpMessageHandler>());
builder.Register<IHttpClientProvider>(context => new HttpClientProvider(context.Resolve<HttpMessageHandler>()));
}
}
[Theory]
[Compose<ClientTestProfile>]
public async Task UsesTheConfiguredResponse(
[Shared] HttpMessageHandler handler,
IHttpClientProvider clientProvider)
{
// configure `handler` to return the response you want, then:
var client = clientProvider.Create();
}
An interface is always resolved by a provider, never by constructor selection, so it never reaches ambiguous-constructor validation. See Reference: Diagnostics for the full cause/fix detail.
For the rest of Compono's 0.x known limitations (Compose-family stacking, Bogus's member-name-matching limits, and more), see each Package Guide's own "What it deliberately doesn't do" section, aggregated in Troubleshooting.
Migration checklist¶
- Remove AutoFixture package references.
- Add the required Compono packages at matching versions.
- Replace custom AutoData attributes with
[Compose]or[Compose<TProfile>]. - Convert real customizations into profiles.
- Delete empty or obsolete fixture abstractions.
- Audit every
[Frozen]usage to determine whether identity is actually required before converting it to[Shared]. - Add
UseNSubstitute()to profiles that need substitutes. - Add explicit stubs where tests relied on
ConfigureMembers. - Convert exact-type specimen builders to registrations.
- Use rules or providers only where their matching behavior is actually needed.
- Remove recursion-behavior configuration and run the suite.
- Introduce
Compono.Boguswhere semantic data improves readability. - Run the complete test suite and inspect failures for hidden dependencies (most commonly,
ConfigureMembers-shaped ones). - Remove unused fixture infrastructure after migration.
Real-world migration evidence¶
This guide's patterns were validated against a real migration of a multi-project .NET test suite — not invented to illustrate a point in the abstract. If you want the full evidence record — post-migration metrics, every finding's classification, and the design decisions it fed into — it's available, but not required reading to complete your own migration:
- Research: AutoFixture vs. Compono Dogfooding — the complete evidence dossier.
- ADR-0002, ADR-0011, ADR-0018, ADR-0025, ADR-0026 — the design decisions this guide's equivalents are drawn from.
- Samples — complete, runnable projects.
- Troubleshooting — if something in your own migrated tests doesn't behave the way you expect.