Skip to content

Commit 5b1c02e

Browse files
authored
[release/8.0] Ensure that, by convention, dependent properties have the same element type as principal properties (#32582)
* 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 * Qwerk
1 parent 7ee1aa2 commit 5b1c02e

File tree

8 files changed

+323
-11
lines changed

8 files changed

+323
-11
lines changed

All.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ The .NET Foundation licenses this file to you under the MIT license.
268268
<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEF4F00E20178B341995BD2EFE53739B5/@KeyIndexDefined">True</s:Boolean>
269269
<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileEF4F00E20178B341995BD2EFE53739B5/RelativePriority/@EntryValue">2</s:Double>
270270
<s:String x:Key="/Default/Environment/PerformanceGuide/SwitchBehaviour/=VsBulb/@EntryIndexedValue">DO_NOTHING</s:String>
271+
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
271272
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
272273
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
273274
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>

src/EFCore/Metadata/Conventions/ConventionSet.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,12 @@ public virtual void Add(IConvention convention)
905905
{
906906
PropertyRemovedConventions.Add(propertyRemovedConvention);
907907
}
908+
909+
if (!ElementTypeChangedConvention.UseOldBehavior32411
910+
&& convention is IPropertyElementTypeChangedConvention elementTypeChangedConvention)
911+
{
912+
PropertyElementTypeChangedConventions.Add(elementTypeChangedConvention);
913+
}
908914
}
909915

910916
/// <summary>

src/EFCore/Metadata/Conventions/ElementMappingConvention.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,15 @@ 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+
var elementType = property.ClrType.TryGetElementType(typeof(IEnumerable<>));
46+
if (ElementTypeChangedConvention.UseOldBehavior32411)
47+
{
48+
property.SetElementType(elementType);
49+
}
50+
else
51+
{
52+
property.Builder.SetElementType(elementType);
53+
}
4654
}
4755
}
4856

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
internal static readonly bool UseOldBehavior32411 =
15+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue32411", out var enabled32411) && enabled32411;
16+
17+
/// <summary>
18+
/// Creates a new instance of <see cref="ElementTypeChangedConvention" />.
19+
/// </summary>
20+
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
21+
public ElementTypeChangedConvention(ProviderConventionSetBuilderDependencies dependencies)
22+
{
23+
Dependencies = dependencies;
24+
}
25+
26+
/// <summary>
27+
/// Dependencies for this service.
28+
/// </summary>
29+
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }
30+
31+
/// <inheritdoc />
32+
public void ProcessPropertyElementTypeChanged(
33+
IConventionPropertyBuilder propertyBuilder,
34+
IElementType? newElementType,
35+
IElementType? oldElementType,
36+
IConventionContext<IElementType> context)
37+
{
38+
var keyProperty = propertyBuilder.Metadata;
39+
foreach (var key in keyProperty.GetContainingKeys())
40+
{
41+
var index = key.Properties.IndexOf(keyProperty);
42+
foreach (var foreignKey in key.GetReferencingForeignKeys())
43+
{
44+
var foreignKeyProperty = foreignKey.Properties[index];
45+
foreignKeyProperty.Builder.SetElementType(newElementType?.ClrType);
46+
}
47+
}
48+
}
49+
50+
/// <inheritdoc />
51+
public void ProcessForeignKeyAdded(
52+
IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext<IConventionForeignKeyBuilder> context)
53+
{
54+
var foreignKeyProperties = foreignKeyBuilder.Metadata.Properties;
55+
var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties;
56+
for (var i = 0; i < foreignKeyProperties.Count; i++)
57+
{
58+
foreignKeyProperties[i].Builder.SetElementType(principalKeyProperties[i].GetElementType()?.ClrType);
59+
}
60+
}
61+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ public virtual ConventionSet CreateConventionSet()
102102
conventionSet.Add(new RuntimeModelConvention(Dependencies));
103103
conventionSet.Add(new ElementMappingConvention(Dependencies));
104104

105+
if (!ElementTypeChangedConvention.UseOldBehavior32411)
106+
{
107+
conventionSet.Add(new ElementTypeChangedConvention(Dependencies));
108+
}
109+
105110
return conventionSet;
106111
}
107112

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)