Skip to content

Commit fd576b9

Browse files
authored
Ensure that, by convention, dependent properties have the same element type as principal properties (#32560)
* Ensure that, by convention, dependent properties have the same element type as principal properties Fixes #32411 The issue here is that when a value converter is applied to a principal property, then that converter is used by the dependent properties unless something else is explicitly configured. However, this meant that when the PK has a converter but the FK does not, then the FK can get configured as a primitive collection, since it doesn't have a converter preventing this. The fix is to add a convention that sets the element type for dependent properties to match that for principal properties. * Update based on review
1 parent d1124fa commit fd576b9

File tree

6 files changed

+301
-11
lines changed

6 files changed

+301
-11
lines changed

src/EFCore/Metadata/Conventions/ElementMappingConvention.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void Validate(IConventionTypeBase typeBase)
4242
var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property);
4343
if (typeMapping is { ElementTypeMapping: not null })
4444
{
45-
property.SetElementType(property.ClrType.TryGetElementType(typeof(IEnumerable<>)));
45+
property.Builder.SetElementType(property.ClrType.TryGetElementType(typeof(IEnumerable<>)));
4646
}
4747
}
4848

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
5+
6+
/// <summary>
7+
/// A convention that reacts to changes made to element types of primitive collections.
8+
/// </summary>
9+
/// <remarks>
10+
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
11+
/// </remarks>
12+
public class ElementTypeChangedConvention : IPropertyElementTypeChangedConvention, IForeignKeyAddedConvention
13+
{
14+
/// <summary>
15+
/// Creates a new instance of <see cref="ElementTypeChangedConvention" />.
16+
/// </summary>
17+
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
18+
public ElementTypeChangedConvention(ProviderConventionSetBuilderDependencies dependencies)
19+
{
20+
Dependencies = dependencies;
21+
}
22+
23+
/// <summary>
24+
/// Dependencies for this service.
25+
/// </summary>
26+
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
27+
28+
/// <inheritdoc />
29+
public void ProcessPropertyElementTypeChanged(
30+
IConventionPropertyBuilder propertyBuilder,
31+
IElementType? newElementType,
32+
IElementType? oldElementType,
33+
IConventionContext<IElementType> context)
34+
{
35+
var keyProperty = propertyBuilder.Metadata;
36+
foreach (var key in keyProperty.GetContainingKeys())
37+
{
38+
var index = key.Properties.IndexOf(keyProperty);
39+
foreach (var foreignKey in key.GetReferencingForeignKeys())
40+
{
41+
var foreignKeyProperty = foreignKey.Properties[index];
42+
foreignKeyProperty.Builder.SetElementType(newElementType?.ClrType);
43+
}
44+
}
45+
}
46+
47+
/// <inheritdoc />
48+
public void ProcessForeignKeyAdded(
49+
IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext<IConventionForeignKeyBuilder> context)
50+
{
51+
var foreignKeyProperties = foreignKeyBuilder.Metadata.Properties;
52+
var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties;
53+
for (var i = 0; i < foreignKeyProperties.Count; i++)
54+
{
55+
foreignKeyProperties[i].Builder.SetElementType(principalKeyProperties[i].GetElementType()?.ClrType);
56+
}
57+
}
58+
}

src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public virtual ConventionSet CreateConventionSet()
101101
conventionSet.Add(new QueryFilterRewritingConvention(Dependencies));
102102
conventionSet.Add(new RuntimeModelConvention(Dependencies));
103103
conventionSet.Add(new ElementMappingConvention(Dependencies));
104+
conventionSet.Add(new ElementTypeChangedConvention(Dependencies));
104105

105106
return conventionSet;
106107
}

test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ public override void Can_insert_and_read_back_with_bare_class_key_and_optional_d
146146
public override void Can_insert_and_read_back_with_comparable_class_key_and_optional_dependents_with_shadow_FK()
147147
=> base.Can_insert_and_read_back_with_comparable_class_key_and_optional_dependents_with_shadow_FK();
148148

149+
[ConditionalFact(Skip = "Issue=#26239")]
150+
public override void Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents()
151+
=> base.Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents();
152+
149153
public class KeysWithConvertersCosmosFixture : KeysWithConvertersFixtureBase
150154
{
151155
protected override ITestStoreFactory TestStoreFactory
@@ -537,6 +541,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
537541
b.Property(e => e.Id).HasConversion(GenericComparableIntClassKey.Converter);
538542
b.OwnsOne(e => e.Owned);
539543
});
544+
545+
modelBuilder.Ignore<EnumerableClassKeyPrincipal>();
546+
modelBuilder.Ignore<EnumerableClassKeyOptionalDependent>();
547+
modelBuilder.Ignore<EnumerableClassKeyRequiredDependent>();
540548
}
549+
550+
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
551+
=> base.AddOptions(builder.ConfigureWarnings(w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning)));
541552
}
542553
}

test/EFCore.InMemory.FunctionalTests/KeysWithConvertersInMemoryTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,26 @@ public override void Can_query_and_update_owned_entity_with_value_converter()
3535
public override void Can_query_and_update_owned_entity_with_int_bare_class_key()
3636
=> base.Can_query_and_update_owned_entity_with_int_bare_class_key();
3737

38+
[ConditionalFact(Skip = "Issue #26238")]
39+
public override void Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents()
40+
=> base.Can_insert_and_read_back_with_enumerable_class_key_and_optional_dependents();
41+
3842
public class KeysWithConvertersInMemoryFixture : KeysWithConvertersFixtureBase
3943
{
4044
protected override ITestStoreFactory TestStoreFactory
4145
=> InMemoryTestStoreFactory.Instance;
46+
47+
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
48+
=> base.AddOptions(builder.ConfigureWarnings(w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning)));
49+
50+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
51+
{
52+
base.OnModelCreating(modelBuilder, context);
53+
54+
// Issue #26238
55+
modelBuilder.Ignore<EnumerableClassKeyPrincipal>();
56+
modelBuilder.Ignore<EnumerableClassKeyOptionalDependent>();
57+
modelBuilder.Ignore<EnumerableClassKeyRequiredDependent>();
58+
}
4259
}
4360
}

0 commit comments

Comments
 (0)