From fe4231e94e13537b22b53ade5ca0440c2fa2998c Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 23 Mar 2026 11:30:54 -0500 Subject: [PATCH 1/5] Add tests for EF Core FK dependency ordering (issue #4180) Tests verify that GetEntityTypesForMigration returns entity types in topological order based on FK dependencies, and that schema migration applies without "relation does not exist" errors when tables have FK relationships. See https://github.com/JasperFx/marten/issues/4180 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EfCoreSchemaTests.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs index 9becfdb2a4..921bf9809b 100644 --- a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs +++ b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs @@ -1,8 +1,12 @@ using System; using System.Linq; +using System.Threading.Tasks; +using JasperFx; using Marten.Testing.Harness; using Microsoft.EntityFrameworkCore; +using Npgsql; using Shouldly; +using Weasel.EntityFrameworkCore; using Weasel.Postgresql.Tables; using Xunit; @@ -126,4 +130,94 @@ public void should_move_tables_without_explicit_schema_to_marten_schema() orderSummariesTable.Identifier.Schema.ShouldBe(martenSchema, "Tables without explicit schema should be moved to Marten's schema"); } + + [Fact] + public void should_return_entity_types_in_fk_dependency_order() + { + // Issue #4180: GetEntityTypesForMigration should return entity types sorted + // so that referenced tables come before referencing tables. + var builder = new DbContextOptionsBuilder(); + builder.UseNpgsql("Host=localhost"); + + using var dbContext = new SeparateSchemaDbContext(builder.Options); + + var entityTypes = DbContextExtensions.GetEntityTypesForMigration(dbContext); + var names = entityTypes.Select(e => e.GetTableName()).ToList(); + + // entity_type must come before entity because entity has a FK to entity_type + var entityTypeIndex = names.IndexOf("entity_type"); + var entityIndex = names.IndexOf("entity"); + + entityTypeIndex.ShouldBeGreaterThanOrEqualTo(0); + entityIndex.ShouldBeGreaterThanOrEqualTo(0); + entityTypeIndex.ShouldBeLessThan(entityIndex, + "entity_type should come before entity due to FK dependency (issue #4180)"); + } + + [Fact] + public async Task should_apply_schema_with_fk_dependencies_without_error() + { + // Issue #4180: End-to-end test proving that Marten can apply schema changes + // for EF Core entities with FK dependencies without table ordering errors. + const string testSchema = "ef_fk_order_test"; + + // Clean up any previous test schema + await using var cleanupConn = new NpgsqlConnection(ConnectionSource.ConnectionString); + await cleanupConn.OpenAsync(); + await using (var cmd = cleanupConn.CreateCommand()) + { + cmd.CommandText = $"DROP SCHEMA IF EXISTS {testSchema} CASCADE"; + await cmd.ExecuteNonQueryAsync(); + } + await cleanupConn.CloseAsync(); + + try + { + await using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = testSchema; + opts.AutoCreateSchemaObjects = AutoCreate.All; + + opts.AddEntityTablesFromDbContext(b => + { + b.UseNpgsql("Host=localhost"); + }); + }); + + // This triggers schema creation - should not throw due to FK ordering + await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); + + // Verify tables were created by querying the information schema + await using var verifyConn = new NpgsqlConnection(ConnectionSource.ConnectionString); + await verifyConn.OpenAsync(); + await using var verifyCmd = verifyConn.CreateCommand(); + verifyCmd.CommandText = @" + SELECT table_name FROM information_schema.tables + WHERE table_schema = @schema + ORDER BY table_name"; + verifyCmd.Parameters.AddWithValue("schema", SeparateSchemaDbContext.EfSchema); + + var tables = new System.Collections.Generic.List(); + await using var reader = await verifyCmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + tables.Add(reader.GetString(0)); + } + + tables.ShouldContain("entity"); + tables.ShouldContain("entity_type"); + } + finally + { + // Clean up + await using var finalConn = new NpgsqlConnection(ConnectionSource.ConnectionString); + await finalConn.OpenAsync(); + await using var finalCmd = finalConn.CreateCommand(); + finalCmd.CommandText = $"DROP SCHEMA IF EXISTS {testSchema} CASCADE"; + await finalCmd.ExecuteNonQueryAsync(); + finalCmd.CommandText = $"DROP SCHEMA IF EXISTS {SeparateSchemaDbContext.EfSchema} CASCADE"; + await finalCmd.ExecuteNonQueryAsync(); + } + } } From 148b370c1426e5cdeb610d33162f7230b5004ddd Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 23 Mar 2026 11:37:01 -0500 Subject: [PATCH 2/5] Add NoEfSchemaDbContext and FK schema test for issue #4192 When EF Core entities have no explicit schema and are moved to Marten's schema, FK references must also point to Marten's schema. This test verifies the fix in Weasel's MoveToSchema method. See https://github.com/JasperFx/marten/issues/4192 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EfCoreSchemaTests.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs index 921bf9809b..3a55fd4ae2 100644 --- a/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs +++ b/src/Marten.EntityFrameworkCore.Tests/EfCoreSchemaTests.cs @@ -70,6 +70,40 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } +/// +/// DbContext with no explicit schema - these entities should be moved to the +/// Marten document store schema, including their FK references. +/// See https://github.com/JasperFx/marten/issues/4192 +/// +public class NoEfSchemaDbContext : DbContext +{ + public NoEfSchemaDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Entities => Set(); + public DbSet EntityTypes => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.ToTable("entity_type"); + entity.HasKey(e => e.Id); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("entity"); + entity.HasKey(e => e.Id); + entity.HasOne(e => e.EntityType) + .WithMany() + .HasForeignKey(e => e.EntityTypeId) + .OnDelete(DeleteBehavior.Cascade); + }); + } +} + public class EfCoreSchemaTests { [Fact] @@ -220,4 +254,43 @@ SELECT table_name FROM information_schema.tables await finalCmd.ExecuteNonQueryAsync(); } } + + [Fact] + public void should_move_tables_and_fk_without_explicit_schema_to_marten_schema() + { + // Issue #4192: When EF Core entities do NOT have an explicit schema, + // tables AND their FK references should be moved to Marten's schema. + const string martenSchema = "test_marten_schema"; + + using var store = DocumentStore.For(opts => + { + opts.Connection(ConnectionSource.ConnectionString); + opts.DatabaseSchemaName = martenSchema; + + // NoEfSchemaDbContext does not set an explicit schema + opts.AddEntityTablesFromDbContext(); + }); + + var extendedObjects = store.Options.Storage.ExtendedSchemaObjects; + + var entityTable = extendedObjects.OfType() + .FirstOrDefault(t => t.Identifier.Name == "entity"); + var entityTypeTable = extendedObjects.OfType
() + .FirstOrDefault(t => t.Identifier.Name == "entity_type"); + + entityTable.ShouldNotBeNull(); + entityTypeTable.ShouldNotBeNull(); + + // These tables should be moved to Marten's schema + entityTable.Identifier.Schema.ShouldBe(martenSchema, + "Entity table should be moved to Marten's schema"); + entityTypeTable.Identifier.Schema.ShouldBe(martenSchema, + "EntityType table should be moved to Marten's schema"); + + // FK references should also point to Marten's schema + var entityFk = entityTable.ForeignKeys.FirstOrDefault(); + entityFk.ShouldNotBeNull("Entity should have a FK to EntityType"); + entityFk.LinkedTable!.Schema.ShouldBe(martenSchema, + "Foreign keys should also be updated to reference the correct schema"); + } } From fea76a3022a8fc8e6144f0c6720c9d81e528a6cc Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 23 Mar 2026 11:59:43 -0500 Subject: [PATCH 3/5] Add tests for EventProjection auto-discovery of document types (issue #4166) Tests verify that document types used in operations.Store() within an explicit ApplyAsync override, and return types from Create methods, are automatically registered as published types by the source generator. See https://github.com/JasperFx/marten/issues/4166 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...ojection_should_register_document_types.cs | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs diff --git a/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs b/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs new file mode 100644 index 0000000000..df6351087a --- /dev/null +++ b/src/EventSourcingTests/Projections/event_projection_should_register_document_types.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JasperFx.Events; +using JasperFx.Events.Projections; +using Marten; +using Marten.Events.Projections; +using Marten.Testing.Harness; +using Shouldly; +using Xunit; + +namespace EventSourcingTests.Projections; + +/// +/// Document type used in an EventProjection but NOT explicitly registered with Marten. +/// The source generator should discover this type from Store/Insert calls in ApplyAsync +/// and register it automatically. +/// See https://github.com/JasperFx/marten/issues/4166 +/// +public class AuditRecord +{ + public Guid Id { get; set; } + public Guid StreamId { get; set; } + public string EventType { get; set; } = string.Empty; + public DateTimeOffset Timestamp { get; set; } +} + +public class AuditableEvent +{ + public string Description { get; set; } = string.Empty; +} + +/// +/// An EventProjection with an explicit ApplyAsync override that stores a document type +/// using operations.Store<T>(). The source generator should detect this and emit a +/// constructor that registers AuditRecord as a published type. +/// See https://github.com/JasperFx/marten/issues/4166 +/// +public partial class AuditRecordProjection : EventProjection +{ + public override ValueTask ApplyAsync(IDocumentOperations operations, IEvent e, CancellationToken cancellation) + { + switch (e.Data) + { + case AuditableEvent: + operations.Store(new AuditRecord + { + Id = Guid.NewGuid(), + StreamId = e.StreamId, + EventType = e.Data.GetType().Name, + Timestamp = e.Timestamp + }); + break; + } + + return new ValueTask(); + } +} + +/// +/// An EventProjection with conventional Create method that returns a document type. +/// The source generator should register this type via the emitted constructor. +/// +public partial class AuditRecordCreatorProjection : EventProjection +{ + public AuditRecord Create(AuditableEvent e) => new AuditRecord + { + Id = Guid.NewGuid(), + EventType = nameof(AuditableEvent) + }; +} + +public class event_projection_should_register_document_types : OneOffConfigurationsContext +{ + [Fact] + public void explicit_apply_async_projection_should_register_document_types() + { + // Issue #4166: Document types used in operations.Store() inside an explicit + // ApplyAsync override should be automatically discovered and registered. + StoreOptions(opts => + { + opts.Projections.Add(new AuditRecordProjection(), ProjectionLifecycle.Inline); + }); + + var documentTypes = theStore.StorageFeatures.AllDocumentMappings + .Select(x => x.DocumentType).ToList(); + + documentTypes.ShouldContain(typeof(AuditRecord), + "AuditRecord should be auto-discovered from operations.Store() in ApplyAsync"); + } + + [Fact] + public void conventional_create_projection_should_register_document_types() + { + // EventProjection with Create method should also register the return type. + StoreOptions(opts => + { + opts.Projections.Add(new AuditRecordCreatorProjection(), ProjectionLifecycle.Inline); + }); + + var documentTypes = theStore.StorageFeatures.AllDocumentMappings + .Select(x => x.DocumentType).ToList(); + + documentTypes.ShouldContain(typeof(AuditRecord), + "AuditRecord should be auto-discovered from Create method return type"); + } +} From ead3781669bfd70457967569aaffb4d019170ed3 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 23 Mar 2026 13:01:03 -0500 Subject: [PATCH 4/5] Add reproduction test for mixed-case table unique index issue Reproduces the user's scenario from Weasel issue #224 where a FlatTableProjection with a mixed-case table name and unique index fails with "relation already exists" on the second migration run. See https://github.com/JasperFx/weasel/issues/224 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...g_4224_unique_index_on_mixed_case_table.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs diff --git a/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs b/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs new file mode 100644 index 0000000000..09516b998d --- /dev/null +++ b/src/CoreTests/Bugs/Bug_4224_unique_index_on_mixed_case_table.cs @@ -0,0 +1,72 @@ +using System; +using System.Threading.Tasks; +using JasperFx; +using JasperFx.Events.Projections; +using Marten; +using Marten.Events.Projections.Flattened; +using Marten.Testing.Harness; +using Weasel.Core; +using Weasel.Postgresql.Tables; +using Xunit; + +namespace CoreTests.Bugs; + +/// +/// Reproduces https://github.com/JasperFx/weasel/issues/224 +/// A FlatTableProjection with a mixed-case table name and a unique index +/// should not fail on the second migration run with "relation already exists". +/// +public class Bug_4224_unique_index_on_mixed_case_table : BugIntegrationContext +{ + [Fact] + public async Task should_not_fail_on_second_migration_with_unique_index() + { + // Matches the user's exact scenario from the issue + StoreOptions(options => + { + options.DatabaseSchemaName = "bug_4224_marten"; + options.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate; + + options.Projections.Add(ProjectionLifecycle.Inline); + }); + + // First migration - creates schema, table, and index + await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); + + // Second migration - should detect existing index and not try to recreate it + await theStore.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); + } +} + +public class TestProjector : FlatTableProjection +{ + public TestProjector() + : base(new DbObjectName("bug_4224", "TestRecords")) + { + Table.AddColumn("id").AsPrimaryKey(); + Table.AddColumn("fake_id"); + Table.AddColumn("year"); + Table.AddColumn("month"); + + Table.Indexes.Add(new IndexDefinition("ix_test_records_unique") + { + Columns = ["fake_id", "year", "month"], + IsUnique = true + }); + + // Need at least one event handler for validation + Project(map => + { + map.Map(x => x.FakeId, "fake_id"); + map.Map(x => x.Year, "year"); + map.Map(x => x.Month, "month"); + }); + } +} + +public class TestEvent +{ + public Guid FakeId { get; set; } + public int Year { get; set; } + public int Month { get; set; } +} From 4fdcf49d5a6cec1b71ad211b399a6b92a0fa1283 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Mon, 23 Mar 2026 13:25:02 -0500 Subject: [PATCH 5/5] Update JasperFx.* and Weasel.* packages to latest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JasperFx 1.21.1 → 1.21.3 - JasperFx.Events 1.24.0 → 1.24.1 - Weasel.Postgresql 8.9.1 → 8.10.1 - Weasel.EntityFrameworkCore 8.9.1 → 8.10.1 Weasel 8.10.1 includes fixes for: - Topological sort of EF Core entity types by FK dependencies (#4180) - MoveToSchema updating FK LinkedTable references (#4192) - Index detection for mixed-case table names (weasel#224) Co-Authored-By: Claude Opus 4.6 (1M context) --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 00d7fcfd7a..c677d54663 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,9 +13,9 @@ - - - + + + @@ -60,8 +60,8 @@ - - + +