From 6a69f5991429edef0c3f37e293ea9e7fcefbefc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:04:55 +0000 Subject: [PATCH 01/13] Initial plan From ec4a241dbac5042a6de46c83f04a2ef5c70b5e7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:26:09 +0000 Subject: [PATCH 02/13] Add ModelValidator check for FK/PK provider type match Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Infrastructure/ModelValidator.cs | 41 +++++++++++++++++++ .../Infrastructure/ModelValidatorTest.cs | 24 +++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index e0a7e8f743c..7058158f948 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -71,6 +71,47 @@ public virtual void Validate(IModel model, IDiagnosticsLogger + /// Validates that the types of the foreign key properties match the types of the principal key properties. + /// + /// + /// This validation is intentionally not run for model snapshots, which are initialized without a validation + /// logger and may store properties using their provider types to round-trip mismatched keys. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateForeignKeys( + IModel model, + IDiagnosticsLogger logger) + { + foreach (var entityType in model.GetEntityTypes()) + { + foreach (var foreignKey in entityType.GetDeclaredForeignKeys()) + { + var dependentProperties = foreignKey.Properties; + var principalProperties = foreignKey.PrincipalKey.Properties; + for (var i = 0; i < dependentProperties.Count; i++) + { + if (GetProviderClrType(dependentProperties[i]) != GetProviderClrType(principalProperties[i])) + { + throw new InvalidOperationException( + CoreStrings.ForeignKeyTypeMismatch( + dependentProperties.Format(includeTypes: true), + foreignKey.DeclaringEntityType.DisplayName(), + principalProperties.Format(includeTypes: true), + foreignKey.PrincipalEntityType.DisplayName())); + } + } + } + } + + static Type GetProviderClrType(IReadOnlyProperty property) + => (property.FindTypeMapping()?.Converter?.ProviderClrType + ?? property.ClrType).UnwrapNullableType(); } private static void ValidateNoIdentifyingRelationshipCycles(Multigraph graph) diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 275a48afaea..33fca6a5adb 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -592,6 +592,30 @@ public virtual void Warns_on_double_uniquified_shadow_key_due_to_wrong_type() modelBuilder); } + [Fact] + public virtual void Passes_on_foreign_key_with_matching_provider_type() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity().HasOne().WithMany().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); + + Validate(modelBuilder); + } + + [Fact] + public virtual void Detects_foreign_key_with_mismatched_provider_type() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity().HasOne().WithMany().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().Property(a => a.P0).HasConversion(); + + VerifyError( + CoreStrings.ForeignKeyTypeMismatch( + "{'P0' : int?}", nameof(A), "{'Id' : int}", nameof(B)), + modelBuilder); + } + [Fact] public virtual void Detects_shadow_key_referenced_by_foreign_key_by_convention() { From 78e7066418fa9c7c4902da7d728f82814b9f2ca0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:40:44 +0000 Subject: [PATCH 03/13] Update relational tests for new FK/PK provider type validation Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../RelationalModelValidatorTest.cs | 4 ++-- .../Storage/RelationalTypeMappingTest.cs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 0856a99610f..ec25b031d3a 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1126,8 +1126,8 @@ public virtual void Detects_incompatible_shared_columns_in_shared_table_with_dif modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); VerifyError( - RelationalStrings.DuplicateColumnNameProviderTypeMismatch( - nameof(A), nameof(A.P0), nameof(B), nameof(B.P0), nameof(B.P0), "Table", "long", "int"), + CoreStrings.ForeignKeyTypeMismatch( + "{'P0' : int?}", nameof(A), "{'Id' : int}", nameof(B)), modelBuilder); } diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs index 6f35a15d84b..79c8f8ecc0b 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs @@ -586,13 +586,19 @@ private class FruityContext(DbContextOptions options) : DbContext(options) } [Fact] - public virtual void Primary_key_type_mapping_can_differ_from_FK() + public virtual void Primary_key_type_mapping_cannot_differ_from_FK() { - using var context = new MismatchedFruityContext(ContextOptions); + var exception = Assert.Throws( + () => + { + using var context = new MismatchedFruityContext(ContextOptions); + _ = context.Model; + }); + Assert.Equal( - typeof(short), - context.Model.FindEntityType(typeof(Banana)).FindProperty("Id").GetTypeMapping().Converter.ProviderClrType); - Assert.Null(context.Model.FindEntityType(typeof(Kiwi)).FindProperty("Id").GetTypeMapping().Converter); + CoreStrings.ForeignKeyTypeMismatch( + "{'Id' : int}", nameof(Kiwi), "{'Id' : int}", nameof(Banana)), + exception.Message); } private class MismatchedFruityContext(DbContextOptions options) : FruityContext(options) From 1dd2d26c34f3ceaec3439d1cd7d0eeeeee00dccc Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 17 Jun 2026 17:37:10 -0700 Subject: [PATCH 04/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 33fca6a5adb..991bee688ee 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -598,6 +598,8 @@ public virtual void Passes_on_foreign_key_with_matching_provider_type() var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().HasOne().WithMany().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().Property(a => a.P0).HasConversion(); + modelBuilder.Entity().Property(b => b.Id).HasConversion(); Validate(modelBuilder); } From 18b7554b989a39090c3f0207735ce41dfb0fb6d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:59:57 +0000 Subject: [PATCH 05/13] Align FK type validation with model CLR types and fix snapshot key type roundtrip Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/CSharpSnapshotGenerator.cs | 4 ++- src/EFCore/Infrastructure/ModelValidator.cs | 9 +---- ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 35 +++++++++++++++++++ .../RelationalModelValidatorTest.cs | 4 +-- .../Storage/RelationalTypeMappingTest.cs | 16 +++------ .../Infrastructure/ModelValidatorTest.cs | 18 ++-------- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 1421672eedd..ca347a90651 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -465,7 +465,9 @@ protected virtual void GenerateProperty( IProperty property, CSharpSnapshotGeneratorParameters parameters) { - var clrType = (FindValueConverter(property)?.ProviderClrType ?? property.ClrType) + var clrType = ((property.IsKey() || property.IsForeignKey()) + ? property.ClrType + : FindValueConverter(property)?.ProviderClrType ?? property.ClrType) .MakeNullable(property.IsNullable); var propertyCall = property.IsPrimitiveCollection ? "PrimitiveCollection" : "Property"; diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 7058158f948..685e3d48f73 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -78,10 +78,6 @@ public virtual void Validate(IModel model, IDiagnosticsLogger /// Validates that the types of the foreign key properties match the types of the principal key properties. /// - /// - /// This validation is intentionally not run for model snapshots, which are initialized without a validation - /// logger and may store properties using their provider types to round-trip mismatched keys. - /// /// The model to validate. /// The logger to use. protected virtual void ValidateForeignKeys( @@ -96,7 +92,7 @@ protected virtual void ValidateForeignKeys( var principalProperties = foreignKey.PrincipalKey.Properties; for (var i = 0; i < dependentProperties.Count; i++) { - if (GetProviderClrType(dependentProperties[i]) != GetProviderClrType(principalProperties[i])) + if (dependentProperties[i].ClrType.UnwrapNullableType() != principalProperties[i].ClrType.UnwrapNullableType()) { throw new InvalidOperationException( CoreStrings.ForeignKeyTypeMismatch( @@ -109,9 +105,6 @@ protected virtual void ValidateForeignKeys( } } - static Type GetProviderClrType(IReadOnlyProperty property) - => (property.FindTypeMapping()?.Converter?.ProviderClrType - ?? property.ClrType).UnwrapNullableType(); } private static void ValidateNoIdentifyingRelationshipCycles(Multigraph graph) diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index ccc47ab78c3..892700157d6 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -121,6 +121,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) Assert.Equal(2, snapshot.Model.GetEntityTypes().Count()); } + [Fact] + public void Snapshot_uses_model_types_for_key_properties_with_converters() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.HasDefaultSchema("DefaultSchema"); + modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); + + modelBuilder.Entity( + "Principal", + b => + { + b.Property("Id").HasConversion().HasColumnType("smallint"); + b.HasKey("Id"); + }); + modelBuilder.Entity( + "Dependent", + b => + { + b.Property("Id").HasConversion().HasColumnType("bigint"); + b.HasKey("Id"); + b.HasOne("Principal").WithMany().HasForeignKey("Id"); + }); + + var model = modelBuilder.FinalizeModel(designTime: true); + var code = CreateMigrationsGenerator().GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model); + var snapshotModel = BuildModelFromSnapshotSource(code); + + Assert.Equal( + typeof(int), + snapshotModel.FindEntityType("Principal")!.FindProperty("Id")!.ClrType); + Assert.Equal( + typeof(int), + snapshotModel.FindEntityType("Dependent")!.FindProperty("Id")!.ClrType); + } + [Fact] public void Snapshot_with_migration_id() { diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index ec25b031d3a..0856a99610f 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1126,8 +1126,8 @@ public virtual void Detects_incompatible_shared_columns_in_shared_table_with_dif modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); VerifyError( - CoreStrings.ForeignKeyTypeMismatch( - "{'P0' : int?}", nameof(A), "{'Id' : int}", nameof(B)), + RelationalStrings.DuplicateColumnNameProviderTypeMismatch( + nameof(A), nameof(A.P0), nameof(B), nameof(B.P0), nameof(B.P0), "Table", "long", "int"), modelBuilder); } diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs index 79c8f8ecc0b..6f35a15d84b 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs @@ -586,19 +586,13 @@ private class FruityContext(DbContextOptions options) : DbContext(options) } [Fact] - public virtual void Primary_key_type_mapping_cannot_differ_from_FK() + public virtual void Primary_key_type_mapping_can_differ_from_FK() { - var exception = Assert.Throws( - () => - { - using var context = new MismatchedFruityContext(ContextOptions); - _ = context.Model; - }); - + using var context = new MismatchedFruityContext(ContextOptions); Assert.Equal( - CoreStrings.ForeignKeyTypeMismatch( - "{'Id' : int}", nameof(Kiwi), "{'Id' : int}", nameof(Banana)), - exception.Message); + typeof(short), + context.Model.FindEntityType(typeof(Banana)).FindProperty("Id").GetTypeMapping().Converter.ProviderClrType); + Assert.Null(context.Model.FindEntityType(typeof(Kiwi)).FindProperty("Id").GetTypeMapping().Converter); } private class MismatchedFruityContext(DbContextOptions options) : FruityContext(options) diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 991bee688ee..eb8bf76b96c 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -593,31 +593,17 @@ public virtual void Warns_on_double_uniquified_shadow_key_due_to_wrong_type() } [Fact] - public virtual void Passes_on_foreign_key_with_matching_provider_type() + public virtual void Passes_on_foreign_key_with_matching_model_type_and_mismatched_provider_type() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity().HasOne().WithMany().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); modelBuilder.Entity().Property(a => a.P0).HasConversion(); - modelBuilder.Entity().Property(b => b.Id).HasConversion(); + modelBuilder.Entity().Property(b => b.Id).HasConversion(); Validate(modelBuilder); } - [Fact] - public virtual void Detects_foreign_key_with_mismatched_provider_type() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder.Entity().HasOne().WithMany().HasForeignKey(a => a.P0).HasPrincipalKey(b => b.Id); - modelBuilder.Entity().Property(a => a.P0).HasConversion(); - - VerifyError( - CoreStrings.ForeignKeyTypeMismatch( - "{'P0' : int?}", nameof(A), "{'Id' : int}", nameof(B)), - modelBuilder); - } - [Fact] public virtual void Detects_shadow_key_referenced_by_foreign_key_by_convention() { From 09d8a764f5ce4d6437b372c82ec3a06a83baff20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:05:31 +0000 Subject: [PATCH 06/13] Apply review feedback for FK type validation and snapshot handling Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Migrations/Design/CSharpSnapshotGenerator.cs | 9 ++++++--- .../CSharpMigrationsGeneratorTest.ModelSnapshot.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index ca347a90651..92c2f38ed0b 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -465,9 +465,7 @@ protected virtual void GenerateProperty( IProperty property, CSharpSnapshotGeneratorParameters parameters) { - var clrType = ((property.IsKey() || property.IsForeignKey()) - ? property.ClrType - : FindValueConverter(property)?.ProviderClrType ?? property.ClrType) + var clrType = GetPropertyClrTypeForSnapshot(property) .MakeNullable(property.IsNullable); var propertyCall = property.IsPrimitiveCollection ? "PrimitiveCollection" : "Property"; @@ -546,6 +544,11 @@ protected virtual void GenerateProperty( private ValueConverter? FindValueConverter(IProperty property) => property.GetTypeMapping().Converter; + private Type GetPropertyClrTypeForSnapshot(IProperty property) + => property.IsKey() || property.IsForeignKey() + ? property.ClrType + : FindValueConverter(property)?.ProviderClrType ?? property.ClrType; + /// /// Returns the filtered annotations for the given property, augmented with derived /// store-mapping annotations (column type, default value, column name) before filtering so diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index 892700157d6..918baadaa8c 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -122,7 +122,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) } [Fact] - public void Snapshot_uses_model_types_for_key_properties_with_converters() + public void Snapshot_uses_model_types_for_key_and_foreign_key_properties_with_converters() { var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.HasDefaultSchema("DefaultSchema"); From fae0c4e98638b01b19a3a42cee14d74e9388c999 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:19:00 +0000 Subject: [PATCH 07/13] Fix snapshot compatibility for mismatched FK/PK property types Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/CSharpSnapshotGenerator.cs | 7 +-- src/EFCore/Metadata/Internal/ForeignKey.cs | 19 +++++++ ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 57 ++++++++++++------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 92c2f38ed0b..1421672eedd 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -465,7 +465,7 @@ protected virtual void GenerateProperty( IProperty property, CSharpSnapshotGeneratorParameters parameters) { - var clrType = GetPropertyClrTypeForSnapshot(property) + var clrType = (FindValueConverter(property)?.ProviderClrType ?? property.ClrType) .MakeNullable(property.IsNullable); var propertyCall = property.IsPrimitiveCollection ? "PrimitiveCollection" : "Property"; @@ -544,11 +544,6 @@ protected virtual void GenerateProperty( private ValueConverter? FindValueConverter(IProperty property) => property.GetTypeMapping().Converter; - private Type GetPropertyClrTypeForSnapshot(IProperty property) - => property.IsKey() || property.IsForeignKey() - ? property.ClrType - : FindValueConverter(property)?.ProviderClrType ?? property.ClrType; - /// /// Returns the filtered annotations for the given property, augmented with derived /// store-mapping annotations (column type, default value, column name) before filtering so diff --git a/src/EFCore/Metadata/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index d0f5ad88e54..a5d3aae4f2d 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -1210,6 +1210,11 @@ public static bool AreCompatible( if (!ArePropertyTypesCompatible(principalProperties, dependentProperties)) { + if (IsModelSnapshotCompatibilityMode(principalEntityType, dependentEntityType)) + { + return true; + } + if (shouldThrow) { throw new InvalidOperationException( @@ -1226,6 +1231,20 @@ public static bool AreCompatible( return true; } + private static bool IsModelSnapshotCompatibilityMode( + IReadOnlyEntityType principalEntityType, + IReadOnlyEntityType dependentEntityType) + { + if (principalEntityType.Model is not Model model + || !ReferenceEquals(model, dependentEntityType.Model)) + { + return false; + } + + return model.ScopedModelDependencies == null + && model.FindAnnotation(CoreAnnotationNames.ProductVersion) != null; + } + private static bool ArePropertyCountsEqual( IReadOnlyList principalProperties, IReadOnlyList dependentProperties) diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index 918baadaa8c..01a721bde3b 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -122,38 +122,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) } [Fact] - public void Snapshot_uses_model_types_for_key_and_foreign_key_properties_with_converters() + public void Snapshot_with_mismatched_key_and_foreign_key_property_types_is_usable() { - var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.HasDefaultSchema("DefaultSchema"); - modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); + const string snapshotCode = + """ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +#nullable disable + +namespace RootNamespace; + +partial class Snapshot : ModelSnapshot +{ + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0"); - modelBuilder.Entity( - "Principal", - b => + modelBuilder.Entity("Dependent", b => { - b.Property("Id").HasConversion().HasColumnType("smallint"); + b.Property("Id") + .HasColumnType("bigint"); + b.HasKey("Id"); + + b.HasOne("Principal") + .WithMany() + .HasForeignKey("Id"); }); - modelBuilder.Entity( - "Dependent", - b => + + modelBuilder.Entity("Principal", b => { - b.Property("Id").HasConversion().HasColumnType("bigint"); + b.Property("Id") + .HasColumnType("smallint"); + b.HasKey("Id"); - b.HasOne("Principal").WithMany().HasForeignKey("Id"); }); + } +} - var model = modelBuilder.FinalizeModel(designTime: true); - var code = CreateMigrationsGenerator().GenerateSnapshot("RootNamespace", typeof(DbContext), "Snapshot", model); - var snapshotModel = BuildModelFromSnapshotSource(code); +"""; - Assert.Equal( - typeof(int), - snapshotModel.FindEntityType("Principal")!.FindProperty("Id")!.ClrType); - Assert.Equal( - typeof(int), - snapshotModel.FindEntityType("Dependent")!.FindProperty("Id")!.ClrType); + var snapshotModel = BuildModelFromSnapshotSource(snapshotCode); + + Assert.Single(snapshotModel.FindEntityType("Dependent")!.GetForeignKeys()); } [Fact] From 9a01263912b313f7052c481ae2bb807de3b2fd56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:19:49 +0000 Subject: [PATCH 08/13] Document snapshot compatibility detection in FK type check Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Metadata/Internal/ForeignKey.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EFCore/Metadata/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index a5d3aae4f2d..29935b75719 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -1241,6 +1241,8 @@ private static bool IsModelSnapshotCompatibilityMode( return false; } + // Snapshot models are built with parameterless ModelBuilder (no scoped dependencies) and set ProductVersion explicitly. + // Allowing type mismatches here preserves compatibility with existing snapshots that captured provider CLR types for FK/PK properties. return model.ScopedModelDependencies == null && model.FindAnnotation(CoreAnnotationNames.ProductVersion) != null; } From 57816e2d716dc45fb1b9db6fd811301374eaa346 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:22:27 +0000 Subject: [PATCH 09/13] Rename snapshot compatibility helper to Model Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Metadata/Internal/ForeignKey.cs | 20 +++----------------- src/EFCore/Metadata/Internal/Model.cs | 10 ++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/EFCore/Metadata/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index 29935b75719..c328ec450c0 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -1210,7 +1210,9 @@ public static bool AreCompatible( if (!ArePropertyTypesCompatible(principalProperties, dependentProperties)) { - if (IsModelSnapshotCompatibilityMode(principalEntityType, dependentEntityType)) + if (principalEntityType.Model is Model model + && ReferenceEquals(model, dependentEntityType.Model) + && model.IsInModelSnapshot) { return true; } @@ -1231,22 +1233,6 @@ public static bool AreCompatible( return true; } - private static bool IsModelSnapshotCompatibilityMode( - IReadOnlyEntityType principalEntityType, - IReadOnlyEntityType dependentEntityType) - { - if (principalEntityType.Model is not Model model - || !ReferenceEquals(model, dependentEntityType.Model)) - { - return false; - } - - // Snapshot models are built with parameterless ModelBuilder (no scoped dependencies) and set ProductVersion explicitly. - // Allowing type mismatches here preserves compatibility with existing snapshots that captured provider CLR types for FK/PK properties. - return model.ScopedModelDependencies == null - && model.FindAnnotation(CoreAnnotationNames.ProductVersion) != null; - } - private static bool ArePropertyCountsEqual( IReadOnlyList principalProperties, IReadOnlyList dependentProperties) diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index dab7ec6d556..a6530758931 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -102,6 +102,16 @@ public virtual ModelDependencies? ScopedModelDependencies set => _scopedModelDependencies = value; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsInModelSnapshot + => ScopedModelDependencies == null + && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; + /// /// Indicates whether the model is read-only. /// From b32eb36a626a795b287d41f3d81d391455054119 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:28:47 +0000 Subject: [PATCH 10/13] Move snapshot detection helper to Model Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Metadata/Internal/Model.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index a6530758931..8b9780d8af4 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -103,11 +103,14 @@ public virtual ModelDependencies? ScopedModelDependencies } /// + /// Gets a value indicating whether this model originated from a migration snapshot. + /// + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// + /// public virtual bool IsInModelSnapshot => ScopedModelDependencies == null && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; From fcf33bec4ed43a49bebd4829988ac871f3025bcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:38:48 +0000 Subject: [PATCH 11/13] Revert redundant foreign key type check from ModelValidator Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/Infrastructure/ModelValidator.cs | 34 --------------------- 1 file changed, 34 deletions(-) diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 685e3d48f73..e0a7e8f743c 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -71,40 +71,6 @@ public virtual void Validate(IModel model, IDiagnosticsLogger - /// Validates that the types of the foreign key properties match the types of the principal key properties. - /// - /// The model to validate. - /// The logger to use. - protected virtual void ValidateForeignKeys( - IModel model, - IDiagnosticsLogger logger) - { - foreach (var entityType in model.GetEntityTypes()) - { - foreach (var foreignKey in entityType.GetDeclaredForeignKeys()) - { - var dependentProperties = foreignKey.Properties; - var principalProperties = foreignKey.PrincipalKey.Properties; - for (var i = 0; i < dependentProperties.Count; i++) - { - if (dependentProperties[i].ClrType.UnwrapNullableType() != principalProperties[i].ClrType.UnwrapNullableType()) - { - throw new InvalidOperationException( - CoreStrings.ForeignKeyTypeMismatch( - dependentProperties.Format(includeTypes: true), - foreignKey.DeclaringEntityType.DisplayName(), - principalProperties.Format(includeTypes: true), - foreignKey.PrincipalEntityType.DisplayName())); - } - } - } - } - } private static void ValidateNoIdentifyingRelationshipCycles(Multigraph graph) From 97ab8039cf182975d81fcab0fac557beec8ae75a Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 17 Jun 2026 21:23:15 -0700 Subject: [PATCH 12/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/EFCore/Metadata/Internal/Model.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 8b9780d8af4..c82b84eefb1 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -111,9 +111,10 @@ public virtual ModelDependencies? ScopedModelDependencies /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModelSnapshot - => ScopedModelDependencies == null - && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; +public virtual bool IsInModelSnapshot + => ScopedModelDependencies == null + && _modelFinalizedConventions is { Count: 0 } + && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; /// /// Indicates whether the model is read-only. From 77b7d954c272ea3af9b33e9f2ef788eae39c410f Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 17 Jun 2026 21:36:04 -0700 Subject: [PATCH 13/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/EFCore/Metadata/Internal/Model.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index c82b84eefb1..a1de99b8fcb 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -111,10 +111,10 @@ public virtual ModelDependencies? ScopedModelDependencies /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public virtual bool IsInModelSnapshot - => ScopedModelDependencies == null - && _modelFinalizedConventions is { Count: 0 } - && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; + public virtual bool IsInModelSnapshot + => ScopedModelDependencies == null + && _modelFinalizedConventions is { Count: 0 } + && FindAnnotation(CoreAnnotationNames.ProductVersion) != null; /// /// Indicates whether the model is read-only.