diff --git a/Directory.Packages.props b/Directory.Packages.props
index 70e8c08de..b234a984b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -100,13 +100,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index a3d96ee6c..c1b040093 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -339,7 +339,9 @@ const config: UserConfig = {
{text: 'Multi-Tenancy', link: '/guide/durability/efcore/multi-tenancy'},
{text: 'Domain Events', link: '/guide/durability/efcore/domain-events'},
{text: 'Database Migrations', link: '/guide/durability/efcore/migrations'},
- {text: 'Query Plans', link: '/guide/durability/efcore/query-plans'}
+ {text: 'Query Plans', link: '/guide/durability/efcore/query-plans'},
+ {text: 'Batch Queries', link: '/guide/durability/efcore/batch-queries'},
+ {text: 'Initial Data', link: '/guide/durability/efcore/initial-data'}
]},
{text: 'Managing Message Storage', link: '/guide/durability/managing'},
diff --git a/docs/guide/durability/efcore/batch-queries.md b/docs/guide/durability/efcore/batch-queries.md
new file mode 100644
index 000000000..ffe164147
--- /dev/null
+++ b/docs/guide/durability/efcore/batch-queries.md
@@ -0,0 +1,160 @@
+# Batch Queries / Futures
+
+Wolverine's EF Core integration can collapse multiple related `SELECT`s inside one handler into a single database round-trip, using [Weasel's `BatchedQuery`](https://weasel.jasperfx.net/efcore/batch-queries.html) (the EF Core counterpart to Marten's [batched query](https://martendb.io/documents/querying/batched-queries.html)) as the underlying mechanism.
+
+This page is Wolverine-focused: handler patterns, auto-batching, what the code generator does for you. For the underlying `BatchedQuery` fluent API, see [Weasel's batch-queries guide](https://weasel.jasperfx.net/efcore/batch-queries.html).
+
+## Why batch?
+
+Every extra `SELECT` in a handler is another round-trip to the database. Four queries against a local SQL Server, same handler, four small rows:
+
+| Strategy | Total (50 iterations) | Per handler | Relative |
+|---|---|---|---|
+| Sequential `await`s | 345.8 ms | **6.92 ms** | 1.0× |
+| Batched via `BatchedQuery` | 124.3 ms | **2.49 ms** | **2.78×** |
+
+Measured locally against `mcr.microsoft.com/mssql/server`, 4 keyed lookups per iteration, after warm-up. The speedup scales with (a) number of queries in the handler and (b) network latency — across a region-to-region hop, a four-query handler can drop from ~40 ms to ~12 ms.
+
+## Pattern 1 — Two `IQueryPlan`s on one handler
+
+The simplest win. If your handler [uses query plans](./query-plans) that implement *both* `IQueryPlan` **and** `IBatchQueryPlan` (which `QueryPlan` and `QueryListPlan` do automatically), Wolverine's code generator detects multiple batch-capable loads on the same handler and rewrites them into a shared `BatchedQuery`:
+
+```csharp
+public class ShipmentOverview
+{
+ public Customer Customer { get; set; } = null!;
+ public IReadOnlyList RecentOrders { get; set; } = [];
+}
+
+public record GetShipmentOverview(Guid CustomerId);
+
+public static class ShipmentOverviewHandler
+{
+ public static async Task Handle(
+ GetShipmentOverview query,
+ CustomerById customerSpec, // IQueryPlan + IBatchQueryPlan via QueryPlan<>
+ RecentOrdersFor ordersSpec) // " " QueryListPlan<>
+ {
+ return new ShipmentOverview
+ {
+ Customer = await customerSpec.FetchAsync(...),
+ RecentOrders = await ordersSpec.FetchAsync(...)
+ };
+ }
+}
+```
+
+Wolverine's `EFCoreBatchingPolicy` inspects the handler chain, detects the two batch-capable plans, and generates code equivalent to:
+
+```csharp
+// Generated (simplified)
+var batch = db.CreateBatchQuery();
+var customerTask = customerSpec.FetchAsync(batch, db);
+var ordersTask = ordersSpec.FetchAsync(batch, db);
+await batch.ExecuteAsync(cancellation);
+var customer = await customerTask;
+var orders = await ordersTask;
+```
+
+One round-trip, no manual batch wiring, no change to the plan classes.
+
+## Pattern 2 — Manual `IBatchQueryPlan`
+
+When a query doesn't fit the `QueryPlan<>` / `QueryListPlan<>` shape — for example, a projection into a DTO or a pre-aggregated count — implement `IBatchQueryPlan` directly:
+
+```csharp
+public class OrderCountFor(Guid customerId) : IBatchQueryPlan
+{
+ public Task FetchAsync(BatchedQuery batch, OrderDbContext db)
+ => batch.QueryCount(db.Orders.Where(x => x.CustomerId == customerId));
+}
+```
+
+Any handler parameter implementing `IBatchQueryPlan` gets the same auto-batching treatment as `IQueryPlan`-derived plans. Use `IBatchQueryPlan` when you need full control over the batched shape; use `QueryPlan<>` / `QueryListPlan<>` when you want the plan to work both standalone and batched.
+
+## Pattern 3 — `[Entity]` primary-key lookup + a spec in the same handler
+
+`[Entity]` lookups and query plans batch together. A common shape — load the root aggregate by id, then fetch a related collection with a spec:
+
+```csharp
+public record ArchiveOrderLines(Guid OrderId);
+
+public static class ArchiveOrderLinesHandler
+{
+ public static async Task Handle(
+ ArchiveOrderLines cmd,
+ [Entity] Order order, // PK lookup — batch-capable
+ ActiveLinesFor linesSpec, // QueryListPlan — batch-capable
+ OrderDbContext db)
+ {
+ order.Archive();
+ foreach (var line in await linesSpec.FetchAsync(...))
+ {
+ line.Archive();
+ }
+ await db.SaveChangesAsync();
+ }
+}
+```
+
+Both the `[Entity]` load and the spec fetch are enlisted into a single `BatchedQuery`, one round-trip for two reads.
+
+## Pattern 4 — Count + page in one round-trip
+
+Paginated list responses almost always want `(totalCount, currentPage)`. Two queries, same filter, one logical operation. Keep it one round-trip:
+
+```csharp
+public record OrderSearch(string? Customer, int Page, int PageSize);
+
+public class OrderSearchCount(string? customer) : IBatchQueryPlan
+{
+ public Task FetchAsync(BatchedQuery batch, OrderDbContext db)
+ => batch.QueryCount(OrdersMatching(db, customer));
+}
+
+public class OrderSearchPage(string? customer, int page, int pageSize)
+ : IBatchQueryPlan>
+{
+ public async Task> FetchAsync(BatchedQuery batch, OrderDbContext db)
+ {
+ var list = await batch.Query(
+ OrdersMatching(db, customer)
+ .OrderByDescending(x => x.PlacedAt)
+ .Skip((page - 1) * pageSize)
+ .Take(pageSize));
+ return list.ToList();
+ }
+}
+
+static IQueryable OrdersMatching(OrderDbContext db, string? customer)
+ => customer is null
+ ? db.Orders
+ : db.Orders.Where(x => x.Customer.Name.Contains(customer));
+
+public static class OrderSearchHandler
+{
+ public static async Task<(int Total, IReadOnlyList Page)> Handle(
+ OrderSearch query,
+ OrderSearchCount countPlan,
+ OrderSearchPage pagePlan)
+ {
+ return (await countPlan.FetchAsync(...), await pagePlan.FetchAsync(...));
+ }
+}
+```
+
+One `SELECT COUNT(*)` + one paginated `SELECT`, merged into a single `DbBatch` on the wire.
+
+## Opting out
+
+If a specific handler genuinely needs sequential round-trips (e.g. because a later query depends on an earlier query's result), just don't use batch-capable plans — the batching policy only fires when two or more `IBatchQueryPlan` parameters appear on the same handler.
+
+## Testing batched handlers
+
+The batching is transparent to tests. The same handler that uses `IQueryPlan` parameters can be unit-tested against an in-memory `DbContext` the normal way — query plans execute standalone through `FetchAsync(DbContext)` when invoked outside a `BatchedQuery`. See [Query Plans → Testing a plan in isolation](./query-plans#testing-a-plan-in-isolation).
+
+## Further reading
+
+- [Weasel — `BatchedQuery` fluent API reference](https://weasel.jasperfx.net/efcore/batch-queries.html)
+- [Query Plans](./query-plans) — the Specification model that backs the auto-batching
+- [Marten — Batched Queries](https://martendb.io/documents/querying/batched-queries.html) — the Critter Stack sibling this mirrors
diff --git a/docs/guide/durability/efcore/index.md b/docs/guide/durability/efcore/index.md
index 9ee4b5ef2..bc8cdd529 100644
--- a/docs/guide/durability/efcore/index.md
+++ b/docs/guide/durability/efcore/index.md
@@ -88,3 +88,78 @@ builder.UseWolverine(opts =>
Right now, we've tested Wolverine with EF Core using both [SQL Server](/guide/durability/sqlserver) and [PostgreSQL](/guide/durability/postgresql) persistence.
+
+## Development-time usage
+
+Wolverine + EF Core is designed to keep the dev loop short: fast schema iteration, cheap per-test database resets, declarative seed data. The three pillars below all work together — and all come for free the moment you call `UseEntityFrameworkCoreTransactions()`.
+
+### Weasel-managed schema migrations
+
+`UseEntityFrameworkCoreWolverineManagedMigrations()` hands schema management to [Weasel](https://weasel.jasperfx.net/efcore/migrations.html) rather than EF Core's migration-chain tooling. The shape of the story:
+
+| | EF Core migrations | Weasel migrations |
+|---|---|---|
+| Model | Ordered chain of up/down scripts checked in alongside code | Diff the live database against the current `DbContext` model at startup |
+| Authoring | Generate + edit migration classes | Nothing — just change your model |
+| Iteration cost | Slow (script regeneration, merge conflicts on parallel branches) | None — restart the app |
+| Best for | Production deployments with a change audit | Local dev, integration tests, short-lived branches |
+
+Register it on `WolverineOptions`:
+
+```csharp
+builder.UseWolverine(opts =>
+{
+ opts.Services.AddDbContextWithWolverineIntegration(
+ x => x.UseSqlServer(connectionString));
+
+ // Diff the DbContext against the live DB at startup and apply missing DDL.
+ opts.UseEntityFrameworkCoreWolverineManagedMigrations();
+});
+```
+
+The [Weasel docs](https://weasel.jasperfx.net/efcore/migrations.html) go deeper on the diff engine, opt-outs, and how it handles schemas.
+
+### IInitialData — declarative seed data
+
+Implement `Weasel.EntityFrameworkCore.IInitialData` (or register a lambda with `services.AddInitialData(...)`) to declare data that should be present every time the database is reset:
+
+```csharp
+public class SeedItems : IInitialData
+{
+ public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
+ {
+ context.Items.Add(new Item { Name = "Seed" });
+ await context.SaveChangesAsync(cancellation);
+ }
+}
+
+builder.Services.AddInitialData();
+```
+
+Multiple seeders run in registration order. See the [dedicated page on initial data](./initial-data) for patterns around layered seeders, lambda-based registration, and multi-tenant seeding.
+
+### Resetting data between tests
+
+Two knobs, finest-grained first:
+
+**Per-DbContext — `host.ResetAllDataAsync()`**
+
+Wipes one `DbContext`'s tables in FK-safe order and reruns that context's `IInitialData` seeders. This is the right default for most integration tests:
+
+```csharp
+[Fact]
+public async Task ordering_flow()
+{
+ await _host.ResetAllDataAsync();
+
+ // arrange ... act ... assert
+}
+```
+
+The underlying `DatabaseCleaner` is registered automatically by `UseEntityFrameworkCoreTransactions()` — no `services.AddDatabaseCleaner()` needed.
+
+**Global — `host.ResetResourceState()`**
+
+Resets every `IStatefulResource` registered with the host — Wolverine's message store, every broker, every `DbContext` cleaner. Bigger hammer; right when a test writes to multiple stores or you've seen cross-test contamination you can't isolate.
+
+**Recommendation:** use the finest-grained mechanism the test actually needs. Resetting the world on every test multiplies your suite runtime for no benefit.
diff --git a/docs/guide/durability/efcore/initial-data.md b/docs/guide/durability/efcore/initial-data.md
new file mode 100644
index 000000000..75f7ee242
--- /dev/null
+++ b/docs/guide/durability/efcore/initial-data.md
@@ -0,0 +1,96 @@
+# Initial Data
+
+`Weasel.EntityFrameworkCore.IInitialData` is the declarative seed-data hook that runs every time a `DbContext` is reset via `DatabaseCleaner.ResetAllDataAsync()` or Wolverine's [`host.ResetAllDataAsync()`](./index#resetting-data-between-tests). It's the recommended way to keep integration tests and local dev loops seeded with a known baseline without hand-rolling setup code per test.
+
+See the [Weasel docs on `IInitialData`](https://weasel.jasperfx.net/efcore/database-cleaner.html#iinitialdata) for the authoritative reference — this page is a Wolverine-focused overview of how to use it well.
+
+## Class-based seeders
+
+The traditional form. Implement `Populate`, register with `services.AddInitialData()`:
+
+```csharp
+public class SeedCoreItems : IInitialData
+{
+ public static readonly Item[] Items =
+ [
+ new Item { Name = "Alpha" },
+ new Item { Name = "Beta" }
+ ];
+
+ public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
+ {
+ context.Items.AddRange(Items);
+ await context.SaveChangesAsync(cancellation);
+ }
+}
+
+// Registration
+builder.Services.AddInitialData();
+```
+
+## Lambda seeders
+
+For small amounts of seed data, authoring a class is overkill. Weasel 8.14+ exposes a lambda overload:
+
+```csharp
+builder.Services.AddInitialData(async (ctx, ct) =>
+{
+ ctx.Items.Add(new Item { Name = "Gamma" });
+ await ctx.SaveChangesAsync(ct);
+});
+```
+
+Class-based and lambda seeders can be freely mixed in the same application; they're all resolved as `IEnumerable>` at reset time and run in registration order.
+
+## Layered seeders
+
+Because every registered `IInitialData` runs on every reset, you can compose seed data by registering multiple seeders rather than stuffing everything into one class:
+
+```csharp
+// Baseline data needed by every test suite.
+builder.Services.AddInitialData();
+
+// Only in Development, add some demo rows.
+if (builder.Environment.IsDevelopment())
+{
+ builder.Services.AddInitialData(async (ctx, ct) =>
+ {
+ ctx.Items.Add(new Item { Name = "Dev Demo" });
+ await ctx.SaveChangesAsync(ct);
+ });
+}
+```
+
+This is easier to maintain than a single "giant seeder" and plays well with conditional registration (environment, feature flags, tenant).
+
+## Interaction with `ResetAllDataAsync`
+
+`host.ResetAllDataAsync()` does two things, in order:
+
+1. Delete every row from the tables mapped by `ItemsDbContext` in foreign-key-safe order.
+2. Invoke every registered `IInitialData`.
+
+So the contract of a seeder is: **after me, the database contains whatever I just wrote, plus whatever later seeders write.** If you need the row to exist after `ResetAllDataAsync()`, put it in an `IInitialData`.
+
+## Idempotency
+
+Seeders are expected to be idempotent across resets — the cleaner always deletes first, so you can use fixed primary keys without collision concerns:
+
+```csharp
+public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
+{
+ context.Items.Add(new Item
+ {
+ Id = Guid.Parse("10000000-0000-0000-0000-000000000001"),
+ Name = "Deterministic seed"
+ });
+ await context.SaveChangesAsync(cancellation);
+}
+```
+
+This makes assertions trivial: look up the known Id, no need to keep a reference returned from setup.
+
+## When not to use `IInitialData`
+
+- **Production bootstrap data** — `IInitialData` runs on *reset*, not on every app start. For data that should exist on first deploy, use EF Core's [`UseSeeding`](https://learn.microsoft.com/ef/core/modeling/data-seeding) or an explicit setup command.
+- **Per-test unique fixtures** — if each test needs fundamentally different baseline data, keep that inside the test (Arrange) rather than baking it into shared seeders. `IInitialData` is for the *common floor* across your suite.
diff --git a/docs/guide/durability/efcore/migrations.md b/docs/guide/durability/efcore/migrations.md
index db7aee88f..8fe80f84a 100644
--- a/docs/guide/durability/efcore/migrations.md
+++ b/docs/guide/durability/efcore/migrations.md
@@ -1,6 +1,6 @@
# Database Migrations
-Wolverine uses [Weasel](https://github.com/JasperFx/weasel) for schema management of EF Core `DbContext` types rather than EF Core's own migration system. This approach provides a consistent schema management experience across the entire "critter stack" (Wolverine + Marten) and avoids issues with EF Core's `Database.EnsureCreatedAsync()` bypassing migration history.
+Wolverine uses [Weasel](https://weasel.jasperfx.net/) for schema management of EF Core `DbContext` types rather than EF Core's own migration system. This approach provides a consistent schema management experience across the entire "critter stack" (Wolverine + Marten) and avoids issues with EF Core's `Database.EnsureCreatedAsync()` bypassing migration history. See the [Weasel EF Core migration docs](https://weasel.jasperfx.net/efcore/migrations.html) for the underlying diff engine, provider-specific behavior, and opt-outs.
## How It Works
diff --git a/docs/guide/durability/efcore/query-plans.md b/docs/guide/durability/efcore/query-plans.md
index 34631efbe..facc0c742 100644
--- a/docs/guide/durability/efcore/query-plans.md
+++ b/docs/guide/durability/efcore/query-plans.md
@@ -124,7 +124,7 @@ You can also return a plan instance directly from a handler's `Load` /
plan type in the return and auto-executes it, passing the materialized
result to `Handle` / `Validate` / `After` parameters. When multiple
batch-capable plans target the same `DbContext` on one handler, they share
-a single [Weasel `BatchedQuery`](https://github.com/JasperFx/weasel) —
+a single [Weasel `BatchedQuery`](https://weasel.jasperfx.net/efcore/batch-queries.html) —
**one database round-trip for all plans**.
```csharp
diff --git a/docs/guide/durability/managing.md b/docs/guide/durability/managing.md
index 7aa07a398..76d275cbe 100644
--- a/docs/guide/durability/managing.md
+++ b/docs/guide/durability/managing.md
@@ -115,7 +115,7 @@ The available commands are:
storage Administer the envelope storage
```
-There's admittedly some duplication here with different options coming from [Oakton](https://jasperfx.github.io/oakton) itself, the [Weasel.CommandLine](https://github.com/JasperFx/weasel) library,
+There's admittedly some duplication here with different options coming from [Oakton](https://jasperfx.github.io/oakton) itself, the [Weasel.CommandLine](https://weasel.jasperfx.net/) library,
and the `storage` command from Wolverine itself. To build out the schema objects for [message persistence](/guide/durability/), you
can use this command to apply any outstanding database changes necessary to bring the database schema to the Wolverine configuration:
diff --git a/src/Persistence/EfCoreTests/auto_database_cleaner_tests.cs b/src/Persistence/EfCoreTests/auto_database_cleaner_tests.cs
new file mode 100644
index 000000000..b5832884a
--- /dev/null
+++ b/src/Persistence/EfCoreTests/auto_database_cleaner_tests.cs
@@ -0,0 +1,115 @@
+using IntegrationTests;
+using JasperFx.Resources;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using SharedPersistenceModels.Items;
+using Shouldly;
+using Weasel.EntityFrameworkCore;
+using Wolverine;
+using Wolverine.EntityFrameworkCore;
+using Wolverine.SqlServer;
+
+namespace EfCoreTests;
+
+///
+/// Verifies the GH-2539 dev-time ergonomics wins on the Wolverine side:
+/// 1.
+/// auto-registers Weasel's open-generic /
+/// so callers no longer need a per-context
+/// services.AddDatabaseCleaner<T>().
+/// 2. The new host.ResetAllDataAsync<T>() extension reaches through a
+/// scope, resolves the cleaner, and wipes+reseeds the one DbContext.
+///
+public class AutoDatabaseCleanerContext : IAsyncLifetime
+{
+ public IHost Host { get; private set; } = null!;
+
+ public async Task InitializeAsync()
+ {
+ Host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
+ .ConfigureServices((_, services) =>
+ {
+ services.AddDbContext(x => x.UseSqlServer(Servers.SqlServerConnectionString));
+
+ // NOTE: no explicit AddDatabaseCleaner() here — that
+ // is exactly the registration we're eliminating. Seed data still
+ // needs to be registered; only the cleaner itself is now automatic.
+ services.AddInitialData();
+ })
+ .UseWolverine(opts =>
+ {
+ opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString);
+ opts.UseEntityFrameworkCoreTransactions();
+ opts.Services.AddResourceSetupOnStartup(StartupAction.ResetState);
+ opts.UseEntityFrameworkCoreWolverineManagedMigrations();
+ opts.Policies.AutoApplyTransactions();
+ })
+ .StartAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await Host.StopAsync();
+ Host.Dispose();
+ }
+}
+
+[Collection("sqlserver")]
+public class auto_database_cleaner_registration_tests : IClassFixture
+{
+ private readonly AutoDatabaseCleanerContext _ctx;
+
+ public auto_database_cleaner_registration_tests(AutoDatabaseCleanerContext ctx)
+ {
+ _ctx = ctx;
+ }
+
+ [Fact]
+ public void IDatabaseCleaner_of_T_resolves_without_explicit_registration()
+ {
+ // UseEntityFrameworkCoreTransactions() alone should be enough.
+ var cleaner = _ctx.Host.Services.GetRequiredService>();
+ cleaner.ShouldNotBeNull();
+ cleaner.ShouldBeOfType>();
+ }
+
+ [Fact]
+ public void concrete_DatabaseCleaner_of_T_also_resolves()
+ {
+ // Consumers that want the concrete type (e.g. internal helpers) should
+ // also get it from DI without registering it themselves.
+ var cleaner = _ctx.Host.Services.GetRequiredService>();
+ cleaner.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void auto_registration_is_singleton()
+ {
+ var a = _ctx.Host.Services.GetRequiredService>();
+ var b = _ctx.Host.Services.GetRequiredService>();
+ a.ShouldBeSameAs(b);
+ }
+
+ [Fact]
+ public async Task host_ResetAllDataAsync_deletes_then_reseeds()
+ {
+ // Seed noise that should not survive the reset.
+ using (var scope = _ctx.Host.Services.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Items.Add(new Item { Id = Guid.NewGuid(), Name = "Noise" });
+ await db.SaveChangesAsync();
+ }
+
+ // Act: the new one-liner for test teardown.
+ await _ctx.Host.ResetAllDataAsync();
+
+ using var check = _ctx.Host.Services.CreateScope();
+ var checkDb = check.ServiceProvider.GetRequiredService();
+ var items = await checkDb.Items.OrderBy(x => x.Name).ToListAsync();
+
+ items.Select(x => x.Name).ShouldBe(
+ SeedItemsForTests.Items.Select(x => x.Name).OrderBy(n => n));
+ }
+}
diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/HostResetExtensions.cs b/src/Persistence/Wolverine.EntityFrameworkCore/HostResetExtensions.cs
new file mode 100644
index 000000000..3ff690341
--- /dev/null
+++ b/src/Persistence/Wolverine.EntityFrameworkCore/HostResetExtensions.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Weasel.EntityFrameworkCore;
+
+namespace Wolverine.EntityFrameworkCore;
+
+public static class HostResetExtensions
+{
+ ///
+ /// Reset the data for a single — delete every row in
+ /// foreign-key-safe order and then re-run every registered
+ /// seeder. A finer-grained alternative to
+ /// host.ResetResourceState() for integration tests that only need one
+ /// context cleaned between runs.
+ ///
+ ///
+ /// Requires WolverineOptions.UseEntityFrameworkCoreTransactions() to have
+ /// registered the open-generic , which is
+ /// the default as of Wolverine 5.x (GH-2539). Creates its own scope so it can be
+ /// called from test fixtures that aren't already inside one.
+ ///
+ /// The DbContext type to reset.
+ /// The running Wolverine/ASP.NET host.
+ /// Optional cancellation token.
+ public static async Task ResetAllDataAsync(this IHost host, CancellationToken ct = default)
+ where T : DbContext
+ {
+ using var scope = host.Services.CreateScope();
+
+ // Resolving the DbContext first exercises any scoped factory/registration (e.g.
+ // Wolverine's multi-tenant DbContext providers) before we ask the cleaner to act.
+ _ = scope.ServiceProvider.GetRequiredService();
+
+ var cleaner = scope.ServiceProvider.GetRequiredService>();
+ await cleaner.ResetAllDataAsync(ct);
+ }
+}
diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs b/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs
index c1f3025b8..210b7afe9 100644
--- a/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs
+++ b/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs
@@ -8,6 +8,7 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Weasel.EntityFrameworkCore;
using Wolverine.EntityFrameworkCore.Codegen;
using Wolverine.EntityFrameworkCore.Internals;
using Wolverine.EntityFrameworkCore.Internals.Migrations;
@@ -200,6 +201,14 @@ public static void UseEntityFrameworkCoreTransactions(this WolverineOptions opti
options.Services.AddScoped(typeof(IDbContextOutbox<>), typeof(DbContextOutbox<>));
options.Services.AddScoped();
options.Services.AddScoped();
+
+ // Open-generic registration of Weasel's DbContext cleaner so every
+ // DbContext used with Wolverine gets a ready-to-use IDatabaseCleaner
+ // without requiring callers to register each one individually. Backs
+ // the new host.ResetAllDataAsync() helper and any per-test cleanup
+ // in IInitialData-driven dev loops. See GH-2539.
+ options.Services.TryAdd(ServiceDescriptor.Singleton(typeof(IDatabaseCleaner<>), typeof(DatabaseCleaner<>)));
+ options.Services.TryAdd(ServiceDescriptor.Singleton(typeof(DatabaseCleaner<>), typeof(DatabaseCleaner<>)));
}
catch (InvalidOperationException e)
{
diff --git a/src/Samples/EFCoreSample/ItemService/Program.cs b/src/Samples/EFCoreSample/ItemService/Program.cs
index 459ae557d..5785cad0d 100644
--- a/src/Samples/EFCoreSample/ItemService/Program.cs
+++ b/src/Samples/EFCoreSample/ItemService/Program.cs
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using JasperFx;
using JasperFx.Resources;
+using Weasel.EntityFrameworkCore;
using Wolverine;
using Wolverine.EntityFrameworkCore;
using Wolverine.Http;
@@ -24,6 +25,21 @@
#endregion
+#region sample_register_initial_data
+// Seed data that will be applied every time
+// host.ResetAllDataAsync() is invoked (typical test flow).
+builder.Services.AddInitialData();
+
+// For small inline seed data, the Weasel 8.14+ lambda overload avoids
+// having to author a dedicated IInitialData class:
+//
+// builder.Services.AddInitialData(async (ctx, ct) =>
+// {
+// ctx.Items.Add(new Item { Name = "Demo" });
+// await ctx.SaveChangesAsync(ct);
+// });
+#endregion
+
#region registration_of_db_context_not_integrated_with_outbox
// Add DbContext that is not integrated with outbox
builder.Services.AddDbContext(
diff --git a/src/Samples/EFCoreSample/ItemService/SeedSampleItems.cs b/src/Samples/EFCoreSample/ItemService/SeedSampleItems.cs
new file mode 100644
index 000000000..69454bcd1
--- /dev/null
+++ b/src/Samples/EFCoreSample/ItemService/SeedSampleItems.cs
@@ -0,0 +1,25 @@
+using Weasel.EntityFrameworkCore;
+
+namespace ItemService;
+
+#region sample_initial_data_seeder
+///
+/// Seed data applied every time host.ResetAllDataAsync<ItemsDbContext>()
+/// or DatabaseCleaner<ItemsDbContext>.ResetAllDataAsync() runs.
+/// Multiple registrations execute in order.
+///
+public class SeedSampleItems : IInitialData
+{
+ public static readonly Item[] Items =
+ [
+ new Item { Id = Guid.Parse("10000000-0000-0000-0000-000000000001"), Name = "Alpha" },
+ new Item { Id = Guid.Parse("10000000-0000-0000-0000-000000000002"), Name = "Beta" }
+ ];
+
+ public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
+ {
+ context.Items.AddRange(Items);
+ await context.SaveChangesAsync(cancellation);
+ }
+}
+#endregion