Skip to content

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 subclassing AutoDataAttribute to 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 ICompositionValueProvider for 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 use AutoNSubstituteCustomization.
  • 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:

// Before
var order = fixture.Create<Order>();

// After
var order = composer.Create<Order>();

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:

// After
[Theory]
[Compose<ProjectTestProfile>]
public void Handles_Order(Order order) { }

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():

public void Configure(CompositionBuilder builder)
{
    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.Bogus where 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: