From bc84fed898f93cba3c639294b8d67715cc202373 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 9 Feb 2026 10:59:15 -0600 Subject: [PATCH 1/3] EF Core migration support with Weasel. Closes GH-2149 updated to the latest Weasel, JasperFx, and Marten Checkpoint: EF Core related tests are all passing checkpoint: hooks are in for resource creation of multi-tenancy Just moved up the timeout on a test Fix EF Core mapped outbox failing to INSERT envelopes with NULL attempts Remove .HasDefaultValue(0) from EF Core entity mappings for the Attempts column on IncomingMessage and OutgoingMessage. The HasDefaultValue sentinel caused EF Core to omit the column from INSERT statements when Attempts was 0, but the database column is NOT NULL, causing SqlException. Also clean up test fixtures to use managed migrations instead of manual table creation. First successful tests WIP: EF Core migrations Spiked in the EntityFrameworkCoreSystemPart --- build/build.cs | 1 - ...parated_behavior_and_scheduled_messages.cs | 2 +- ...configuration_of_domain_events_scrapers.cs | 23 +- .../Migrations/with_one_postgresql_context.cs | 103 +++++ .../Migrations/with_one_sqlserver_context.cs | 151 ++++++++ .../EfCoreTests/NoParallelization.cs | 2 +- ...cy_with_non_wolverine_mapped_db_context.cs | 3 - .../end_to_end_efcore_persistence.cs | 48 +-- ...rage_return_types_and_entity_attributes.cs | 16 +- ...0519181411_InitialCreateOrders.Designer.cs | 45 --- .../20250519181411_InitialCreateOrders.cs | 39 -- .../20250514191755_InitialCreate.Designer.cs | 49 --- .../ItemsDb/20250514191755_InitialCreate.cs | 39 -- .../ItemsDb/ItemsDbContextModelSnapshot.cs | 48 --- .../OrdersDbContextModelSnapshot.cs | 42 -- .../Program.cs | 3 + .../20250519140210_InitialCreate.Designer.cs | 51 --- .../ItemsDb/20250519140210_InitialCreate.cs | 40 -- .../ItemsDb/ItemsDbContextModelSnapshot.cs | 48 --- .../20250519140201_InitialCreate.Designer.cs | 44 --- .../OrdersDb/20250519140201_InitialCreate.cs | 39 -- .../OrdersDb/OrdersDbContextModelSnapshot.cs | 42 -- .../Program.cs | 3 + .../Wolverine.MySql/MySqlBackedPersistence.cs | 4 + .../MySqlTenantedMessageStore.cs | 14 +- .../INTERNALS.md | 365 ++++++++++++++++++ .../Internals/IDbContextBuilder.cs | 5 + .../EntityFrameworkCoreSystemPart.cs | 122 ++++++ ...antedDbContextBuilderByConnectionString.cs | 25 +- .../TenantedDbContextBuilderByDbDataSource.cs | 28 +- .../Wolverine.EntityFrameworkCore.csproj | 7 +- .../WolverineEntityCoreExtensions.cs | 19 +- .../MartenMessageDatabaseSource.cs | 14 +- .../Wolverine.Marten/Wolverine.Marten.csproj | 2 +- .../WolverineOptionsMartenExtensions.cs | 3 + .../PostgresqlBackedPersistence.cs | 6 +- .../PostgresqlMessageStore.cs | 1 + .../PostgresqlTenantedMessageStore.cs | 14 +- .../Wolverine.Postgresql.csproj | 2 +- .../Wolverine.RDBMS/Wolverine.RDBMS.csproj | 2 +- .../Persistence/SqlServerMessageStore.cs | 3 +- .../SqlServerBackedPersistence.cs | 6 +- .../SqlServerTenantedMessageStore.cs | 14 +- .../Wolverine.SqlServer.csproj | 2 +- src/Wolverine/AssemblyAttributes.cs | 2 + .../Persistence/Durability/IMessageStore.cs | 1 + src/Wolverine/Wolverine.csproj | 2 +- wolverine.sln | 24 +- 48 files changed, 927 insertions(+), 641 deletions(-) create mode 100644 src/Persistence/EfCoreTests/Migrations/with_one_postgresql_context.cs create mode 100644 src/Persistence/EfCoreTests/Migrations/with_one_sqlserver_context.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.Designer.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.Designer.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/OrdersDbContextModelSnapshot.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.Designer.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.Designer.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.cs delete mode 100644 src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/OrdersDbContextModelSnapshot.cs create mode 100644 src/Persistence/Wolverine.EntityFrameworkCore/INTERNALS.md create mode 100644 src/Persistence/Wolverine.EntityFrameworkCore/Internals/Migrations/EntityFrameworkCoreSystemPart.cs diff --git a/build/build.cs b/build/build.cs index 1497683fc..7dd61e0a2 100644 --- a/build/build.cs +++ b/build/build.cs @@ -317,7 +317,6 @@ class Build : NukeBuild Solution.Transports.GCP.Wolverine_Pubsub, Solution.Persistence.Wolverine_RDBMS, Solution.Persistence.Wolverine_Postgresql, - Solution.Persistence.Wolverine_EntityFrameworkCore, Solution.Persistence.Wolverine_Marten, Solution.Persistence.Wolverine_RavenDb, Solution.Persistence.Wolverine_SqlServer, diff --git a/src/Persistence/EfCoreTests/Bugs/Bug_2075_separated_behavior_and_scheduled_messages.cs b/src/Persistence/EfCoreTests/Bugs/Bug_2075_separated_behavior_and_scheduled_messages.cs index 9af0aed44..1c0fcb655 100644 --- a/src/Persistence/EfCoreTests/Bugs/Bug_2075_separated_behavior_and_scheduled_messages.cs +++ b/src/Persistence/EfCoreTests/Bugs/Bug_2075_separated_behavior_and_scheduled_messages.cs @@ -41,7 +41,7 @@ public async Task MyBug() }) .StartAsync(); - var session = await host.TrackActivity().DoNotAssertOnExceptionsDetected().WaitForMessageToBeReceivedAt(host) + var session = await host.TrackActivity().DoNotAssertOnExceptionsDetected().WaitForMessageToBeReceivedAt(host).Timeout(30.Seconds()) .SendMessageAndWaitAsync(new SayStuffy0()); session.Executed.SingleMessage().ShouldNotBeNull(); diff --git a/src/Persistence/EfCoreTests/DomainEvents/configuration_of_domain_events_scrapers.cs b/src/Persistence/EfCoreTests/DomainEvents/configuration_of_domain_events_scrapers.cs index 67ca0bd7d..a1dd37887 100644 --- a/src/Persistence/EfCoreTests/DomainEvents/configuration_of_domain_events_scrapers.cs +++ b/src/Persistence/EfCoreTests/DomainEvents/configuration_of_domain_events_scrapers.cs @@ -60,29 +60,14 @@ public async Task startHostAsync(Action configure) opts.Services.AddScoped(); configure(opts); + + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); + opts.Services.AddResourceSetupOnStartup(); + //opts.PublishDomainEventsFromEntityFrameworkCore(); }).StartAsync(); await theHost.RebuildAllEnvelopeStorageAsync(); - - await withItemsTable(); - } - - private async Task withItemsTable() - { - await using (var conn = new SqlConnection(Servers.SqlServerConnectionString)) - { - await conn.OpenAsync(); - var migration = await SchemaMigration.DetermineAsync(conn, ItemsTable); - if (migration.Difference != SchemaPatchDifference.None) - { - var sqlServerMigrator = new SqlServerMigrator(); - - await sqlServerMigrator.ApplyAllAsync(conn, migration, AutoCreate.CreateOrUpdate); - } - - await conn.CloseAsync(); - } } [Fact] diff --git a/src/Persistence/EfCoreTests/Migrations/with_one_postgresql_context.cs b/src/Persistence/EfCoreTests/Migrations/with_one_postgresql_context.cs new file mode 100644 index 000000000..dbc1f69d1 --- /dev/null +++ b/src/Persistence/EfCoreTests/Migrations/with_one_postgresql_context.cs @@ -0,0 +1,103 @@ +using IntegrationTests; +using JasperFx.CommandLine.Descriptions; +using JasperFx.Core.Reflection; +using JasperFx.Environment; +using JasperFx.Resources; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Npgsql; +using Shouldly; +using Weasel.Core.Migrations; +using Weasel.Postgresql; +using Wolverine; +using Wolverine.EntityFrameworkCore; +using Wolverine.EntityFrameworkCore.Internals.Migrations; +using Wolverine.Postgresql; +using Wolverine.SqlServer; + +namespace EfCoreTests.Migrations; + +[Collection("postgresql")] +public class with_one_postgresql_context : IAsyncLifetime +{ + private IHost _host; + + public async Task InitializeAsync() + { + using var conn = new NpgsqlConnection(Servers.PostgresConnectionString); + await conn.OpenAsync(); + await conn.DropSchemaAsync("blogs"); + await conn.CloseAsync(); + + _host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + WolverineEntityCoreExtensions.AddDbContextWithWolverineIntegration(opts.Services, x => + { + x.UseNpgsql(Servers.PostgresConnectionString); + }); + + opts.Discovery.DisableConventionalDiscovery(); + + opts.Services.AddResourceSetupOnStartup(StartupAction.ResetState); + + opts.PersistMessagesWithPostgresql(Servers.PostgresConnectionString); + opts.UseEntityFrameworkCoreTransactions(); + + // TODO -- this might go away and get merged into UseEntityFrameworkCoreTransactions() above + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); + }).StartAsync(); + } + + public async Task DisposeAsync() + { + await _host.StopAsync(); + } + + [Fact] + public void registers_the_system_part() + { + _host.Services.GetServices().OfType() + .Any().ShouldBeTrue(); + } + + [Fact] + public async Task did_apply() + { + using var scope = _host.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + await context.Blogs.AddAsync(new Blog() + { + BlogId = 1, + Url = "http://codebetter.com" + }); + await context.SaveChangesAsync(); + } + + [Fact] + public async Task smoke_test_write_to_console() + { + var part = _host.Services.GetServices().OfType().First(); + await part.WriteToConsole(); + } + + [Fact] + public async Task smoke_test_check_connectivity() + { + var part = _host.Services.GetServices().OfType().First(); + var results = new EnvironmentCheckResults(); + await part.AssertEnvironmentAsync(_host.Services, results, CancellationToken.None); + + results.Failures.Any().ShouldBeFalse(); + } + + [Fact] + public async Task smoke_tests_describe_databases() + { + var part = _host.Services.GetServices().OfType().First(); + var usage = await part.As().DescribeDatabasesAsync(CancellationToken.None); + usage.ShouldNotBeNull(); + } +} \ No newline at end of file diff --git a/src/Persistence/EfCoreTests/Migrations/with_one_sqlserver_context.cs b/src/Persistence/EfCoreTests/Migrations/with_one_sqlserver_context.cs new file mode 100644 index 000000000..05f33749f --- /dev/null +++ b/src/Persistence/EfCoreTests/Migrations/with_one_sqlserver_context.cs @@ -0,0 +1,151 @@ +using System.ComponentModel.DataAnnotations.Schema; +using IntegrationTests; +using JasperFx.CommandLine.Descriptions; +using JasperFx.Core.Reflection; +using JasperFx.Environment; +using JasperFx.Resources; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SharedPersistenceModels.Items; +using Shouldly; +using Weasel.Core.Migrations; +using Weasel.SqlServer; +using Wolverine; +using Wolverine.EntityFrameworkCore; +using Wolverine.EntityFrameworkCore.Internals.Migrations; +using Wolverine.SqlServer; + +namespace EfCoreTests.Migrations; + +[Collection("sqlserver")] +public class with_one_sqlserver_context : IAsyncLifetime +{ + private IHost _host; + + public async Task InitializeAsync() + { + using var conn = new SqlConnection(Servers.SqlServerConnectionString); + await conn.OpenAsync(); + await conn.DropSchemaAsync("blogs"); + await conn.CloseAsync(); + + _host = await Host.CreateDefaultBuilder() + .UseWolverine(opts => + { + opts.Services.AddDbContextWithWolverineIntegration(x => + x.UseSqlServer(Servers.SqlServerConnectionString)); + + opts.Discovery.DisableConventionalDiscovery(); + + opts.Services.AddResourceSetupOnStartup(StartupAction.ResetState); + + opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString, "blogs"); + opts.UseEntityFrameworkCoreTransactions(); + + // TODO -- this might go away and get merged into UseEntityFrameworkCoreTransactions() above + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); + }).StartAsync(); + } + + public async Task DisposeAsync() + { + await _host.StopAsync(); + } + + [Fact] + public void registers_the_system_part() + { + _host.Services.GetServices().OfType() + .Any().ShouldBeTrue(); + } + + [Fact] + public async Task did_apply() + { + using var scope = _host.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + await context.Blogs.AddAsync(new Blog() + { + BlogId = 1, + Url = "http://codebetter.com" + }); + await context.SaveChangesAsync(); + } + + [Fact] + public async Task smoke_test_write_to_console() + { + var part = _host.Services.GetServices().OfType().First(); + await part.WriteToConsole(); + } + + [Fact] + public async Task smoke_test_check_connectivity() + { + var part = _host.Services.GetServices().OfType().First(); + var results = new EnvironmentCheckResults(); + await part.AssertEnvironmentAsync(_host.Services, results, CancellationToken.None); + + results.Failures.Any().ShouldBeFalse(); + } + + [Fact] + public async Task smoke_tests_describe_databases() + { + var part = _host.Services.GetServices().OfType().First(); + var usage = await part.As().DescribeDatabasesAsync(CancellationToken.None); + usage.ShouldNotBeNull(); + } +} + +public class Blog +{ + [Column("id")] + public int BlogId { get; set; } + + [Column("url")] + public string Url { get; set; } + // Navigation property for related posts + public List Posts { get; set; } +} + +public class Post +{ + [Column("post_id")] + public int PostId { get; set; } + + [Column("title")] + public string Title { get; set; } + + [Column("content")] + public string Content { get; set; } + // Foreign key to the Blog + [Column("blog_id")] + public int BlogId { get; set; } + // Navigation property for the related blog + public Blog Blog { get; set; } +} + +public class BloggingContext : DbContext +{ + // DbSet properties represent the collections of entities in the context + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + // Constructor for dependency injection (recommended in ASP.NET Core apps) + public BloggingContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.HasDefaultSchema("blogs"); + + + } +} \ No newline at end of file diff --git a/src/Persistence/EfCoreTests/NoParallelization.cs b/src/Persistence/EfCoreTests/NoParallelization.cs index 12b690c67..7f777c3ae 100644 --- a/src/Persistence/EfCoreTests/NoParallelization.cs +++ b/src/Persistence/EfCoreTests/NoParallelization.cs @@ -1 +1 @@ -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/src/Persistence/EfCoreTests/eager_idempotency_with_non_wolverine_mapped_db_context.cs b/src/Persistence/EfCoreTests/eager_idempotency_with_non_wolverine_mapped_db_context.cs index 647a4973f..516c7bc7a 100644 --- a/src/Persistence/EfCoreTests/eager_idempotency_with_non_wolverine_mapped_db_context.cs +++ b/src/Persistence/EfCoreTests/eager_idempotency_with_non_wolverine_mapped_db_context.cs @@ -22,10 +22,7 @@ public class eager_idempotency_with_non_wolverine_mapped_db_context : IClassFixt public eager_idempotency_with_non_wolverine_mapped_db_context(EFCorePersistenceContext context) { Host = context.theHost; - ItemsTable = context.ItemsTable; } - - public Table ItemsTable { get; } public IHost Host { get; } diff --git a/src/Persistence/EfCoreTests/end_to_end_efcore_persistence.cs b/src/Persistence/EfCoreTests/end_to_end_efcore_persistence.cs index 3ef08a460..b909c0118 100644 --- a/src/Persistence/EfCoreTests/end_to_end_efcore_persistence.cs +++ b/src/Persistence/EfCoreTests/end_to_end_efcore_persistence.cs @@ -45,15 +45,11 @@ public EFCorePersistenceContext() : base(true) .CustomizeQueues((_, q) => q.UseDurableInbox()); options.Policies.AutoApplyTransactions(); + + options.UseEntityFrameworkCoreWolverineManagedMigrations(); }); - - ItemsTable = new Table(new DbObjectName("mt_items", "items")); - ItemsTable.AddColumn("Id").AsPrimaryKey(); - ItemsTable.AddColumn("Name"); - ItemsTable.AddColumn("Approved"); } - public Table ItemsTable { get; } } [Collection("sqlserver")] @@ -62,19 +58,14 @@ public class end_to_end_efcore_persistence : IClassFixture>() @@ -253,8 +240,6 @@ public async Task persist_an_outgoing_envelope_mapped() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - using (var nested = Host.Services.CreateScope()) { var messaging = nested.ServiceProvider.GetRequiredService>() @@ -283,23 +268,6 @@ public async Task persist_an_outgoing_envelope_mapped() loadedEnvelope.OwnerId.ShouldBe(envelope.OwnerId); } - private async Task withItemsTable() - { - await using (var conn = new SqlConnection(Servers.SqlServerConnectionString)) - { - await conn.OpenAsync(); - var migration = await SchemaMigration.DetermineAsync(conn, ItemsTable); - if (migration.Difference != SchemaPatchDifference.None) - { - var sqlServerMigrator = new SqlServerMigrator(); - - await sqlServerMigrator.ApplyAllAsync(conn, migration, AutoCreate.CreateOrUpdate); - } - - await conn.CloseAsync(); - } - } - [Fact] public async Task use_non_generic_outbox_raw() { @@ -307,8 +275,6 @@ public async Task use_non_generic_outbox_raw() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - var waiter = OutboxedMessageHandler.WaitForNextMessage(); using (var nested = Host.Services.CreateScope()) @@ -341,8 +307,6 @@ public async Task use_non_generic_outbox_mapped() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - var waiter = OutboxedMessageHandler.WaitForNextMessage(); using (var nested = Host.Services.CreateScope()) @@ -375,8 +339,6 @@ public async Task use_generic_outbox_raw() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - var waiter = OutboxedMessageHandler.WaitForNextMessage(); using (var nested = Host.Services.CreateScope()) @@ -406,8 +368,6 @@ public async Task use_generic_outbox_mapped() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - var waiter = OutboxedMessageHandler.WaitForNextMessage(); using (var nested = Host.Services.CreateScope()) @@ -450,8 +410,6 @@ public async Task persist_an_incoming_envelope_raw() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - using (var nested = Host.Services.CreateScope()) { var context = nested.ServiceProvider.GetRequiredService(); @@ -497,8 +455,6 @@ public async Task persist_an_incoming_envelope_mapped() var container = Host.Services.GetRequiredService(); - await withItemsTable(); - using (var nested = Host.Services.CreateScope()) { var context = nested.ServiceProvider.GetRequiredService(); diff --git a/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs b/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs index 65b73a1db..fe570b7fc 100644 --- a/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs +++ b/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs @@ -1,4 +1,5 @@ using IntegrationTests; +using JasperFx.Resources; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -22,18 +23,9 @@ protected override void configureWolverine(WolverineOptions opts) }, "wolverine"); opts.PersistMessagesWithSqlServer(Servers.SqlServerConnectionString); - } - - protected override async Task initialize() - { - var table = new Table(new DbObjectName("todo_app", "todos")); - table.AddColumn("id").AsPrimaryKey(); - table.AddColumn("name"); - table.AddColumn("is_complete"); - - using var conn = new SqlConnection(Servers.SqlServerConnectionString); - await conn.OpenAsync(); - await table.MigrateAsync(conn); + + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); + opts.Services.AddResourceSetupOnStartup(); } public override async Task Load(string id) diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.Designer.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.Designer.cs deleted file mode 100644 index a4527dc92..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.Designer.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using SharedPersistenceModels.Orders; - -#nullable disable - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations -{ - [DbContext(typeof(OrdersDbContext))] - [Migration("20250519181411_InitialCreateOrders")] - partial class InitialCreateOrders - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Orders.Order", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("OrderStatus") - .HasColumnType("integer"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("orders", "mt_orders"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.cs deleted file mode 100644 index 52e6d4a14..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/20250519181411_InitialCreateOrders.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations -{ - /// - public partial class InitialCreateOrders : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "mt_orders"); - - migrationBuilder.CreateTable( - name: "orders", - schema: "mt_orders", - columns: table => new - { - Id = table.Column(type: "text", nullable: false), - OrderStatus = table.Column(type: "integer", nullable: false), - Version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_orders", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "orders", - schema: "mt_orders"); - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.Designer.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.Designer.cs deleted file mode 100644 index f27ac6742..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.Designer.cs +++ /dev/null @@ -1,49 +0,0 @@ -// - -#nullable disable - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using SharedPersistenceModels.Items; - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations.ItemsDb -{ - [DbContext(typeof(ItemsDbContext))] - [Migration("20250514191755_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Items.Item", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Approved") - .HasColumnType("boolean") - .HasColumnName("approved"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.HasKey("Id"); - - b.ToTable("items", "mt_items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.cs deleted file mode 100644 index 7dc2d1221..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/20250514191755_InitialCreate.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations.ItemsDb -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "mt_items"); - - migrationBuilder.CreateTable( - name: "items", - schema: "mt_items", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - name = table.Column(type: "text", nullable: false), - approved = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_items", x => x.id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "items", - schema: "mt_items"); - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs deleted file mode 100644 index f32b50130..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using SharedPersistenceModels.Items; - -#nullable disable - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations -{ - [DbContext(typeof(ItemsDbContext))] - partial class ItemsDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Items.Item", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("Approved") - .HasColumnType("boolean") - .HasColumnName("approved"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.HasKey("Id"); - - b.ToTable("items", "mt_items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/OrdersDbContextModelSnapshot.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/OrdersDbContextModelSnapshot.cs deleted file mode 100644 index 15f94a79a..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Migrations/OrdersDbContextModelSnapshot.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using SharedPersistenceModels.Orders; - -#nullable disable - -namespace MultiTenantedEfCoreWithPostgreSQL.Migrations -{ - [DbContext(typeof(OrdersDbContext))] - partial class OrdersDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Orders.Order", b => - { - b.Property("Id") - .HasColumnType("text"); - - b.Property("OrderStatus") - .HasColumnType("integer"); - - b.Property("Version") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("orders", "mt_orders"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Program.cs b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Program.cs index c2537e4c6..d8ba7914e 100644 --- a/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Program.cs +++ b/src/Persistence/MultiTenantedEfCoreWithPostgreSQL/Program.cs @@ -6,6 +6,7 @@ using SharedPersistenceModels.Items; using SharedPersistenceModels.Orders; using Wolverine; +using Wolverine.EntityFrameworkCore; using Wolverine.Http; namespace MultiTenantedEfCoreWithPostgreSQL; @@ -28,6 +29,8 @@ public static async Task Main(string[] args) opts.Policies.UseDurableLocalQueues(); TestingOverrides.Extension?.Configure(opts); + + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); }); builder.Services.AddWolverineHttp(); diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.Designer.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.Designer.cs deleted file mode 100644 index 71e82601c..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.Designer.cs +++ /dev/null @@ -1,51 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SharedPersistenceModels.Items; - -#nullable disable - -namespace MultiTenantedEfCoreWithSqlServer.Migrations.ItemsDb -{ - [DbContext(typeof(ItemsDbContext))] - [Migration("20250519140210_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Items.Item", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("id"); - - b.Property("Approved") - .HasColumnType("bit") - .HasColumnName("approved"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("name"); - - b.HasKey("Id"); - - b.ToTable("items", "mt_items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.cs deleted file mode 100644 index 1d545dc38..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/20250519140210_InitialCreate.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace MultiTenantedEfCoreWithSqlServer.Migrations.ItemsDb -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "mt_items"); - - migrationBuilder.CreateTable( - name: "items", - schema: "mt_items", - columns: table => new - { - id = table.Column(type: "uniqueidentifier", nullable: false), - name = table.Column(type: "nvarchar(max)", nullable: false), - approved = table.Column(type: "bit", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_items", x => x.id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "items", - schema: "mt_items"); - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs deleted file mode 100644 index 0671ca224..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/ItemsDb/ItemsDbContextModelSnapshot.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SharedPersistenceModels.Items; - -#nullable disable - -namespace MultiTenantedEfCoreWithSqlServer.Migrations.ItemsDb -{ - [DbContext(typeof(ItemsDbContext))] - partial class ItemsDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Items.Item", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("id"); - - b.Property("Approved") - .HasColumnType("bit") - .HasColumnName("approved"); - - b.Property("Name") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("name"); - - b.HasKey("Id"); - - b.ToTable("items", "mt_items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.Designer.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.Designer.cs deleted file mode 100644 index 98bf4bbab..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// - -#nullable disable - -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using SharedPersistenceModels.Orders; - -namespace MultiTenantedEfCoreWithSqlServer.Migrations.OrdersDb -{ - [DbContext(typeof(OrdersDbContext))] - [Migration("20250519140201_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Orders.Order", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("OrderStatus") - .HasColumnType("int"); - - b.Property("Version") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.ToTable("orders", "mt_orders"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.cs deleted file mode 100644 index ec48dff5c..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/20250519140201_InitialCreate.cs +++ /dev/null @@ -1,39 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace MultiTenantedEfCoreWithSqlServer.Migrations.OrdersDb -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "mt_orders"); - - migrationBuilder.CreateTable( - name: "orders", - schema: "mt_orders", - columns: table => new - { - Id = table.Column(type: "nvarchar(450)", nullable: false), - OrderStatus = table.Column(type: "int", nullable: false), - Version = table.Column(type: "int", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_orders", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "orders", - schema: "mt_orders"); - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/OrdersDbContextModelSnapshot.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/OrdersDbContextModelSnapshot.cs deleted file mode 100644 index aa60d7761..000000000 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Migrations/OrdersDb/OrdersDbContextModelSnapshot.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SharedPersistenceModels.Orders; - -#nullable disable - -namespace MultiTenantedEfCoreWithSqlServer.Migrations -{ - [DbContext(typeof(OrdersDbContext))] - partial class OrdersDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.5") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("SharedPersistenceModels.Orders.Order", b => - { - b.Property("Id") - .HasColumnType("nvarchar(450)"); - - b.Property("OrderStatus") - .HasColumnType("int"); - - b.Property("Version") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.ToTable("orders", "mt_orders"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Program.cs b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Program.cs index bb5aed1bc..bc6e7327f 100644 --- a/src/Persistence/MultiTenantedEfCoreWithSqlServer/Program.cs +++ b/src/Persistence/MultiTenantedEfCoreWithSqlServer/Program.cs @@ -6,6 +6,7 @@ using SharedPersistenceModels.Items; using SharedPersistenceModels.Orders; using Wolverine; +using Wolverine.EntityFrameworkCore; using Wolverine.Http; namespace MultiTenantedEfCoreWithSqlServer; @@ -28,6 +29,8 @@ public static async Task Main(string[] args) opts.Policies.UseDurableLocalQueues(); TestingOverrides.Extension?.Configure(opts); + + opts.UseEntityFrameworkCoreWolverineManagedMigrations(); }); builder.Services.AddWolverineHttp(); diff --git a/src/Persistence/MySql/Wolverine.MySql/MySqlBackedPersistence.cs b/src/Persistence/MySql/Wolverine.MySql/MySqlBackedPersistence.cs index 2e395150f..67eac71b1 100644 --- a/src/Persistence/MySql/Wolverine.MySql/MySqlBackedPersistence.cs +++ b/src/Persistence/MySql/Wolverine.MySql/MySqlBackedPersistence.cs @@ -5,7 +5,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MySqlConnector; +using Weasel.Core; using Weasel.Core.Migrations; +using Weasel.MySql; using Wolverine.ErrorHandling; using Wolverine.MySql.Transport; using Wolverine.Persistence.Durability; @@ -178,6 +180,8 @@ public void Configure(WolverineOptions options) options.Services.AddSingleton(s => BuildMessageStore(s.GetRequiredService())); options.Services.AddSingleton(); + + options.Services.AddSingleton(); } public IMessageStore BuildMessageStore(IWolverineRuntime runtime) diff --git a/src/Persistence/MySql/Wolverine.MySql/MySqlTenantedMessageStore.cs b/src/Persistence/MySql/Wolverine.MySql/MySqlTenantedMessageStore.cs index a184c96bf..d635231a1 100644 --- a/src/Persistence/MySql/Wolverine.MySql/MySqlTenantedMessageStore.cs +++ b/src/Persistence/MySql/Wolverine.MySql/MySqlTenantedMessageStore.cs @@ -95,7 +95,17 @@ private MySqlMessageStore buildTenantStoreForDataSource(MySqlDataSource source) return store; } - public async Task RefreshAsync() + public Task RefreshAsync() + { + return RefreshAsync(true); + } + + public Task RefreshLiteAsync() + { + return RefreshAsync(false); + } + + public async Task RefreshAsync(bool withMigration) { if (_persistence.ConnectionStringTenancy != null) { @@ -108,7 +118,7 @@ public async Task RefreshAsync() var store = buildTenantStoreForConnectionString(assignment.Value); store.TenantIds.Fill(assignment.TenantId); - if (_runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) + if (withMigration && _runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) { await store.Admin.MigrateAsync(); } diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/INTERNALS.md b/src/Persistence/Wolverine.EntityFrameworkCore/INTERNALS.md new file mode 100644 index 000000000..21abe9efc --- /dev/null +++ b/src/Persistence/Wolverine.EntityFrameworkCore/INTERNALS.md @@ -0,0 +1,365 @@ +# Wolverine.EntityFrameworkCore Internals + +This document describes the internal architecture of the `Wolverine.EntityFrameworkCore` project, which integrates Entity Framework Core with Wolverine's message handling pipeline. + +## Overview + +The project implements the **transactional outbox pattern** for EF Core. When a message handler modifies domain entities via a `DbContext`, any outgoing messages are persisted to the same database within the same transaction. After commit, Wolverine flushes the persisted messages to their transport destinations. This guarantees that database changes and message publishing are atomic — either both happen or neither does. + +The project also provides: + +- **Saga persistence** via EF Core (load, insert, update, delete) +- **Multi-tenancy** with per-tenant `DbContext` creation (by connection string or `DbDataSource`) +- **Domain event publishing** by scraping entities from the EF Core `ChangeTracker` +- **Idempotency** via inbox tracking of processed messages +- **Code generation** frames that compile into Wolverine's JIT-generated handler pipelines + +## Class Diagram + +```mermaid +classDiagram + direction TB + + %% ── Interfaces ────────────────────────────────────────────── + class IMessageBus { + <> + } + + class IDbContextOutbox { + <> + +DbContext ActiveContext + +Enroll(DbContext) + +SaveChangesAndFlushMessagesAsync() + +FlushOutgoingMessagesAsync() + } + + class IDbContextOutbox~T~ { + <> + +T DbContext + +SaveChangesAndFlushMessagesAsync() + +FlushOutgoingMessagesAsync() + } + + class IDbContextOutboxFactory { + <> + +CreateForTenantAsync~T~(tenantId) + } + + class IEnvelopeTransaction { + <> + +PersistOutgoingAsync(Envelope) + +PersistIncomingAsync(Envelope) + +CommitAsync() + +RollbackAsync() + } + + class IDbContextBuilder { + <> + +BuildForMain() DbContext + +DbContextType Type + +ApplyAllChangesToDatabasesAsync() + } + + class IDbContextBuilder~T~ { + <> + +BuildAndEnrollAsync(MessageContext, CancellationToken) + +BuildAsync(tenantId) + +BuildOptionsForMain() + } + + class IDomainEventScraper { + <> + +ScrapeEvents(DbContext, MessageContext) + } + + class IPersistenceFrameProvider { + <> + +CanPersist(entityType, chain, out dbContextType) + +DetermineLoadFrame() + +DetermineInsertFrame() + +DetermineUpdateFrame() + +DetermineDeleteFrame() + +CommitUnitOfWorkFrame() + +ApplyTransactionSupport() + } + + class IWolverineExtension { + <> + +Configure(WolverineOptions) + } + + %% ── Implementations ───────────────────────────────────────── + class MessageContext { + <> + } + + class DbContextOutbox { + -IDomainEventScraper[] _scrapers + +DbContext ActiveContext + +Enroll(DbContext) + +SaveChangesAndFlushMessagesAsync() + } + + class DbContextOutbox~T~ { + -IDomainEventScraper[] _scrapers + +T DbContext + +SaveChangesAndFlushMessagesAsync() + } + + class DbContextOutboxFactory { + -IWolverineRuntime _runtime + +CreateForTenantAsync~T~(tenantId) + } + + class EfCoreEnvelopeTransaction { + -DbContext _dbContext + -MessageContext _messaging + -IDomainEventScraper[] _scrapers + +PersistOutgoingAsync(Envelope) + +PersistIncomingAsync(Envelope) + +CommitAsync() + +RollbackAsync() + +TryMakeEagerIdempotencyCheckAsync() + } + + class TenantedDbContextBuilderByConnectionString~T~ { + -IServiceProvider _services + -MultiTenantedMessageStore _store + +BuildAndEnrollAsync(MessageContext, CancellationToken) + +BuildAsync(tenantId) + +ApplyAllChangesToDatabasesAsync() + } + + class TenantedDbContextBuilderByDbDataSource~T~ { + -IServiceProvider _services + -MultiTenantedMessageStore _store + +BuildAndEnrollAsync(MessageContext, CancellationToken) + +BuildAsync(tenantId) + +ApplyAllChangesToDatabasesAsync() + } + + %% ── Entity Models ─────────────────────────────────────────── + class IncomingMessage { + +Guid Id + +string Status + +int OwnerId + +DateTimeOffset ExecutionTime + +int Attempts + +byte[] Body + +string MessageType + +Uri ReceivedAt + +DateTimeOffset KeepUntil + } + + class OutgoingMessage { + +Guid Id + +int OwnerId + +Uri Destination + +DateTimeOffset DeliverBy + +byte[] Body + +int Attempts + +string MessageType + } + + %% ── Domain Events ─────────────────────────────────────────── + class OutgoingDomainEvents { + <> + } + + class OutgoingDomainEventsScraper { + -OutgoingDomainEvents _events + +ScrapeEvents(DbContext, MessageContext) + } + + class DomainEventScraper~T,TEvent~ { + -Func source + +ScrapeEvents(DbContext, MessageContext) + } + + %% ── Code Generation Frames ────────────────────────────────── + class EFCorePersistenceFrameProvider { + +CanPersist(entityType, chain, out dbContextType) + +DetermineLoadFrame() + +DetermineInsertFrame() + +DetermineUpdateFrame() + +DetermineDeleteFrame() + +CommitUnitOfWorkFrame() + +ApplyTransactionSupport() + } + + class EnrollDbContextInTransaction { + <> + -Type _dbContextType + } + + class StartDatabaseTransactionForDbContext { + <> + -Type _dbContextType + } + + class LoadEntityFrame { + <> + -Type _dbContextType + -Type _sagaType + } + + class DbContextOperationFrame { + <> + -string _methodName + } + + %% ── Bootstrap ─────────────────────────────────────────────── + class EntityFrameworkCoreBackedPersistence { + +Configure(WolverineOptions) + } + + class EntityFrameworkCoreBackedPersistence~T~ { + +Configure(WolverineOptions) + } + + class WolverineModelCustomizer { + +Customize(ModelBuilder, DbContext) + } + + class WolverineEntityCoreExtensions { + <> + +AddDbContextWithWolverineIntegration~T~()$ + +AddDbContextWithWolverineManagedMultiTenancy~T~()$ + +UseEntityFrameworkCoreTransactions()$ + +MapWolverineEnvelopeStorage()$ + +PublishDomainEventsFromEntityFrameworkCore()$ + } + + %% ── Inheritance / Implementation ──────────────────────────── + IDbContextOutbox --|> IMessageBus + IDbContextOutbox~T~ --|> IMessageBus + IDbContextBuilder~T~ --|> IDbContextBuilder + + DbContextOutbox --|> MessageContext + DbContextOutbox ..|> IDbContextOutbox + DbContextOutbox~T~ --|> MessageContext + DbContextOutbox~T~ ..|> IDbContextOutbox~T~ + + DbContextOutboxFactory ..|> IDbContextOutboxFactory + EfCoreEnvelopeTransaction ..|> IEnvelopeTransaction + EFCorePersistenceFrameProvider ..|> IPersistenceFrameProvider + EntityFrameworkCoreBackedPersistence ..|> IWolverineExtension + EntityFrameworkCoreBackedPersistence~T~ ..|> IWolverineExtension + + TenantedDbContextBuilderByConnectionString~T~ ..|> IDbContextBuilder~T~ + TenantedDbContextBuilderByDbDataSource~T~ ..|> IDbContextBuilder~T~ + + OutgoingDomainEventsScraper ..|> IDomainEventScraper + DomainEventScraper~T,TEvent~ ..|> IDomainEventScraper + + %% ── Composition / Dependencies ────────────────────────────── + DbContextOutbox~T~ --> EfCoreEnvelopeTransaction : creates + DbContextOutbox --> EfCoreEnvelopeTransaction : creates + EfCoreEnvelopeTransaction --> IncomingMessage : persists + EfCoreEnvelopeTransaction --> OutgoingMessage : persists + EfCoreEnvelopeTransaction --> IDomainEventScraper : invokes + + DbContextOutboxFactory --> IDbContextBuilder~T~ : resolves + DbContextOutboxFactory --> DbContextOutbox~T~ : creates + + EFCorePersistenceFrameProvider --> LoadEntityFrame : creates + EFCorePersistenceFrameProvider --> DbContextOperationFrame : creates + EFCorePersistenceFrameProvider --> EnrollDbContextInTransaction : creates + EFCorePersistenceFrameProvider --> StartDatabaseTransactionForDbContext : creates + + EntityFrameworkCoreBackedPersistence~T~ --> EFCorePersistenceFrameProvider : registers + WolverineModelCustomizer --> IncomingMessage : maps + WolverineModelCustomizer --> OutgoingMessage : maps +``` + +## Key Subsystems + +### 1. Transactional Outbox + +The core of the project is the transactional outbox, implemented by `EfCoreEnvelopeTransaction`. When a Wolverine message handler runs inside an EF Core transaction: + +1. The handler modifies entities on the `DbContext`. +2. Outgoing messages are captured as `OutgoingMessage` entities on the same `DbContext`. +3. The incoming message is recorded as an `IncomingMessage` (for idempotency). +4. On commit, everything is saved in a single database transaction. +5. After the transaction commits, outgoing messages are flushed to their transport destinations. + +If the `DbContext` has Wolverine's entity mappings (via `MapWolverineEnvelopeStorage()`), messages are persisted as EF Core entities. Otherwise, raw SQL commands are built using `DatabasePersistence` utilities from `Wolverine.RDBMS`. + +### 2. Outbox Entry Points + +There are two ways to use the outbox: + +- **`IDbContextOutbox`** (generic) — Injected when you know the `DbContext` type at compile time. Wraps a specific `DbContext` instance. +- **`IDbContextOutbox`** (non-generic) — Injected when the `DbContext` type isn't known until runtime. Call `Enroll(dbContext)` to attach any `DbContext`. + +Both extend `MessageContext`, so they function as full Wolverine message buses with outbox semantics. Calling `SaveChangesAndFlushMessagesAsync()` saves entity changes and flushes outgoing messages atomically. + +### 3. Code Generation + +Wolverine compiles message handler pipelines at startup using JasperFx code generation. The `EFCorePersistenceFrameProvider` plugs into this system: + +- **`EnrollDbContextInTransaction`** — Generated middleware frame for non-multi-tenanted DbContexts. Creates the `EfCoreEnvelopeTransaction`, begins a DB transaction, runs the handler, and commits or rolls back. +- **`StartDatabaseTransactionForDbContext`** — Generated middleware frame for multi-tenanted DbContexts. The DbContext is created via `IDbContextBuilder.BuildAndEnrollAsync()` earlier in the pipeline. +- **`LoadEntityFrame`** — Generates `await dbContext.FindAsync(id)` to load sagas. +- **`DbContextOperationFrame`** — Generates `dbContext.Add()`, `dbContext.Update()`, or `dbContext.Remove()` calls for saga persistence. + +### 4. Multi-Tenancy + +Two builder implementations resolve per-tenant DbContexts: + +- **`TenantedDbContextBuilderByConnectionString`** — Resolves a connection string per tenant from the `MultiTenantedMessageStore`, then constructs a `DbContext` with tenant-specific `DbContextOptions`. +- **`TenantedDbContextBuilderByDbDataSource`** — Same pattern but resolves a `DbDataSource` per tenant, used when integrating with Marten's data source management. + +Both builders cache resolved connections and use `FastExpressionCompiler` to compile efficient `DbContext` constructors at runtime. The `TenantedDbContextInitializer` hooks into Wolverine's resource management to apply migrations across all tenant databases at startup. + +### 5. Domain Event Publishing + +Domain events are extracted from entity changes before the transaction commits: + +- **`OutgoingDomainEventsScraper`** — Takes events from an `OutgoingDomainEvents` collection (populated manually by handler code) and enqueues them. +- **`DomainEventScraper`** — Scans the `ChangeTracker` for entities of type `T`, extracts events via a configured function, and publishes them. + +Scrapers implement `IDomainEventScraper` and are invoked by `EfCoreEnvelopeTransaction.CommitAsync()` just before the database transaction commits. + +### 6. EF Core Model Integration + +`WolverineModelCustomizer` extends EF Core's `RelationalModelCustomizer` to add entity mappings for `IncomingMessage` and `OutgoingMessage` to any `DbContext` that opts in via `MapWolverineEnvelopeStorage()`. This allows Wolverine's envelope storage tables to coexist with application tables in the same database and participate in the same EF Core migrations. + +## Transaction Flow + +### Non-Multi-Tenanted Handler + +``` +Request arrives + → EnrollDbContextInTransaction (generated middleware) + → Resolve DbContext from DI + → Create EfCoreEnvelopeTransaction + → Begin DB transaction + → [Optional] Eager idempotency check (insert IncomingMessage) + → Execute handler (modifies entities, sends messages) + → EfCoreEnvelopeTransaction.CommitAsync() + → Scrape domain events from ChangeTracker + → Persist OutgoingMessage entities + → Mark IncomingMessage as handled + → DbContext.SaveChangesAsync() + → DB transaction commits + → Flush outgoing messages to transports +``` + +### Multi-Tenanted Handler + +``` +Request arrives + → CreateTenantedDbContext (generated frame) + → IDbContextBuilder.BuildAndEnrollAsync() + → Resolve tenant connection string / data source + → Construct DbContext with tenant-specific options + → Create EfCoreEnvelopeTransaction and enroll in MessageContext + → StartDatabaseTransactionForDbContext (generated middleware) + → Begin DB transaction + → [Optional] Idempotency check + → Execute handler + → Commit transaction + → Flush outgoing messages +``` diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/IDbContextBuilder.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/IDbContextBuilder.cs index c7342bf47..5720e07d7 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/IDbContextBuilder.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/IDbContextBuilder.cs @@ -2,6 +2,7 @@ using JasperFx.CodeGeneration.Frames; using JasperFx.CodeGeneration.Model; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using Microsoft.EntityFrameworkCore; using Wolverine.Runtime; @@ -20,6 +21,10 @@ public interface IDbContextBuilder Task ApplyAllChangesToDatabasesAsync(); Task EnsureAllDatabasesAreCreatedAsync(); + + Task> FindAllAsync(); + + DatabaseCardinality Cardinality { get; } } public interface IDbContextBuilder : IDbContextBuilder where T : DbContext diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/Migrations/EntityFrameworkCoreSystemPart.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/Migrations/EntityFrameworkCoreSystemPart.cs new file mode 100644 index 000000000..ff19ad75c --- /dev/null +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/Migrations/EntityFrameworkCoreSystemPart.cs @@ -0,0 +1,122 @@ +using JasperFx; +using JasperFx.CommandLine; +using JasperFx.CommandLine.Descriptions; +using JasperFx.Core.Reflection; +using JasperFx.Descriptors; +using JasperFx.Environment; +using JasperFx.Resources; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Weasel.Core.CommandLine; +using Weasel.Core.Migrations; +using Weasel.EntityFrameworkCore; + +namespace Wolverine.EntityFrameworkCore.Internals.Migrations; + +public class EntityFrameworkCoreSystemPart : ISystemPart, IDatabaseSource +{ + private readonly IServiceContainer _container; + private readonly JasperFxOptions _options; + private readonly IDbContextBuilder[] _sources; + + public EntityFrameworkCoreSystemPart(IServiceContainer container, JasperFxOptions options) + { + _container = container; + _options = options; + + _sources = container.Services.GetServices().ToArray(); + + Cardinality = _sources.Length switch + { + 1 => _sources[0].Cardinality, + > 1 => _sources.Any(x => x.Cardinality == DatabaseCardinality.DynamicMultiple) + ? DatabaseCardinality.DynamicMultiple + : DatabaseCardinality.StaticMultiple, + _ => DatabaseCardinality.Single + }; + } + + public string Title { get; } = "Entity Core Framework"; + public Uri SubjectUri { get; } = new Uri("efcore://"); + public async Task WriteToConsole() + { + var databases = await BuildDatabases(); + + var description = OptionsDescription.For(this); + description + .AddChildSet("DbContexts") + .Rows + .AddRange(databases.Select(x => x.Describe())); + + OptionDescriptionWriter.Write(description); + } + + public async ValueTask> FindResources() + { + var databases = await BuildDatabases(); + var resources = databases.Select(x => new DatabaseResource(x, new Uri("efcore://"))).ToList(); + + return resources; + } + + public async Task AssertEnvironmentAsync(IServiceProvider services, EnvironmentCheckResults results, CancellationToken token) + { + var databases = await BuildDatabases(); + foreach (var database in databases) + { + try + { + await database.AssertConnectivityAsync(token); + results.RegisterSuccess("Able to connect to " + database.Describe().DatabaseUri()); + } + catch (Exception e) + { + results.RegisterFailure("Unable to connect to " + database.Describe().DatabaseUri(), e); + } + } + } + + public DatabaseCardinality Cardinality { get; } + public async ValueTask DescribeDatabasesAsync(CancellationToken token) + { + var databases = await BuildDatabases(); + return new DatabaseUsage + { + Cardinality = Cardinality, + MainDatabase = databases.Count == 1 ? databases[0].Describe() : null, + Databases = databases.Select(x => x.Describe()).ToList() + }; + } + + public async ValueTask> BuildDatabases() + { + var dbContextTypes = _container + .FindMatchingServices(type => type.CanBeCastTo()).Where(x => !x.IsKeyedService).Select(x => x.ServiceType).ToArray(); + var list = new List(); + + using var scope = _container.GetInstance().CreateScope(); + + foreach (var dbContextType in dbContextTypes) + { + var matching = _sources.FirstOrDefault(x => x.DbContextType == dbContextType); + if (matching == null) + { + var context = (DbContext)scope.ServiceProvider.GetRequiredService(dbContextType); + var database = _container.Services.CreateDatabase(context, dbContextType.FullNameInCode()); + list.Add(database); + } + else + { + var contexts = await matching.FindAllAsync(); + foreach (var dbContext in contexts) + { + await dbContext.Database.EnsureCreatedAsync(); + var database = _container.Services.CreateDatabase(dbContext, dbContextType.FullNameInCode()); + list.Add(database); + } + } + } + + return list; + } +} \ No newline at end of file diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByConnectionString.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByConnectionString.cs index 02734e99f..2a0f80822 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByConnectionString.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByConnectionString.cs @@ -4,11 +4,12 @@ using JasperFx; using JasperFx.Core; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using JasperFx.MultiTenancy; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.DependencyInjection; +using Weasel.EntityFrameworkCore; +using Wolverine.EntityFrameworkCore.Internals.Migrations; using Wolverine.Persistence.Durability; using Wolverine.RDBMS; using Wolverine.Runtime; @@ -84,7 +85,7 @@ public async Task> BuildAllAsync() var list = new List(); list.Add((T)BuildForMain()); - await _store.Source.RefreshAsync(); + await _store.Source.RefreshLiteAsync(); foreach (var assignment in _store.Source.AllActiveByTenant()) { var dbContext = await BuildAsync(assignment.TenantId, CancellationToken.None); @@ -121,8 +122,11 @@ public async Task ApplyAllChangesToDatabasesAsync() foreach (var context in contexts) { - var migrator = context.Database.GetInfrastructure().GetRequiredService(); - await migrator.MigrateAsync(); + await context.Database.EnsureCreatedAsync(); + await using var migration = await _serviceProvider.CreateMigrationAsync(context, CancellationToken.None); + + // TODO -- add some logging here! + await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, CancellationToken.None); } } @@ -133,7 +137,7 @@ public async Task EnsureAllDatabasesAreCreatedAsync() foreach (var context in contexts) { // TODO -- let's put some debug logging here!!!! - await context.Database.MigrateAsync(); + await context.Database.EnsureCreatedAsync(); } } @@ -192,5 +196,12 @@ public DbContextOptions BuildOptionsForMain() } public Type DbContextType => typeof(T); - + + public async Task> FindAllAsync() + { + var all = await BuildAllAsync(); + return all; + } + + public DatabaseCardinality Cardinality => _store.Source.Cardinality; } \ No newline at end of file diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByDbDataSource.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByDbDataSource.cs index b15978b31..fedc89564 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByDbDataSource.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Internals/TenantedDbContextBuilderByDbDataSource.cs @@ -4,11 +4,13 @@ using ImTools; using JasperFx; using JasperFx.Core.Reflection; +using JasperFx.Descriptors; using JasperFx.MultiTenancy; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; +using Weasel.EntityFrameworkCore; using Wolverine.Persistence.Durability; using Wolverine.RDBMS; using Wolverine.Runtime; @@ -98,15 +100,37 @@ public async Task ApplyAllChangesToDatabasesAsync() public async Task EnsureAllDatabasesAreCreatedAsync() { - var contexts = await BuildAllAsync(); + var list = new List(); + list.Add((T)BuildForMain()); + + await _store.Source.RefreshLiteAsync(); + + foreach (var assignment in _store.Source.AllActiveByTenant()) + { + var dbContext = await BuildAsync(assignment.TenantId, CancellationToken.None); + list.Add(dbContext); + } + + // Filter out duplicates when multiple tenants address the same database + var contexts = list.GroupBy(x => x.Database.GetConnectionString()).Select(x => x.First()).ToList(); foreach (var context in contexts) { + await context.Database.EnsureCreatedAsync(); + await using var migration = await _serviceProvider.CreateMigrationAsync(context, CancellationToken.None); + await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, CancellationToken.None); // TODO -- let's put some debug logging here!!!! - await context.Database.MigrateAsync(); } } + public async Task> FindAllAsync() + { + var all = await BuildAllAsync(); + return all; + } + + public DatabaseCardinality Cardinality => _store.Source.Cardinality; + public async ValueTask BuildAsync(string tenantId, CancellationToken cancellationToken) { var connectionString = await findDataSource(tenantId); diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj b/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj index 352e8b08f..2e96735e3 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj @@ -12,13 +12,14 @@ + - - - + + + diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs b/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs index 88e7eb42d..1328fb3d0 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs +++ b/src/Persistence/Wolverine.EntityFrameworkCore/WolverineEntityCoreExtensions.cs @@ -1,5 +1,6 @@ using System.Data.Common; using JasperFx; +using JasperFx.CommandLine.Descriptions; using JasperFx.Core.Reflection; using JasperFx.MultiTenancy; using JasperFx.Resources; @@ -8,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Wolverine.EntityFrameworkCore.Internals; +using Wolverine.EntityFrameworkCore.Internals.Migrations; using Wolverine.Persistence.Durability; using Wolverine.RDBMS; using Wolverine.Runtime; @@ -193,6 +195,19 @@ public static void UseEntityFrameworkCoreTransactions(this WolverineOptions opti options.Include(); } + /// + /// Adds "It just works" support for Wolverine stateful resource support for your EF Core registrations + /// to build out the table structure implied by your DbContext model at development time. + /// + /// This ties in to IServiceCollection.AddResourceSetupAtStartup() and can be enabled or disabled by setting the + /// JasperFxOptions.Production.ResourceAutoCreate + /// + /// + public static void UseEntityFrameworkCoreWolverineManagedMigrations(this WolverineOptions options) + { + options.Services.AddSingleton(); + } + internal static bool IsWolverineEnabled(this DbContext dbContext) { return dbContext.Model.FindAnnotation(WolverineEnabled) != null; @@ -223,7 +238,7 @@ public static ModelBuilder MapWolverineEnvelopeStorage(this ModelBuilder modelBu eb.Property(x => x.Status).HasColumnName(DatabaseConstants.Status).IsRequired(); eb.Property(x => x.OwnerId).HasColumnName(DatabaseConstants.OwnerId).IsRequired(); eb.Property(x => x.ExecutionTime).HasColumnName(DatabaseConstants.ExecutionTime).HasDefaultValue(null); - eb.Property(x => x.Attempts).HasColumnName(DatabaseConstants.Attempts).HasDefaultValue(0); + eb.Property(x => x.Attempts).HasColumnName(DatabaseConstants.Attempts); eb.Property(x => x.Body).HasColumnName(DatabaseConstants.Body).IsRequired(); eb.Property(x => x.MessageType).HasColumnName(DatabaseConstants.MessageType).IsRequired(); eb.Property(x => x.ReceivedAt).HasColumnName(DatabaseConstants.ReceivedAt); @@ -241,7 +256,7 @@ public static ModelBuilder MapWolverineEnvelopeStorage(this ModelBuilder modelBu eb.Property(x => x.DeliverBy).HasColumnName(DatabaseConstants.DeliverBy); eb.Property(x => x.Body).HasColumnName(DatabaseConstants.Body).IsRequired(); - eb.Property(x => x.Attempts).HasColumnName(DatabaseConstants.Attempts).HasDefaultValue(0); + eb.Property(x => x.Attempts).HasColumnName(DatabaseConstants.Attempts); eb.Property(x => x.MessageType).HasColumnName(DatabaseConstants.MessageType).IsRequired(); }); diff --git a/src/Persistence/Wolverine.Marten/MartenMessageDatabaseSource.cs b/src/Persistence/Wolverine.Marten/MartenMessageDatabaseSource.cs index 59e5c4de5..0bc478582 100644 --- a/src/Persistence/Wolverine.Marten/MartenMessageDatabaseSource.cs +++ b/src/Persistence/Wolverine.Marten/MartenMessageDatabaseSource.cs @@ -110,7 +110,17 @@ public async ValueTask FindAsync(string tenantId) return store; } - public async Task RefreshAsync() + public Task RefreshAsync() + { + return RefreshAsync(true); + } + + public Task RefreshLiteAsync() + { + return RefreshAsync(false); + } + + public async Task RefreshAsync(bool withMigration) { var martenDatabases = await _store.Storage.AllDatabases(); foreach (var martenDatabase in martenDatabases) @@ -118,7 +128,7 @@ public async Task RefreshAsync() if (!_databases.Contains(martenDatabase.Identifier)) { var wolverineStore = createTenantWolverineStore(martenDatabase); - if (_runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) + if (withMigration && _runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) { await wolverineStore.Admin.MigrateAsync(); } diff --git a/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj b/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj index 03bfdcda9..c63e804fb 100644 --- a/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj +++ b/src/Persistence/Wolverine.Marten/Wolverine.Marten.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Persistence/Wolverine.Marten/WolverineOptionsMartenExtensions.cs b/src/Persistence/Wolverine.Marten/WolverineOptionsMartenExtensions.cs index 3432e0f83..5cf3c4814 100644 --- a/src/Persistence/Wolverine.Marten/WolverineOptionsMartenExtensions.cs +++ b/src/Persistence/Wolverine.Marten/WolverineOptionsMartenExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Npgsql; +using Weasel.Core; using Weasel.Core.Migrations; using Weasel.Postgresql; using Wolverine.Marten.Distribution; @@ -79,6 +80,8 @@ public static MartenServiceCollectionExtensions.MartenConfigurationExpression In configure?.Invoke(integration); } + expression.Services.AddSingleton(); + expression.Services.AddSingleton(); expression.Services.AddScoped(); diff --git a/src/Persistence/Wolverine.Postgresql/PostgresqlBackedPersistence.cs b/src/Persistence/Wolverine.Postgresql/PostgresqlBackedPersistence.cs index f49d2c800..1dcc7aab8 100644 --- a/src/Persistence/Wolverine.Postgresql/PostgresqlBackedPersistence.cs +++ b/src/Persistence/Wolverine.Postgresql/PostgresqlBackedPersistence.cs @@ -1,4 +1,4 @@ -using System.Data.Common; +using System.Data.Common; using JasperFx; using JasperFx.Core; using JasperFx.MultiTenancy; @@ -6,7 +6,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Npgsql; +using Weasel.Core; using Weasel.Core.Migrations; +using Weasel.Postgresql; using Wolverine.ErrorHandling; using Wolverine.Persistence.Durability; using Wolverine.Persistence.Sagas; @@ -201,6 +203,8 @@ public void Configure(WolverineOptions options) transportConfiguration(expression); } } + + options.Services.AddSingleton(); } public IMessageStore BuildMessageStore(IWolverineRuntime runtime) diff --git a/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs b/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs index acf508de2..dbcb0d29f 100644 --- a/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs +++ b/src/Persistence/Wolverine.Postgresql/PostgresqlMessageStore.cs @@ -21,6 +21,7 @@ using Wolverine.Runtime.Agents; using Wolverine.Runtime.WorkerQueues; using Wolverine.Transports; +using CascadeAction = Weasel.Postgresql.CascadeAction; using DbCommandBuilder = Weasel.Core.DbCommandBuilder; using Table = Weasel.Postgresql.Tables.Table; diff --git a/src/Persistence/Wolverine.Postgresql/PostgresqlTenantedMessageStore.cs b/src/Persistence/Wolverine.Postgresql/PostgresqlTenantedMessageStore.cs index 337452b7b..caa16c4a7 100644 --- a/src/Persistence/Wolverine.Postgresql/PostgresqlTenantedMessageStore.cs +++ b/src/Persistence/Wolverine.Postgresql/PostgresqlTenantedMessageStore.cs @@ -102,7 +102,17 @@ private PostgresqlMessageStore buildTenantStoreForDataSource(NpgsqlDataSource so return store; } - public async Task RefreshAsync() + public Task RefreshAsync() + { + return RefreshAsync(true); + } + + public Task RefreshLiteAsync() + { + return RefreshAsync(false); + } + + public async Task RefreshAsync(bool withMigration) { if (_persistence.ConnectionStringTenancy != null) { @@ -116,7 +126,7 @@ public async Task RefreshAsync() var store = buildTenantStoreForConnectionString(assignment.Value); store.TenantIds.Fill(assignment.TenantId); - if (_runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) + if (withMigration && _runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) { await store.Admin.MigrateAsync(); } diff --git a/src/Persistence/Wolverine.Postgresql/Wolverine.Postgresql.csproj b/src/Persistence/Wolverine.Postgresql/Wolverine.Postgresql.csproj index e24718784..615604179 100644 --- a/src/Persistence/Wolverine.Postgresql/Wolverine.Postgresql.csproj +++ b/src/Persistence/Wolverine.Postgresql/Wolverine.Postgresql.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Persistence/Wolverine.RDBMS/Wolverine.RDBMS.csproj b/src/Persistence/Wolverine.RDBMS/Wolverine.RDBMS.csproj index f327cfdb7..8e6243553 100644 --- a/src/Persistence/Wolverine.RDBMS/Wolverine.RDBMS.csproj +++ b/src/Persistence/Wolverine.RDBMS/Wolverine.RDBMS.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Persistence/Wolverine.SqlServer/Persistence/SqlServerMessageStore.cs b/src/Persistence/Wolverine.SqlServer/Persistence/SqlServerMessageStore.cs index 2c9c520e0..a3ca7e3d0 100644 --- a/src/Persistence/Wolverine.SqlServer/Persistence/SqlServerMessageStore.cs +++ b/src/Persistence/Wolverine.SqlServer/Persistence/SqlServerMessageStore.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Data.Common; using ImTools; using JasperFx; @@ -22,6 +22,7 @@ using Wolverine.SqlServer.Schema; using Wolverine.SqlServer.Util; using Wolverine.Transports; +using CascadeAction = Weasel.SqlServer.CascadeAction; using DbCommandBuilder = Weasel.Core.DbCommandBuilder; using Table = Weasel.SqlServer.Tables.Table; diff --git a/src/Persistence/Wolverine.SqlServer/SqlServerBackedPersistence.cs b/src/Persistence/Wolverine.SqlServer/SqlServerBackedPersistence.cs index d97682f4d..99b889533 100644 --- a/src/Persistence/Wolverine.SqlServer/SqlServerBackedPersistence.cs +++ b/src/Persistence/Wolverine.SqlServer/SqlServerBackedPersistence.cs @@ -1,4 +1,4 @@ -using System.Data.Common; +using System.Data.Common; using JasperFx; using JasperFx.CodeGeneration.Model; using JasperFx.Core; @@ -7,7 +7,9 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Weasel.Core; using Weasel.Core.Migrations; +using Weasel.SqlServer; using Wolverine.Persistence.Durability; using Wolverine.Persistence.Sagas; using Wolverine.RDBMS; @@ -174,6 +176,8 @@ public void Configure(WolverineOptions options) options.CodeGeneration.Sources.Add(new DatabaseBackedPersistenceMarker()); options.CodeGeneration.Sources.Add(new SagaStorageVariableSource()); + options.Services.AddSingleton(); + options.Services.AddSingleton(s => BuildMessageStore(s.GetRequiredService())); options.Services.AddSingleton(); diff --git a/src/Persistence/Wolverine.SqlServer/SqlServerTenantedMessageStore.cs b/src/Persistence/Wolverine.SqlServer/SqlServerTenantedMessageStore.cs index 660c9a3e6..ea6cc2a82 100644 --- a/src/Persistence/Wolverine.SqlServer/SqlServerTenantedMessageStore.cs +++ b/src/Persistence/Wolverine.SqlServer/SqlServerTenantedMessageStore.cs @@ -78,7 +78,17 @@ private SqlServerMessageStore buildTenantStoreForConnectionString(string connect return store; } - public async Task RefreshAsync() + public Task RefreshAsync() + { + return RefreshAsync(true); + } + + public Task RefreshLiteAsync() + { + return RefreshAsync(false); + } + + public async Task RefreshAsync(bool withMigration) { await _persistence.ConnectionStringTenancy.RefreshAsync(); @@ -90,7 +100,7 @@ public async Task RefreshAsync() var store = buildTenantStoreForConnectionString(assignment.Value); store.TenantIds.Fill(assignment.TenantId); - if (_runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) + if (withMigration && _runtime.Options.AutoBuildMessageStorageOnStartup != AutoCreate.None) { await store.Admin.MigrateAsync(); } diff --git a/src/Persistence/Wolverine.SqlServer/Wolverine.SqlServer.csproj b/src/Persistence/Wolverine.SqlServer/Wolverine.SqlServer.csproj index 0935772ce..d06c63a8b 100644 --- a/src/Persistence/Wolverine.SqlServer/Wolverine.SqlServer.csproj +++ b/src/Persistence/Wolverine.SqlServer/Wolverine.SqlServer.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Wolverine/AssemblyAttributes.cs b/src/Wolverine/AssemblyAttributes.cs index 763a65d71..af6376d1e 100644 --- a/src/Wolverine/AssemblyAttributes.cs +++ b/src/Wolverine/AssemblyAttributes.cs @@ -27,6 +27,8 @@ [assembly: InternalsVisibleTo("MySqlTests")] [assembly: InternalsVisibleTo("ScheduledJobTests")] [assembly: InternalsVisibleTo("Wolverine.RDBMS")] +[assembly: InternalsVisibleTo("Wolverine.SqlServer")] +[assembly: InternalsVisibleTo("Wolverine.Postgresql")] [assembly: InternalsVisibleTo("Wolverine.Marten")] [assembly: InternalsVisibleTo("Wolverine.EntityFrameworkCore")] [assembly: InternalsVisibleTo("Wolverine.Pulsar")] diff --git a/src/Wolverine/Persistence/Durability/IMessageStore.cs b/src/Wolverine/Persistence/Durability/IMessageStore.cs index f71f3ab14..6bdfebbfb 100644 --- a/src/Wolverine/Persistence/Durability/IMessageStore.cs +++ b/src/Wolverine/Persistence/Durability/IMessageStore.cs @@ -159,4 +159,5 @@ public AncillaryMessageStore(Type markerType, IMessageStore inner) public interface ITenantedMessageSource : ITenantedSource { + Task RefreshLiteAsync(); } \ No newline at end of file diff --git a/src/Wolverine/Wolverine.csproj b/src/Wolverine/Wolverine.csproj index 1e4727d63..88a5e114e 100644 --- a/src/Wolverine/Wolverine.csproj +++ b/src/Wolverine/Wolverine.csproj @@ -4,7 +4,7 @@ WolverineFx - + diff --git a/wolverine.sln b/wolverine.sln index 656d8b1c5..71ce3d3d7 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -340,6 +340,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlTests.LeaderElection", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RavenDbTests.LeaderElection", "src\Persistence\LeaderElection\RavenDbTests.LeaderElection\RavenDbTests.LeaderElection.csproj", "{366074CD-5E56-481E-A6CE-D7E9C19AFEDA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attached", "Attached", "{3B05C001-84E3-1311-E8D3-16CD1127E3D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Weasel.EntityFrameworkCore", "..\weasel\src\Weasel.EntityFrameworkCore\Weasel.EntityFrameworkCore.csproj", "{790A63AD-98A6-4842-8121-8A6B5E4BE212}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EFCore", "EFCore", "{4E69232F-1E78-4486-9406-EDB991DFDC9A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1898,6 +1904,18 @@ Global {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x64.Build.0 = Release|Any CPU {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x86.ActiveCfg = Release|Any CPU {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x86.Build.0 = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|Any CPU.Build.0 = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x64.ActiveCfg = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x64.Build.0 = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x86.ActiveCfg = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x86.Build.0 = Debug|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|Any CPU.ActiveCfg = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|Any CPU.Build.0 = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x64.ActiveCfg = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x64.Build.0 = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x86.ActiveCfg = Release|Any CPU + {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1934,7 +1952,7 @@ Global {32122415-00BB-4513-A30E-648FA5AC1C2E} = {77430826-FF01-4D14-A0B5-2B544872F252} {317567E6-9AEB-4ABD-80AA-55ADE710F01A} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B} {57DA3CBC-7337-4BE5-8737-DACD066EC043} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B} - {EFD8CCE3-BF02-4EA4-8859-DF02692E08C8} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} + {EFD8CCE3-BF02-4EA4-8859-DF02692E08C8} = {4E69232F-1E78-4486-9406-EDB991DFDC9A} {3D535F13-5F10-46B1-B68C-A37E6DBA8A9B} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B} {2348222B-ECFA-46EB-9956-E6500C1B7AF0} = {F429686D-BB41-4E1C-A84E-518F8A289AEF} {13882AAB-B2EC-48E0-8D11-20810DDB7516} = {F429686D-BB41-4E1C-A84E-518F8A289AEF} @@ -2001,7 +2019,7 @@ Global {4FA38CED-74C9-4969-83B2-6BD54F245E6C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} {DC18FDD3-2AD9-43C9-B8CC-850457FE1A07} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} {C8E1C3C8-4BF7-4D93-9959-A4DAA3859A0D} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C} - {229147BA-99A7-4761-BA5E-3E65277C9B8E} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} + {229147BA-99A7-4761-BA5E-3E65277C9B8E} = {4E69232F-1E78-4486-9406-EDB991DFDC9A} {F3ECE3DC-153D-4F8A-AC48-2396F2B46ED7} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} {D01C99ED-B735-4854-A07D-B55E506A1441} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} {4257ECCC-A172-4FC0-BE0D-A60FDE0C8A74} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} @@ -2054,6 +2072,8 @@ Global {CA42E468-E4F3-41D4-8999-FEF55D43BAFD} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} {F81E3302-5747-42DB-8185-BF14F5E5DDBB} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} {366074CD-5E56-481E-A6CE-D7E9C19AFEDA} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} + {790A63AD-98A6-4842-8121-8A6B5E4BE212} = {3B05C001-84E3-1311-E8D3-16CD1127E3D5} + {4E69232F-1E78-4486-9406-EDB991DFDC9A} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {30422362-0D90-4DBE-8C97-DD2B5B962768} From 53baab42f8c2c187bd8955949566e8d8b775fde4 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Tue, 10 Feb 2026 10:06:27 -0600 Subject: [PATCH 2/3] Replace local Weasel.EntityFrameworkCore ProjectReference with NuGet PackageReference 8.6.0 Co-Authored-By: Claude Opus 4.6 --- .../Wolverine.EntityFrameworkCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj b/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj index 2e96735e3..55d69c4ea 100644 --- a/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj +++ b/src/Persistence/Wolverine.EntityFrameworkCore/Wolverine.EntityFrameworkCore.csproj @@ -12,7 +12,7 @@ - + From 274f3bc86282d77469d15f7a7e954b95acf2a70b Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Tue, 10 Feb 2026 10:36:03 -0600 Subject: [PATCH 3/3] cleaned up project reference --- wolverine.sln | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/wolverine.sln b/wolverine.sln index 71ce3d3d7..23884b8a5 100644 --- a/wolverine.sln +++ b/wolverine.sln @@ -340,10 +340,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlTests.LeaderElection", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RavenDbTests.LeaderElection", "src\Persistence\LeaderElection\RavenDbTests.LeaderElection\RavenDbTests.LeaderElection.csproj", "{366074CD-5E56-481E-A6CE-D7E9C19AFEDA}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attached", "Attached", "{3B05C001-84E3-1311-E8D3-16CD1127E3D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Weasel.EntityFrameworkCore", "..\weasel\src\Weasel.EntityFrameworkCore\Weasel.EntityFrameworkCore.csproj", "{790A63AD-98A6-4842-8121-8A6B5E4BE212}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EFCore", "EFCore", "{4E69232F-1E78-4486-9406-EDB991DFDC9A}" EndProject Global @@ -1904,18 +1900,6 @@ Global {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x64.Build.0 = Release|Any CPU {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x86.ActiveCfg = Release|Any CPU {366074CD-5E56-481E-A6CE-D7E9C19AFEDA}.Release|x86.Build.0 = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|Any CPU.Build.0 = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x64.ActiveCfg = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x64.Build.0 = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x86.ActiveCfg = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Debug|x86.Build.0 = Debug|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|Any CPU.ActiveCfg = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|Any CPU.Build.0 = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x64.ActiveCfg = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x64.Build.0 = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x86.ActiveCfg = Release|Any CPU - {790A63AD-98A6-4842-8121-8A6B5E4BE212}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2072,7 +2056,6 @@ Global {CA42E468-E4F3-41D4-8999-FEF55D43BAFD} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} {F81E3302-5747-42DB-8185-BF14F5E5DDBB} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} {366074CD-5E56-481E-A6CE-D7E9C19AFEDA} = {C99DD42F-A3FF-6981-C32E-EABE15982CE0} - {790A63AD-98A6-4842-8121-8A6B5E4BE212} = {3B05C001-84E3-1311-E8D3-16CD1127E3D5} {4E69232F-1E78-4486-9406-EDB991DFDC9A} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution