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(