diff --git a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs index d9b1f765868..db99c9607c4 100644 --- a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs @@ -42,7 +42,7 @@ void Validate(IConventionTypeBase typeBase) var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property); if (typeMapping is { ElementTypeMapping: not null }) { - property.SetElementType(property.ClrType.TryGetElementType(typeof(IEnumerable<>))); + property.Builder.SetElementType(property.ClrType.TryGetElementType(typeof(IEnumerable<>))); } } diff --git a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs new file mode 100644 index 00000000000..2139e9ee857 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that reacts to changes made to element types of primitive collections. +/// +/// +/// See Model building conventions for more information and examples. +/// +public class ElementTypeChangedConvention : IPropertyElementTypeChangedConvention, IForeignKeyAddedConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public ElementTypeChangedConvention(ProviderConventionSetBuilderDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public void ProcessPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType, + IConventionContext context) + { + var keyProperty = propertyBuilder.Metadata; + foreach (var key in keyProperty.GetContainingKeys()) + { + var index = key.Properties.IndexOf(keyProperty); + foreach (var foreignKey in key.GetReferencingForeignKeys()) + { + var foreignKeyProperty = foreignKey.Properties[index]; + foreignKeyProperty.Builder.SetElementType(newElementType?.ClrType); + } + } + } + + /// + public void ProcessForeignKeyAdded( + IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) + { + var foreignKeyProperties = foreignKeyBuilder.Metadata.Properties; + var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties; + for (var i = 0; i < foreignKeyProperties.Count; i++) + { + foreignKeyProperties[i].Builder.SetElementType(principalKeyProperties[i].GetElementType()?.ClrType); + } + } +} diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 43b3ec9adaa..f535fb4ed2e 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -101,6 +101,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.Add(new QueryFilterRewritingConvention(Dependencies)); conventionSet.Add(new RuntimeModelConvention(Dependencies)); conventionSet.Add(new ElementMappingConvention(Dependencies)); + conventionSet.Add(new ElementTypeChangedConvention(Dependencies)); return conventionSet; } diff --git a/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs index cc8b27c7fed..3de51ae6933 100644 --- a/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs @@ -146,6 +146,10 @@ public override void Can_insert_and_read_back_with_bare_class_key_and_optional_d public override void Can_insert_and_read_back_with_comparable_class_key_and_optional_dependents_with_shadow_FK() => base.Can_insert_and_read_back_with_comparable_class_key_and_optional_dependents_with_shadow_FK(); + [ConditionalFact(Skip = "Issue=#26239")] + public override void Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents() + => base.Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents(); + public class KeysWithConvertersCosmosFixture : KeysWithConvertersFixtureBase { protected override ITestStoreFactory TestStoreFactory @@ -537,6 +541,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.Id).HasConversion(GenericComparableIntClassKey.Converter); b.OwnsOne(e => e.Owned); }); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); } + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder.ConfigureWarnings(w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning))); } } diff --git a/test/EFCore.InMemory.FunctionalTests/KeysWithConvertersInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/KeysWithConvertersInMemoryTest.cs index cfa1936828f..8b6c30e5a97 100644 --- a/test/EFCore.InMemory.FunctionalTests/KeysWithConvertersInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/KeysWithConvertersInMemoryTest.cs @@ -35,9 +35,26 @@ public override void Can_query_and_update_owned_entity_with_value_converter() public override void Can_query_and_update_owned_entity_with_int_bare_class_key() => base.Can_query_and_update_owned_entity_with_int_bare_class_key(); + [ConditionalFact(Skip = "Issue #26238")] + public override void Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents() + => base.Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents(); + public class KeysWithConvertersInMemoryFixture : KeysWithConvertersFixtureBase { protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder.ConfigureWarnings(w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning))); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + // Issue #26238 + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + } } } diff --git a/test/EFCore.Specification.Tests/KeysWithConvertersTestBase.cs b/test/EFCore.Specification.Tests/KeysWithConvertersTestBase.cs index 596ac22c933..5ea27ece407 100644 --- a/test/EFCore.Specification.Tests/KeysWithConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/KeysWithConvertersTestBase.cs @@ -764,6 +764,105 @@ void Validate( d => ((IntClassKeyOptionalDependent)d).Principal); } + [ConditionalFact] + public virtual void Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents() + { + InsertOptionalGraph(); + + using (var context = CreateContext()) + { + RunQueries(context, out var principals, out var dependents); + + Validate( + principals, + dependents, + new[] { (0, new[] { 0 }), (1, new[] { 1 }), (2, new[] { 2, 2, 2 }), (3, new int[0]) }, + new (int, int?)[] { (0, 0), (1, 1), (2, 2), (3, 2), (4, 2), (5, null) }); + + foreach (var principal in principals) + { + principal.Foo = "Mutant!"; + } + + dependents[5].Principal = principals[0]; + dependents[4].PrincipalId = null; + dependents[3].PrincipalId = principals[0].Id; + principals[1].OptionalDependents.Clear(); + + context.Remove(dependents[0]); + principals[0].OptionalDependents.Add( + new EnumerableClassKeyOptionalDependent { Id = new EnumerableClassKey(dependents[0].Id.Id), }); + + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + RunQueries(context, out var principals, out var dependents); + + Validate( + principals, + dependents, + new[] { (0, new[] { 0, 3, 5 }), (1, new int[0]), (2, new[] { 2 }), (3, new int[0]) }, + new (int, int?)[] { (0, 0), (1, null), (2, 2), (3, 0), (4, null), (5, 0) }); + } + + void RunQueries( + DbContext context, + out EnumerableClassKeyPrincipal[] principals, + out EnumerableClassKeyOptionalDependent[] dependents) + { + var two = 2; + var three = new EnumerableClassKey(3); + + principals = new[] + { + context.Set().Include(e => e.OptionalDependents) + .Single(e => e.Id.Equals(new EnumerableClassKey(1))), + context.Set().Include(e => e.OptionalDependents) + .Single(e => e.Id.Equals(new EnumerableClassKey(two))), + context.Set().Include(e => e.OptionalDependents).Single(e => e.Id.Equals(three)), + context.Set().Include(e => e.OptionalDependents) + .Single(e => e.Id.Equals(new EnumerableClassKey(4))) + }; + + var oneOhTwo = 102; + var oneOhThree = new EnumerableClassKey(103); + var oneOhFive = 105; + var oneOhSix = new EnumerableClassKey(106); + + dependents = new[] + { + context.Set().Single(e => e.Id.Equals(new EnumerableClassKey(101))), + context.Set().Single(e => e.Id.Equals(new EnumerableClassKey(oneOhTwo))), + context.Set().Single(e => e.Id.Equals(oneOhThree)), + context.Set().Single(e => e.Id == new EnumerableClassKey(104)), + context.Set().Single(e => e.Id == new EnumerableClassKey(oneOhFive)), + context.Set().Single(e => e.Id == oneOhSix) + }; + + Assert.Same(dependents[0], context.Set().Find(new EnumerableClassKey(101))); + Assert.Same(dependents[1], context.Set().Find(new EnumerableClassKey(oneOhTwo))); + Assert.Same(dependents[2], context.Set().Find(oneOhThree)); + Assert.Same(dependents[3], context.Find(new EnumerableClassKey(104))); + Assert.Same(dependents[4], context.Find(new EnumerableClassKey(oneOhFive))); + Assert.Same(dependents[5], context.Find(oneOhSix)); + } + + void Validate( + EnumerableClassKeyPrincipal[] principals, + EnumerableClassKeyOptionalDependent[] dependents, + (int, int[])[] expectedPrincipalToDependents, + (int, int?)[] expectedDependentToPrincipals) + => ValidateOptional( + principals, + dependents, + expectedPrincipalToDependents, + expectedDependentToPrincipals, + p => ((EnumerableClassKeyPrincipal)p).OptionalDependents.Select(d => (IIntOptionalDependent)d).ToList(), + d => ((EnumerableClassKeyOptionalDependent)d).Principal); + } + [ConditionalFact] public virtual void Can_insert_and_read_back_with_bare_class_key_and_optional_dependents() { @@ -5581,6 +5680,36 @@ public override int GetHashCode() public int Id { get; set; } } + protected class EnumerableClassKey : IEnumerable + { + public static ValueConverter Converter + = new(v => v.Id, v => new EnumerableClassKey(v)); + + public EnumerableClassKey(int id) + { + Id = id; + } + + public IEnumerator GetEnumerator() + => throw new NotImplementedException(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + protected bool Equals(EnumerableClassKey other) + => other != null && Id == other.Id; + + public override bool Equals(object obj) + => obj == this + || obj?.GetType() == GetType() + && Equals((IntClassKey)obj); + + public override int GetHashCode() + => Id; + + public int Id { get; set; } + } + protected class BareIntClassKey { public static ValueConverter Converter @@ -6152,6 +6281,63 @@ public int BackingPrincipalId } } + protected class EnumerableClassKeyPrincipal : IIntPrincipal + { + public EnumerableClassKey Id { get; set; } + public string Foo { get; set; } + public ICollection OptionalDependents { get; set; } + public ICollection RequiredDependents { get; set; } + + [NotMapped] + public int BackingId + { + get => Id.Id; + set => Id = new EnumerableClassKey(value); + } + } + + protected class EnumerableClassKeyOptionalDependent : IIntOptionalDependent + { + public EnumerableClassKey Id { get; set; } + public EnumerableClassKey PrincipalId { get; set; } + public EnumerableClassKeyPrincipal Principal { get; set; } + + [NotMapped] + public int BackingId + { + get => Id.Id; + set => Id = new EnumerableClassKey(value); + } + + [NotMapped] + public int? BackingPrincipalId + { + get => PrincipalId?.Id; + set => PrincipalId = value.HasValue ? new EnumerableClassKey(value.Value) : null; + } + } + + protected class EnumerableClassKeyRequiredDependent : IIntRequiredDependent + { + public EnumerableClassKey Id { get; set; } + public EnumerableClassKey PrincipalId { get; set; } + public EnumerableClassKeyPrincipal Principal { get; set; } + + [NotMapped] + public int BackingId + { + get => Id.Id; + set => Id = new EnumerableClassKey(value); + } + + [NotMapped] + public int BackingPrincipalId + { + get => PrincipalId.Id; + set => PrincipalId = new EnumerableClassKey(value); + } + } + protected class BareIntClassKeyPrincipal : IIntPrincipal { public BareIntClassKey Id { get; set; } @@ -6887,14 +7073,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b => { b.Property(e => e.Id).HasConversion(IntStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(IntStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(IntStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(IntStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( @@ -6914,6 +7100,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.PrincipalId).HasConversion(IntClassKey.Converter); }); + modelBuilder.Entity( + b => { b.Property(e => e.Id).HasConversion(EnumerableClassKey.Converter); }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).HasConversion(EnumerableClassKey.Converter); + b.Property(e => e.PrincipalId); + }); + + modelBuilder.Entity( + b => + { + b.Property(e => e.Id).HasConversion(EnumerableClassKey.Converter); + b.Property(e => e.PrincipalId); + }); + modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(BareIntClassKey.Converter, BareIntClassKey.Comparer); }); @@ -6938,14 +7141,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b => { b.Property(e => e.Id).HasConversion(ComparableIntStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableIntStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(ComparableIntStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableIntStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( @@ -6972,14 +7175,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b => { b.Property(e => e.Id).HasConversion(StructuralComparableBytesStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(StructuralComparableBytesStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(StructuralComparableBytesStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(StructuralComparableBytesStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( @@ -7006,14 +7209,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b => { b.Property(e => e.Id).HasConversion(ComparableBytesStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableBytesStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(ComparableBytesStructKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableBytesStructKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( @@ -7040,14 +7243,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b => { b.Property(e => e.Id).HasConversion(ComparableIntClassKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableIntClassKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity( b => { b.Property(e => e.Id).HasConversion(ComparableIntClassKey.Converter); - b.Property(e => e.PrincipalId).HasConversion(ComparableIntClassKey.Converter); + b.Property(e => e.PrincipalId); }); modelBuilder.Entity(