diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs index b3250c3..7fa4c42 100644 --- a/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs +++ b/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs @@ -47,18 +47,19 @@ public static ModelBuilder MapStringEnumAsPostgresEnum(this ModelBuilder foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { - if (entityType.ClrType is null) - continue; - - var entityBuilder = modelBuilder.Entity(entityType.ClrType); - foreach (var property in entityType.GetProperties()) { if (property.ClrType != typeof(TEnum)) continue; - // No HasConversion — see PropertyBuilderExtensions.HasPostgresStringEnum for why. - entityBuilder.Property(property.Name).HasColumnType(columnType); + // Mutate IMutableProperty directly instead of going through modelBuilder.Entity(...).Property(...). + // The latter re-enters EF's entity-discovery conventions, which will probe every CLR property + // on the entity (and on any reference-typed nested types they pull in). For an entity whose + // complex-collection element exposes e.g. Dictionary with a user-supplied HasConversion, + // that probing fires before the user's fluent config has been applied — and EF then throws + // "Unable to determine the relationship represented by navigation ...". No HasConversion call + // here either — see PropertyBuilderExtensions.HasPostgresStringEnum for why. + property.SetColumnType(columnType); } } diff --git a/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/ModelBuilderExtensionsTests.cs b/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/ModelBuilderExtensionsTests.cs index 57adbad..b4df357 100644 --- a/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/ModelBuilderExtensionsTests.cs +++ b/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/ModelBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Xunit; namespace StrEnum.Npgsql.EntityFrameworkCore.UnitTests; @@ -94,4 +95,72 @@ public void MapStringEnumAsPostgresEnum_QualifiesColumnTypeWithSchema() sportProperty.GetColumnType().Should().Be("races.sport"); } + + public class Stage + { + public Dictionary? Splits { get; set; } + } + + public class Schedule + { + public List Stages { get; set; } = new(); + } + + public class Tournament + { + public Guid Id { get; set; } + public Schedule Schedule { get; set; } = new(); + public Sport Sport { get; set; } = null!; + } + + private sealed class DictionaryToStringConverter : ValueConverter?, string> + { + public DictionaryToStringConverter() + : base(d => "", s => new Dictionary()) + { + } + } + + private sealed class TournamentContext : DbContext + { + public DbSet Tournaments => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseNpgsql("Host=localhost;Database=tests") + .UseStringEnums() + .ReplaceService(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Walker call FIRST, before the entity is configured — matches the failing consumer order. + modelBuilder.MapStringEnumAsPostgresEnum(); + + modelBuilder.Entity(t => + { + t.ComplexProperty(x => x.Schedule, schedule => + { + schedule.ToJson("schedule"); + schedule.ComplexCollection(s => s.Stages, stage => + { + stage.Property(s => s.Splits).HasConversion(new DictionaryToStringConverter()); + }); + }); + }); + } + + public IModel DesignTimeModel => this.GetService().Model; + } + + [Fact] + public void MapStringEnumAsPostgresEnum_DoesNotProbeCustomConvertedComplexCollectionProperties() + { + using var context = new TournamentContext(); + + var act = () => context.DesignTimeModel; + + act.Should().NotThrow(); + } }