Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bd6e60e
Add [ExcludeFromServiceCapabilities] assembly attribute to filter int…
jeremydmiller Feb 19, 2026
29ef089
Add VersionJsonConverter to fix System.Version JSON serialization
jeremydmiller Feb 19, 2026
eb82207
Remove VersionJsonConverter, now provided by JasperFx.Descriptors
jeremydmiller Feb 19, 2026
6f94969
Exclude system endpoints from ServiceCapabilities
jeremydmiller Feb 20, 2026
adba5fd
Tweak to HandlerPipeline for the combination of sequenced partitioned…
jeremydmiller Feb 23, 2026
9c380f5
Refactoring on PersistenceMetrics to ease the usage of IWolverineObse…
jeremydmiller Feb 23, 2026
583d918
Use pg_class reltuples to estimate inbox partition row counts
jeremydmiller Feb 23, 2026
9d25660
Changes to publish persistence metrics
jeremydmiller Feb 23, 2026
a0bbf45
Changes to support publishing persistence counts
jeremydmiller Feb 23, 2026
d999a2e
Shifted the metrics accumulation publishing to be through the observe…
jeremydmiller Feb 23, 2026
e24f4d0
Add SumByDestination, SumByMessageType, and Weight methods to Message…
jeremydmiller Feb 23, 2026
4c28e2e
Add XML API documentation to all types in Wolverine.Runtime.Metrics n…
jeremydmiller Feb 23, 2026
4215578
Replace local JasperFx project references with NuGet packages and upd…
jeremydmiller Feb 24, 2026
2926266
Replace EF Core EnsureCreatedAsync with Weasel schema management and …
jeremydmiller Feb 17, 2026
f74c981
Split EfCoreTests multi-tenancy into separate project to fix connecti…
jeremydmiller Feb 24, 2026
5196f26
Convert SqlServer persistence to use BatchBuilder (SqlBatch) instead …
jeremydmiller Feb 24, 2026
6d6d0ba
Upgrade Weasel.* NuGet packages from 8.7.1 to 8.8.0
jeremydmiller Feb 24, 2026
a65b218
removed extraneous projection references
jeremydmiller Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/efcore.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: efcore

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

env:
config: Release
disable_test_parallelization: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup .NET 8
uses: actions/setup-dotnet@v1
with:
dotnet-version: 8.0.x

- name: Setup .NET 9
uses: actions/setup-dotnet@v1
with:
dotnet-version: 9.0.x

- name: Setup .NET 10
uses: actions/setup-dotnet@v1
with:
dotnet-version: 10.0.x

- name: Start containers
run: docker compose up -d postgresql sqlserver

- name: Build
run: |
dotnet build src/Persistence/EfCoreTests/EfCoreTests.csproj --configuration ${{ env.config }} --framework net9.0
dotnet build src/Persistence/EfCoreTests.MultiTenancy/EfCoreTests.MultiTenancy.csproj --configuration ${{ env.config }} --framework net9.0

- name: Wait for PostgreSQL
run: |
echo "Waiting for PostgreSQL to be ready..."
for i in {1..30}; do
if docker compose exec -T postgresql pg_isready -U postgres; then
echo "PostgreSQL is ready"
break
fi
echo "Attempt $i: PostgreSQL not ready yet, waiting..."
sleep 2
done

- name: Wait for SQL Server
run: |
echo "Waiting for SQL Server to be ready..."
for i in {1..30}; do
if docker compose exec -T sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'P@55w0rd' -C -Q "SELECT 1" > /dev/null 2>&1; then
echo "SQL Server is ready"
break
fi
echo "Attempt $i: SQL Server not ready yet, waiting..."
sleep 2
done

- name: Test EfCoreTests
run: dotnet test src/Persistence/EfCoreTests/EfCoreTests.csproj --configuration ${{ env.config }} --framework net9.0 --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"

- name: Test EfCoreTests.MultiTenancy
run: dotnet test src/Persistence/EfCoreTests.MultiTenancy/EfCoreTests.MultiTenancy.csproj --configuration ${{ env.config }} --framework net9.0 --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"

- name: Stop containers
if: always()
run: docker compose down
18 changes: 9 additions & 9 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<PackageVersion Include="Grpc.Core" Version="2.46.6" />
<PackageVersion Include="Grpc.Tools" Version="2.72.0" />
<PackageVersion Include="HtmlTags" Version="9.0.0" />
<PackageVersion Include="JasperFx" Version="1.19.0" />
<PackageVersion Include="JasperFx.Events" Version="1.21.0" />
<PackageVersion Include="JasperFx" Version="1.20.0" />
<PackageVersion Include="JasperFx.Events" Version="1.21.1" />
<PackageVersion Include="JasperFx.RuntimeCompiler" Version="4.4.0" />
<PackageVersion Include="Lamar.Microsoft.DependencyInjection" Version="15.0.1" />
<PackageVersion Include="Marten" Version="8.22.0" />
Expand Down Expand Up @@ -79,13 +79,13 @@
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.5" />
<PackageVersion Include="System.Net.NameResolution" Version="4.3.0" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.5" />
<PackageVersion Include="Weasel.Core" Version="8.6.2" />
<PackageVersion Include="Weasel.EntityFrameworkCore" Version="8.6.2" />
<PackageVersion Include="Weasel.MySql" Version="8.6.2" />
<PackageVersion Include="Weasel.Oracle" Version="8.6.2" />
<PackageVersion Include="Weasel.Postgresql" Version="8.6.2" />
<PackageVersion Include="Weasel.SqlServer" Version="8.6.2" />
<PackageVersion Include="Weasel.Sqlite" Version="8.6.2" />
<PackageVersion Include="Weasel.Core" Version="8.8.0" />
<PackageVersion Include="Weasel.EntityFrameworkCore" Version="8.8.0" />
<PackageVersion Include="Weasel.MySql" Version="8.8.0" />
<PackageVersion Include="Weasel.Oracle" Version="8.8.0" />
<PackageVersion Include="Weasel.Postgresql" Version="8.8.0" />
<PackageVersion Include="Weasel.SqlServer" Version="8.8.0" />
<PackageVersion Include="Weasel.Sqlite" Version="8.8.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.assemblyfixture" Version="2.2.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
Expand Down
5 changes: 3 additions & 2 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,9 @@ const config: UserConfig<DefaultTheme.Config> = {
{text: 'Operation Side Effects', link: '/guide/durability/efcore/operations'},
{text: 'Saga Storage', link: '/guide/durability/efcore/sagas'},
{text: 'Multi-Tenancy', link: '/guide/durability/efcore/multi-tenancy'},
{text: 'Domain Events', link: '/guide/durability/efcore/domain-events'}

{text: 'Domain Events', link: '/guide/durability/efcore/domain-events'},
{text: 'Database Migrations', link: '/guide/durability/efcore/migrations'}

]},
{text: 'Managing Message Storage', link: '/guide/durability/managing'},
{text: 'Dead Letter Storage', link: '/guide/durability/dead-letter-storage'},
Expand Down
108 changes: 108 additions & 0 deletions docs/guide/durability/efcore/migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 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.

## How It Works

When you register a `DbContext` with Wolverine using `AddDbContextWithWolverineIntegration<T>()` or call `UseEntityFrameworkCoreWolverineManagedMigrations()`, Wolverine will:

1. **Read the EF Core model** — Wolverine inspects your `DbContext`'s entity types, properties, and relationships to build a Weasel schema representation
2. **Compare against the actual database** — Weasel connects to the database and compares the expected schema with the current state
3. **Apply deltas** — Only the necessary changes (new tables, added columns, foreign keys) are applied

This all happens automatically at application startup when you use `UseResourceSetupOnStartup()` or through Wolverine's resource management commands.

## Enabling Weasel-Managed Migrations

To opt into Weasel-managed migrations for your EF Core `DbContext` types, add this to your Wolverine configuration:

```csharp
builder.UseWolverine(opts =>
{
opts.PersistMessagesWithSqlServer(connectionString);

opts.Services.AddDbContextWithWolverineIntegration<MyDbContext>(
x => x.UseSqlServer(connectionString));

// Enable Weasel-managed migrations for all registered DbContext types
opts.UseEntityFrameworkCoreWolverineManagedMigrations();
});
```

With this in place, Wolverine will create and update your EF Core tables using Weasel at startup, alongside any Wolverine envelope storage tables.

## What Gets Migrated

Weasel will manage the following schema elements from your EF Core model:

- **Tables** — Created from entity types registered in `DbSet<T>` properties
- **Columns** — Mapped from entity properties, including types, nullability, and default values
- **Primary keys** — Derived from `DbContext` key configuration
- **Foreign keys** — Including cascade delete behavior
- **Schema names** — Respects EF Core's `ToSchema()` configuration

Entity types excluded from migrations via EF Core's `ExcludeFromMigrations()` are also excluded from Weasel management.

## Programmatic Migration

You can also trigger migrations programmatically using the Weasel extension methods on `IServiceProvider`:

```csharp
// Create a migration plan for a specific DbContext
await using var migration = await serviceProvider
.CreateMigrationAsync(dbContext, CancellationToken.None);

// Apply the migration (only applies if there are actual differences)
await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, CancellationToken.None);
```

The `CreateMigrationAsync()` method compares the EF Core model against the actual database schema and produces a `DbContextMigration` object. Calling `ExecuteAsync()` applies any necessary changes.

### Creating the Database

If you need to ensure the database itself exists (not just the tables), use:

```csharp
await serviceProvider.EnsureDatabaseExistsAsync(dbContext);
```

This uses Weasel's provider-specific database creation logic, which only creates the database catalog — it does not create any tables or schema objects.

## Multi-Tenancy

For multi-tenant setups where each tenant has its own database, Wolverine will automatically ensure each tenant database exists and apply schema migrations when using the tenanted `DbContext` builder. See [Multi-Tenancy](./multi-tenancy) for details.

## Weasel vs EF Core Migrations

| Feature | Weasel (Wolverine) | EF Core Migrations |
|---------|-------------------|-------------------|
| Migration tracking | Compares live schema | Migration history table |
| Code generation | None needed | `dotnet ef migrations add` |
| Additive changes | Automatic | Requires new migration |
| Works with Marten | Yes, unified approach | No |
| Rollback support | No | Yes, via `Down()` method |

::: tip
Weasel migrations are **additive** — they can create tables and add columns, but will not drop columns or tables automatically. This makes them safe for `CreateOrUpdate` scenarios in production.
:::

::: warning
If you are already using EF Core's migration system (`dotnet ef migrations add`, `Database.MigrateAsync()`), you should choose one approach or the other. Mixing EF Core migrations with Weasel-managed migrations can lead to conflicts. Wolverine's Weasel-managed approach is recommended for applications in the "critter stack" ecosystem.
:::

## CLI Commands

When Weasel-managed migrations are enabled, you can use Wolverine's built-in resource management:

```bash
# Apply all pending schema changes
dotnet run -- resources setup

# Check current database status
dotnet run -- resources list

# Reset all state (development only!)
dotnet run -- resources clear
```

These commands manage both Wolverine's internal tables and your EF Core entity tables together.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TargetFrameworks>net9.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Alba" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="GitHubActionsTestLogger" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL"/>
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\Servers.cs">
<Link>Servers.cs</Link>
</Compile>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Testing\Wolverine.ComplianceTests\Wolverine.ComplianceTests.csproj" />
<ProjectReference Include="..\MultiTenantedEfCoreWithPostgreSQL\MultiTenantedEfCoreWithPostgreSQL.csproj" />
<ProjectReference Include="..\MultiTenantedEfCoreWithSqlServer\MultiTenantedEfCoreWithSqlServer.csproj" />
<ProjectReference Include="..\SharedPersistenceModels\SharedPersistenceModels.csproj" />
<ProjectReference Include="..\Wolverine.EntityFrameworkCore\Wolverine.EntityFrameworkCore.csproj"/>
<ProjectReference Include="..\Wolverine.Marten\Wolverine.Marten.csproj" />
<ProjectReference Include="..\Wolverine.Postgresql\Wolverine.Postgresql.csproj"/>
<ProjectReference Include="..\Wolverine.SqlServer\Wolverine.SqlServer.csproj"/>
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Persistence/EfCoreTests.MultiTenancy/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,29 @@ public async Task InitializeAsync()
theBuilder = theHost.Services.GetRequiredService<IDbContextBuilder<ItemsDbContext>>();
}

public Task DisposeAsync()
public async Task DisposeAsync()
{
return theHost.StopAsync();
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await theHost.StopAsync(cts.Token);
}
catch (Exception)
{
// Swallow shutdown errors - host may have already stopped or timed out
}

try
{
theHost.Dispose();
}
catch (Exception)
{
// Swallow errors from inner WebApplicationFactory dispose
}

NpgsqlConnection.ClearAllPools();
SqlConnection.ClearAllPools();
}

public abstract void Configure(WolverineOptions options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,5 @@ public async Task HandleAsync(CreateItem command, TenantId tenantId, Cancellatio

#endregion

public record CreateItem(string Name);

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: CollectionBehavior(DisableTestParallelization = true)]
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,5 @@ public override void Configure(WolverineOptions opts)
opts.Services.RemoveAll(typeof(OrdersDbContext));
opts.AddSagaType<Order>();

opts.Services.AddResourceSetupOnStartup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,5 @@ public override void Configure(WolverineOptions opts)
builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,5 @@ public override void Configure(WolverineOptions opts)
b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ public override void Configure(WolverineOptions opts)
builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();

}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ public override void Configure(WolverineOptions opts)
builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();
}

[Fact]
public async Task opens_the_db_context_to_the_correct_database_1()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ public override void Configure(WolverineOptions opts)
builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();


}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,8 @@ public override void Configure(WolverineOptions opts)
builder.UseNpgsql<ItemsDbContext>((DbDataSource)dataSource, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL"));
}, AutoCreate.CreateOrUpdate);

opts.Services.AddResourceSetupOnStartup();
}

[Fact]
public async Task opens_the_db_context_to_the_correct_database_1()
{
Expand Down
Loading
Loading