Aspire Seed Gate
An AppHost-side gate that seeds an EF Core database on exactly two triggers: the first time the database is set up, and, if you ask for it, a destructive recreation on every start. An ordinary AppHost restart never re-seeds. The “already seeded” marker lives inside the database, so dropping the container volume drops the marker too, and the next start is a first-time setup again.
dotnet add package Shiny.EntityFrameworkCore.Aspire.Hosting// AppHost Program.csvar db = builder.AddPostgres("pg") .WithDataVolume() .AddDatabase("appdb") .WithDbContextSeeder<AppDbContext>( (options, connectionString) => options.UseNpgsql(connectionString), async (db, ctx, ct) => { db.Countries.Add(new Country { Code = "CA", Name = "Canada" }); // pending changes are saved for you before the marker is written });
builder.AddProject<Projects.Api>("api") .WithReference(db) .WaitFor(db); // the API starts only after the gate has finishedWhen the gate seeds
Section titled “When the gate seeds”.WithDbContextSeeder<AppDbContext>(configure, seed); // DbSeedMode.FirstTimeOnly (default)
// AppHost start #1 -> no marker -> migrate, seed, write marker// AppHost start #2 -> marker present -> migrate, skip seeding// `docker volume rm` the volume, #3 -> marker gone -> migrate, seed, write markerEvery start brings the schema up to date, so a migration you just added is applied without a volume drop. Only the seed callback is gated.
| Mode | No marker (first-time setup) | Marker present |
|---|---|---|
FirstTimeOnly (default) |
initialize, seed (Recreate = false) |
initialize, skip |
Recreate |
initialize, seed (Recreate = false) |
EnsureDeleted, initialize, seed (Recreate = true) |
The marker is written only after the callback succeeds (and after a SaveChangesAsync for anything it left
pending). A seed that throws is retried as a first-time setup on the next start instead of being silently
skipped.
The callback
Section titled “The callback”.WithDbContextSeeder<AppDbContext>( (options, cs) => options.UseSqlServer(cs), async (db, ctx, ct) => { // db : your AppDbContext, connected to the resource's database, schema already initialized // ctx : DbSeedGateContext(ResourceName, ConnectionString, Recreate) await DbSeedRunner.RunAsync(db, [new CountrySeeder(), new CurrencySeeder()], ct); // reuse app seeders }, DbSeedMode.Recreate, DbInitialization.Migrate);| Parameter | |
|---|---|
configure |
points DbContextOptionsBuilder<TContext> at the connection string: UseNpgsql, UseSqlServer, UseMySQL, UseSqlite, plus anything else you’d configure |
seed |
(TContext db, DbSeedGateContext ctx, CancellationToken ct) => Task |
mode |
FirstTimeOnly (default) or Recreate |
initialization |
Migrate (default), EnsureCreated, or None. None leaves the schema to your callback and isn’t allowed with Recreate, which drops the whole database |
The context is created with ActivatorUtilities, so the usual AppDbContext(DbContextOptions<AppDbContext>)
constructor works, and extra constructor parameters come from the AppHost’s services.
Running your app’s IDbSeeders inside the callback works well. The app’s own startup seeding
then finds their markers and does nothing.
Referencing your DbContext from the AppHost
Section titled “Referencing your DbContext from the AppHost”The AppHost needs the DbContext type and its EF provider. Reference the project that defines the context.
If that project is a library rather than a service the AppHost runs, mark it so Aspire doesn’t treat it as a
project resource:
<ItemGroup> <ProjectReference Include="..\MyApp.Data\MyApp.Data.csproj" IsAspireProjectResource="false" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" /></ItemGroup>Migrations come from the context’s migrations assembly, the same as at app runtime.
Raw gate: WithDbSeeder
Section titled “Raw gate: WithDbSeeder”When the seeding doesn’t go through a DbContext of yours (SQL scripts, a CLI tool, another library), use the
raw gate. It hands you the connection string and which trigger fired. Your callback owns the schema and the
wipe:
builder.AddPostgres("pg").AddDatabase("appdb") .WithDbSeeder( (options, cs) => options.UseNpgsql(cs), // how the gate reaches its marker async (ctx, ct) => { if (ctx.Recreate) await DropEverythingAsync(ctx.ConnectionString, ct); // false on a first-time setup await RunSeedScriptAsync(ctx.ConnectionString, ct); }, DbSeedMode.Recreate);The marker is still read and written through EF, with a model-less DbContext that the first argument points
at the connection string. That is why the package carries no Npgsql / SqlClient / MySQL driver of its own: your
AppHost already references the EF provider. If the callback drops the database and doesn’t recreate it, the gate
throws instead of writing the marker into nothing.
How it works
Section titled “How it works”- Each gate subscribes to the database resource’s
ResourceReadyEvent. Aspire raises it once the database is created and healthy, and resources thatWaitFor(db)don’t start until the handler finishes. So your API never sees a half-seeded database. - The marker is a row in the Seeding package’s
__ShinySeedHistorytable, keyedaspire:<resource>:<ContextType>forWithDbContextSeederandaspire:<resource>forWithDbSeeder. Several context types can share one database and each still gets its own first-time setup. - If the database doesn’t exist, there’s no marker, and that counts as a first-time setup.
Why reuse the Seeding table instead of a separate gate table? DocumentDb kept them apart because its seed
markers are documents that need a configured store, while its AppHost only had a raw connection. Here both sides
have an EF context, so one table, one set of per-provider SQL, and one concurrency story serve both. The aspire:
prefix keeps gate markers away from your seeder names, and a Recreate wipe resets both, which is what you want
from “start from scratch”.
Provider support
Section titled “Provider support”Any database resource whose connection string your EF provider accepts. Tested with
AddPostgres().AddDatabase(), AddSqlServer().AddDatabase() and AddMySql().AddDatabase(). The marker SQL
supports PostgreSQL, SQL Server, MySQL (MySql.EntityFrameworkCore) and SQLite. See
Versioned Seeding → Provider support.
Limitations
Section titled “Limitations”Recreateis whole-database.EnsureDeleteddrops the database, so twoRecreategates on one database wipe each other’s data. Use one gate per database inRecreatemode.- The extension returns
IResourceBuilder<IResourceWithConnectionString>, so you can call it with only the context type,WithDbContextSeeder<AppDbContext>(…). Call it last in a resource chain, or keep the original builder in a variable. - AppHost-only. Nothing here runs in a deployed environment. Aspire publishing ignores the eventing subscription.


