From 51f85b0e53355d1b360c6b07c612e8f5301bf254 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:39:59 +0000 Subject: [PATCH 1/6] Make element type a creation-time concern; remove SetElementType APIs and convention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../EFCore.Relational.baseline.json | 26 ++--- .../Metadata/RelationalMemberClassifier.cs | 4 +- src/EFCore/EFCore.baseline.json | 48 ++++----- .../Builders/IConventionPropertyBuilder.cs | 18 ---- .../Builders/IConventionTypeBaseBuilder.cs | 62 +++++++++++ .../Metadata/Conventions/ConventionSet.cs | 21 ---- .../Conventions/ElementMappingConvention.cs | 5 +- .../ElementTypeChangedConvention.cs | 25 +---- .../IPropertyElementTypeChangedConvention.cs | 26 ----- .../ConventionDispatcher.ConventionScope.cs | 5 - ...entionDispatcher.DelayedConventionScope.cs | 23 ---- ...tionDispatcher.ImmediateConventionScope.cs | 33 ------ .../Internal/ConventionDispatcher.cs | 12 --- .../NonNullableReferencePropertyConvention.cs | 14 --- .../PropertyDiscoveryConvention.cs | 22 ++-- src/EFCore/Metadata/IConventionProperty.cs | 8 -- src/EFCore/Metadata/IMemberClassifier.cs | 6 ++ src/EFCore/Metadata/IMutableProperty.cs | 6 -- src/EFCore/Metadata/IMutableTypeBase.cs | 12 +++ .../Internal/InternalPropertyBuilder.cs | 18 ---- .../Internal/InternalTypeBaseBuilder.cs | 102 +++++++++++++++++- src/EFCore/Metadata/Internal/Property.cs | 33 ------ src/EFCore/Metadata/Internal/TypeBase.cs | 27 ++++- src/EFCore/Metadata/MemberClassifier.cs | 12 ++- .../Conventions/ConventionDispatcherTest.cs | 95 ---------------- .../Metadata/Internal/PropertyTest.cs | 9 +- 26 files changed, 271 insertions(+), 401 deletions(-) delete mode 100644 src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index cd08c5c1aa8..f41a789b4e2 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -10205,6 +10205,9 @@ { "Member": "RelationalAnnotationProvider(Microsoft.EntityFrameworkCore.Metadata.RelationalAnnotationProviderDependencies dependencies);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RelationalJsonIndex CreateJsonIndex(Microsoft.EntityFrameworkCore.Metadata.IIndex modelIndex, Microsoft.EntityFrameworkCore.Metadata.ITableIndex tableIndex);" + }, { "Member": "static Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement FindJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase property, Microsoft.EntityFrameworkCore.Metadata.ITable table);" }, @@ -10264,9 +10267,6 @@ }, { "Member": "virtual System.Collections.Generic.IEnumerable For(Microsoft.EntityFrameworkCore.Metadata.ITrigger trigger, bool designTime);" - }, - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RelationalJsonIndex CreateJsonIndex(Microsoft.EntityFrameworkCore.Metadata.IIndex modelIndex, Microsoft.EntityFrameworkCore.Metadata.ITableIndex tableIndex);" } ], "Properties": [ @@ -13742,7 +13742,7 @@ "Member": "override bool IsCandidateNavigationProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out System.Type? elementType, out bool? shouldBeOwned, out bool explicitlyConfigured);" }, { - "Member": "override bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out bool explicitlyConfigured);" + "Member": "override bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out System.Type? elementType, out bool explicitlyConfigured);" } ] }, @@ -14569,20 +14569,16 @@ "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder HasJsonPropertyName(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder navigationBuilder, string? name);" }, { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder);", - "Stage": "Obsolete" + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder);" }, { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder);", - "Stage": "Obsolete" + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder);" }, { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);", - "Stage": "Obsolete" + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);" }, { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);", - "Stage": "Obsolete" + "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);" } ] }, @@ -17610,9 +17606,6 @@ { "Member": "virtual void ConfigureParameter(System.Data.Common.DbParameter parameter);" }, - { - "Member": "virtual object? GetDefaultProviderValue();" - }, { "Member": "virtual System.Data.Common.DbParameter CreateParameter(System.Data.Common.DbCommand command, string name, object? value, bool? nullable = null, System.Data.ParameterDirection direction = System.Data.ParameterDirection.Input);" }, @@ -17634,6 +17627,9 @@ { "Member": "static System.Reflection.MethodInfo GetDataReaderMethod(System.Type type);" }, + { + "Member": "virtual object? GetDefaultProviderValue();" + }, { "Member": "virtual string ProcessStoreType(Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping.RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase);" }, diff --git a/src/EFCore.Relational/Metadata/RelationalMemberClassifier.cs b/src/EFCore.Relational/Metadata/RelationalMemberClassifier.cs index 2771de5e7c4..824d3f3a109 100644 --- a/src/EFCore.Relational/Metadata/RelationalMemberClassifier.cs +++ b/src/EFCore.Relational/Metadata/RelationalMemberClassifier.cs @@ -62,9 +62,10 @@ public override bool IsCandidatePrimitiveProperty( IConventionModel model, bool useAttributes, out CoreTypeMapping? typeMapping, + out Type? elementType, out bool explicitlyConfigured) { - if (base.IsCandidatePrimitiveProperty(memberInfo, model, useAttributes, out typeMapping, out explicitlyConfigured)) + if (base.IsCandidatePrimitiveProperty(memberInfo, model, useAttributes, out typeMapping, out elementType, out explicitlyConfigured)) { return true; } @@ -77,6 +78,7 @@ public override bool IsCandidatePrimitiveProperty( && HasExplicitColumnType(memberInfo)) { typeMapping = null; + elementType = null; explicitlyConfigured = true; return true; } diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 5d19f141e9c..199e1b8272e 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -2795,9 +2795,6 @@ { "Member": "virtual System.Collections.Generic.List PropertyAutoLoadChangedConventions { get; }" }, - { - "Member": "virtual System.Collections.Generic.List PropertyElementTypeChangedConventions { get; }" - }, { "Member": "virtual System.Collections.Generic.List PropertyFieldChangedConventions { get; }" }, @@ -7048,7 +7045,7 @@ ] }, { - "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementTypeChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyElementTypeChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyPropertiesChangedConvention", + "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementTypeChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyPropertiesChangedConvention", "Methods": [ { "Member": "ElementTypeChangedConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" @@ -7058,9 +7055,6 @@ }, { "Member": "void ProcessForeignKeyPropertiesChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionForeignKeyBuilder relationshipBuilder, System.Collections.Generic.IReadOnlyList oldDependentProperties, Microsoft.EntityFrameworkCore.Metadata.IConventionKey oldPrincipalKey, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext> context);" - }, - { - "Member": "void ProcessPropertyElementTypeChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.IElementType? newElementType, Microsoft.EntityFrameworkCore.Metadata.IElementType? oldElementType, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" } ], "Properties": [ @@ -11484,9 +11478,6 @@ { "Member": "Microsoft.EntityFrameworkCore.Metadata.PropertySaveBehavior? SetBeforeSaveBehavior(Microsoft.EntityFrameworkCore.Metadata.PropertySaveBehavior? beforeSaveBehavior, bool fromDataAnnotation = false);" }, - { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionElementType? SetElementType(System.Type? elementType, bool fromDataAnnotation = false);" - }, { "Member": "bool? SetIsAutoLoaded(bool? autoLoaded, bool fromDataAnnotation = false);" }, @@ -12115,6 +12106,12 @@ { "Member": "bool CanHaveIndexerProperty(System.Type propertyType, string propertyName, bool fromDataAnnotation = false);" }, + { + "Member": "bool CanHavePrimitiveCollection(System.Type? propertyType, string propertyName, System.Type? elementType = null, bool fromDataAnnotation = false);" + }, + { + "Member": "bool CanHavePrimitiveCollection(System.Reflection.MemberInfo memberInfo, System.Type? elementType = null, bool fromDataAnnotation = false);" + }, { "Member": "bool CanHaveProperty(System.Type? propertyType, string propertyName, bool fromDataAnnotation = false);" }, @@ -12203,6 +12200,12 @@ { "Member": "bool IsIgnored(string memberName, bool fromDataAnnotation = false);" }, + { + "Member": "Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder? PrimitiveCollection(System.Type propertyType, string propertyName, System.Type? elementType = null, bool setTypeConfigurationSource = true, bool fromDataAnnotation = false);" + }, + { + "Member": "Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder? PrimitiveCollection(System.Reflection.MemberInfo memberInfo, System.Type? elementType = null, bool fromDataAnnotation = false);" + }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder? Property(System.Type propertyType, string propertyName, bool setTypeConfigurationSource = true, bool fromDataAnnotation = false);" }, @@ -13521,7 +13524,7 @@ "Member": "bool IsCandidateNavigationProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out System.Type? elementType, out bool? shouldBeOwned, out bool explicitlyConfigured);" }, { - "Member": "bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out bool explicitlyConfigured);" + "Member": "bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out System.Type? elementType, out bool explicitlyConfigured);" }, { "Member": "bool IsCandidateServiceProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Metadata.IParameterBindingFactory? bindingFactory, out bool explicitlyConfigured);" @@ -14330,9 +14333,6 @@ { "Member": "void SetBeforeSaveBehavior(Microsoft.EntityFrameworkCore.Metadata.PropertySaveBehavior? beforeSaveBehavior);" }, - { - "Member": "void SetElementType(System.Type? elementType);" - }, { "Member": "void SetIsUnicode(bool? unicode);" }, @@ -14496,6 +14496,9 @@ { "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableProperty AddProperty(string name, System.Type propertyType);" }, + { + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableProperty AddProperty(string name, System.Type propertyType, System.Type elementType);" + }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableProperty AddProperty(string name, System.Type propertyType, System.Reflection.MemberInfo memberInfo);" }, @@ -15273,14 +15276,6 @@ } ] }, - { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyElementTypeChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention", - "Methods": [ - { - "Member": "void ProcessPropertyElementTypeChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.IElementType? newElementType, Microsoft.EntityFrameworkCore.Metadata.IElementType? oldElementType, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" - } - ] - }, { "Type": "interface Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyFieldChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention", "Methods": [ @@ -18279,7 +18274,7 @@ "Member": "virtual bool IsCandidateNavigationProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out System.Type? elementType, out bool? shouldBeOwned, out bool explicitlyConfigured);" }, { - "Member": "virtual bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out bool explicitlyConfigured);" + "Member": "virtual bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? typeMapping, out System.Type? elementType, out bool explicitlyConfigured);" }, { "Member": "virtual bool IsCandidateServiceProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionModel model, bool useAttributes, out Microsoft.EntityFrameworkCore.Metadata.IParameterBindingFactory? bindingFactory, out bool explicitlyConfigured);" @@ -19418,7 +19413,7 @@ ] }, { - "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableReferencePropertyConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableConventionBase, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyFieldChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyElementTypeChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IComplexPropertyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IComplexPropertyFieldChangedConvention", + "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableReferencePropertyConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.NonNullableConventionBase, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyFieldChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IComplexPropertyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IComplexPropertyFieldChangedConvention", "Methods": [ { "Member": "NonNullableReferencePropertyConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" @@ -19432,9 +19427,6 @@ { "Member": "virtual void ProcessPropertyAdded(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" }, - { - "Member": "virtual void ProcessPropertyElementTypeChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.IElementType? newElementType, Microsoft.EntityFrameworkCore.Metadata.IElementType? oldElementType, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" - }, { "Member": "virtual void ProcessPropertyFieldChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, System.Reflection.FieldInfo? newFieldInfo, System.Reflection.FieldInfo? oldFieldInfo, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" } @@ -20799,7 +20791,7 @@ "Member": "virtual System.Collections.Generic.IEnumerable GetMembers(Microsoft.EntityFrameworkCore.Metadata.IConventionTypeBase structuralType);" }, { - "Member": "virtual bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionTypeBase structuralType, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? mapping);" + "Member": "virtual bool IsCandidatePrimitiveProperty(System.Reflection.MemberInfo memberInfo, Microsoft.EntityFrameworkCore.Metadata.IConventionTypeBase structuralType, out Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping? mapping, out System.Type? elementType);" }, { "Member": "void ProcessComplexPropertyAdded(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionComplexPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" diff --git a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs index 1a24761eac9..531d5a70427 100644 --- a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs @@ -570,22 +570,4 @@ bool CanSetProviderValueComparer( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, bool fromDataAnnotation = false); - - /// - /// 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. - /// - [EntityFrameworkInternal] - IConventionElementTypeBuilder? SetElementType(Type? elementType, bool fromDataAnnotation = false); - - /// - /// 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. - /// - [EntityFrameworkInternal] - bool CanSetElementType(Type? elementType, bool fromDataAnnotation = false); } diff --git a/src/EFCore/Metadata/Builders/IConventionTypeBaseBuilder.cs b/src/EFCore/Metadata/Builders/IConventionTypeBaseBuilder.cs index f9220d047bb..cd38f45cd8a 100644 --- a/src/EFCore/Metadata/Builders/IConventionTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionTypeBaseBuilder.cs @@ -111,6 +111,68 @@ bool CanHaveProperty( /// if the property can be added. bool CanHaveProperty(MemberInfo memberInfo, bool fromDataAnnotation = false); + /// + /// Returns an object that can be used to configure the primitive collection with the given name. + /// If no matching property exists, then a new property will be added. + /// + /// The type of value the property will hold. + /// The name of the property to be configured. + /// The element type of the collection, or to discover it. + /// Indicates whether the type configuration source should be set. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the property if it exists on the type, + /// otherwise. + /// + IConventionPropertyBuilder? PrimitiveCollection( + Type propertyType, + string propertyName, + Type? elementType = null, + bool setTypeConfigurationSource = true, + bool fromDataAnnotation = false); + + /// + /// Returns an object that can be used to configure the primitive collection with the given member info. + /// If no matching property exists, then a new property will be added. + /// + /// The or of the property. + /// The element type of the collection, or to discover it. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the property if it exists on the type, + /// otherwise. + /// + IConventionPropertyBuilder? PrimitiveCollection( + MemberInfo memberInfo, + Type? elementType = null, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given primitive collection can be added to this type. + /// + /// The type of value the property will hold. + /// The name of the property to be configured. + /// The element type of the collection, or to discover it. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be added. + bool CanHavePrimitiveCollection( + Type? propertyType, + string propertyName, + Type? elementType = null, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given primitive collection can be added to this type. + /// + /// The or of the property. + /// The element type of the collection, or to discover it. + /// Indicates whether the configuration was specified using a data annotation. + /// if the property can be added. + bool CanHavePrimitiveCollection( + MemberInfo memberInfo, + Type? elementType = null, + bool fromDataAnnotation = false); + /// /// Returns an object that can be used to configure the indexer property with the given name. /// If no matching property exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index 77ec5d71286..b564d69afce 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -277,11 +277,6 @@ public class ConventionSet /// public virtual List PropertyFieldChangedConventions { get; } = []; - /// - /// Conventions to run when the field of a property is changed. - /// - public virtual List PropertyElementTypeChangedConventions { get; } = []; - /// /// Conventions to run when an annotation is changed on a property. /// @@ -643,12 +638,6 @@ public virtual void Replace(TImplementation newConvention) PropertyRemovedConventions.Add(propertyRemovedConvention); } - if (newConvention is IPropertyElementTypeChangedConvention propertyElementTypeChangedConvention - && !Replace(PropertyElementTypeChangedConventions, propertyElementTypeChangedConvention, oldConventionType)) - { - PropertyElementTypeChangedConventions.Add(propertyElementTypeChangedConvention); - } - if (newConvention is IElementTypeNullabilityChangedConvention elementTypeNullabilityChangedConvention && !Replace(ElementTypeNullabilityChangedConventions, elementTypeNullabilityChangedConvention, oldConventionType)) { @@ -974,11 +963,6 @@ public virtual void Add(IConvention convention) PropertyFieldChangedConventions.Add(propertyFieldChangedConvention); } - if (convention is IPropertyElementTypeChangedConvention propertyElementTypeChangedConvention) - { - PropertyElementTypeChangedConventions.Add(propertyElementTypeChangedConvention); - } - if (convention is IPropertyAnnotationChangedConvention propertyAnnotationChangedConvention) { PropertyAnnotationChangedConventions.Add(propertyAnnotationChangedConvention); @@ -1337,11 +1321,6 @@ public virtual void Remove(Type conventionType) Remove(PropertyRemovedConventions, conventionType); } - if (typeof(IPropertyElementTypeChangedConvention).IsAssignableFrom(conventionType)) - { - Remove(PropertyElementTypeChangedConventions, conventionType); - } - if (typeof(IElementTypeNullabilityChangedConvention).IsAssignableFrom(conventionType)) { Remove(ElementTypeNullabilityChangedConventions, conventionType); diff --git a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs index bb02b589115..6ba4c9007bc 100644 --- a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// @@ -40,7 +42,8 @@ void Validate(IConventionTypeBase typeBase) var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property); if (typeMapping is { ElementTypeMapping: not null }) { - property.Builder.SetElementType(property.ClrType.TryGetElementType(typeof(IEnumerable<>))); + ((InternalPropertyBuilder)property.Builder).SetElementType( + property.ClrType.TryGetElementType(typeof(IEnumerable<>)), ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs index f9dac029672..4ed3fdd1625 100644 --- a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// @@ -10,7 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// See Model building conventions for more information and examples. /// public class ElementTypeChangedConvention : - IPropertyElementTypeChangedConvention, IForeignKeyAddedConvention, IForeignKeyPropertiesChangedConvention { @@ -26,25 +27,6 @@ public ElementTypeChangedConvention(ProviderConventionSetBuilderDependencies dep /// 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, @@ -70,7 +52,8 @@ private static void ProcessForeignKey(IConventionForeignKeyBuilder foreignKeyBui var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties; for (var i = 0; i < foreignKeyProperties.Count; i++) { - foreignKeyProperties[i].Builder.SetElementType(principalKeyProperties[i].GetElementType()?.ClrType); + ((InternalPropertyBuilder)foreignKeyProperties[i].Builder).SetElementType( + principalKeyProperties[i].GetElementType()?.ClrType, ConfigurationSource.Convention); } } } diff --git a/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs deleted file mode 100644 index 45062b7b94e..00000000000 --- a/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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; - -/// -/// Represents an operation that should be performed when the for a property is changed. -/// -/// -/// See Model building conventions for more information and examples. -/// -public interface IPropertyElementTypeChangedConvention : IConvention -{ - /// - /// Called after the element type for a property is changed. - /// - /// The builder for the property. - /// The new element type. - /// The old element type. - /// Additional information associated with convention execution. - void ProcessPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType, - IConventionContext context); -} diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs index e239ba6b771..962d27f7b51 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs @@ -252,11 +252,6 @@ public int GetLeafCount() IConventionTypeBaseBuilder typeBaseBuilder, IConventionProperty property); - public abstract IElementType? OnPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType); - public abstract IConventionTriggerBuilder? OnTriggerAdded(IConventionTriggerBuilder triggerBuilder); public abstract IConventionTrigger? OnTriggerRemoved(IConventionEntityTypeBuilder entityTypeBuilder, IConventionTrigger trigger); diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs index 819dd93dac3..104d09eba94 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs @@ -457,15 +457,6 @@ public override IConventionProperty OnPropertyRemoved( Add(new OnPropertyRemovedNode(typeBaseBuilder, property)); return property; } - - public override IElementType? OnPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType) - { - Add(new OnPropertyElementTypeChangedNode(propertyBuilder, newElementType, oldElementType)); - return newElementType; - } } private sealed class OnModelAnnotationChangedNode( @@ -1043,20 +1034,6 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnPropertyFieldChanged(PropertyBuilder, NewFieldInfo, OldFieldInfo); } - private sealed class OnPropertyElementTypeChangedNode( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType) - : ConventionNode - { - public IConventionPropertyBuilder PropertyBuilder { get; } = propertyBuilder; - public IElementType? NewElementType { get; } = newElementType; - public IElementType? OldElementType { get; } = oldElementType; - - public override void Run(ConventionDispatcher dispatcher) - => dispatcher._immediateConventionScope.OnPropertyElementTypeChanged(PropertyBuilder, NewElementType, OldElementType); - } - private sealed class OnPropertyAnnotationChangedNode( IConventionPropertyBuilder propertyBuilder, string name, diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs index c82166298b2..a39a60031b9 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs @@ -31,7 +31,6 @@ private sealed class ImmediateConventionScope(ConventionSet conventionSet, Conve private readonly ConventionContext _stringConventionContext = new(dispatcher); private readonly ConventionContext _nullableStringConventionContext = new(dispatcher); private readonly ConventionContext _fieldInfoConventionContext = new(dispatcher); - private readonly ConventionContext _elementTypeConventionContext = new(dispatcher); private readonly ConventionContext _boolConventionContext = new(dispatcher); private readonly ConventionContext?> _boolListConventionContext = new(dispatcher); @@ -1710,38 +1709,6 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _fieldInfoConventionContext.Result; } - public override IElementType? OnPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType) - { - if (!propertyBuilder.Metadata.IsInModel - || !propertyBuilder.Metadata.DeclaringType.IsInModel) - { - return null; - } -#if DEBUG - var initialValue = propertyBuilder.Metadata.GetElementType(); -#endif - _elementTypeConventionContext.ResetState(newElementType); - foreach (var propertyConvention in conventionSet.PropertyElementTypeChangedConventions) - { - propertyConvention.ProcessPropertyElementTypeChanged( - propertyBuilder, newElementType, oldElementType, _elementTypeConventionContext); - if (_elementTypeConventionContext.ShouldStopProcessing()) - { - return _elementTypeConventionContext.Result; - } -#if DEBUG - Check.DebugAssert( - initialValue == propertyBuilder.Metadata.GetElementType(), - $"Convention {propertyConvention.GetType().Name} changed value without terminating"); -#endif - } - - return _elementTypeConventionContext.Result; - } - public override IConventionAnnotation? OnPropertyAnnotationChanged( IConventionPropertyBuilder propertyBuilder, string name, diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs index 2885a935cd5..ce56fbf99c7 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -724,18 +724,6 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder FieldInfo? oldFieldInfo) => _scope.OnPropertyFieldChanged(propertyBuilder, newFieldInfo, oldFieldInfo); - /// - /// 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 IElementType? OnPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType) - => _scope.OnPropertyElementTypeChanged(propertyBuilder, newElementType, oldElementType); - /// /// 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 diff --git a/src/EFCore/Metadata/Conventions/NonNullableReferencePropertyConvention.cs b/src/EFCore/Metadata/Conventions/NonNullableReferencePropertyConvention.cs index 1a07dfc22f1..c5854e6c87a 100644 --- a/src/EFCore/Metadata/Conventions/NonNullableReferencePropertyConvention.cs +++ b/src/EFCore/Metadata/Conventions/NonNullableReferencePropertyConvention.cs @@ -15,7 +15,6 @@ public class NonNullableReferencePropertyConvention(ProviderConventionSetBuilder : NonNullableConventionBase(dependencies), IPropertyAddedConvention, IPropertyFieldChangedConvention, - IPropertyElementTypeChangedConvention, IComplexPropertyAddedConvention, IComplexPropertyFieldChangedConvention { @@ -69,19 +68,6 @@ public virtual void ProcessPropertyFieldChanged( } } - /// - public virtual void ProcessPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType? newElementType, - IElementType? oldElementType, - IConventionContext context) - { - if (newElementType != null) - { - Process(propertyBuilder); - } - } - /// public virtual void ProcessComplexPropertyAdded( IConventionComplexPropertyBuilder propertyBuilder, diff --git a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs index b127b8bce5d..b9cc1bd1d26 100644 --- a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs @@ -79,19 +79,18 @@ protected virtual void DiscoverPrimitiveProperties( var structuralType = structuralTypeBuilder.Metadata; foreach (var propertyInfo in GetMembers(structuralType)) { - if (!IsCandidatePrimitiveProperty(propertyInfo, structuralType, out var mapping)) + if (!IsCandidatePrimitiveProperty(propertyInfo, structuralType, out _, out var elementType)) { continue; } - var propertyBuilder = structuralTypeBuilder.Property(propertyInfo); - if (mapping?.ElementTypeMapping != null) + if (elementType != null) { - var elementType = propertyInfo.GetMemberType().TryGetElementType(typeof(IEnumerable<>)); - if (elementType != null) - { - propertyBuilder?.SetElementType(elementType); - } + structuralTypeBuilder.PrimitiveCollection(propertyInfo, elementType); + } + else + { + structuralTypeBuilder.Property(propertyInfo); } } } @@ -113,10 +112,13 @@ protected virtual IEnumerable GetMembers(IConventionTypeBase structu /// The member. /// The type for which the properties will be discovered. /// The type mapping for the property. + /// The element type if the member is a primitive collection; otherwise . protected virtual bool IsCandidatePrimitiveProperty( MemberInfo memberInfo, IConventionTypeBase structuralType, - out CoreTypeMapping? mapping) - => Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(memberInfo, structuralType.Model, UseAttributes, out mapping, out _) + out CoreTypeMapping? mapping, + out Type? elementType) + => Dependencies.MemberClassifier.IsCandidatePrimitiveProperty( + memberInfo, structuralType.Model, UseAttributes, out mapping, out elementType, out _) && ((Model)structuralType.Model).FindIsComplexConfigurationSource(memberInfo.GetMemberType().UnwrapNullableType()) == null; } diff --git a/src/EFCore/Metadata/IConventionProperty.cs b/src/EFCore/Metadata/IConventionProperty.cs index 11f8080dfe1..dd9377f3363 100644 --- a/src/EFCore/Metadata/IConventionProperty.cs +++ b/src/EFCore/Metadata/IConventionProperty.cs @@ -475,14 +475,6 @@ bool IsImplicitlyCreated() /// The configuration for the elements. new IConventionElementType? GetElementType(); - /// - /// Sets the configuration for elements of the primitive collection represented by this property. - /// - /// If , then the type mapping has an element type, otherwise it is removed. - /// Indicates whether the configuration was specified using a data annotation. - /// The configuration for the elements. - IConventionElementType? SetElementType(Type? elementType, bool fromDataAnnotation = false); - /// /// Returns the configuration source for . /// diff --git a/src/EFCore/Metadata/IMemberClassifier.cs b/src/EFCore/Metadata/IMemberClassifier.cs index d024768ab30..2d6616fd1b5 100644 --- a/src/EFCore/Metadata/IMemberClassifier.cs +++ b/src/EFCore/Metadata/IMemberClassifier.cs @@ -143,6 +143,11 @@ bool IsCandidateNavigationProperty( /// The model. /// Whether attributes found on the member should be considered. /// When this method returns, the type mapping for the member, if one was found. + /// + /// When this method returns, the element type for a primitive collection, or when the member + /// is not a primitive collection. This is non- only when has an + /// element type mapping and the member's CLR type has an element type. + /// /// When this method returns, indicates whether the type was explicitly configured. /// if the member is a candidate primitive property; otherwise . bool IsCandidatePrimitiveProperty( @@ -150,6 +155,7 @@ bool IsCandidatePrimitiveProperty( IConventionModel model, bool useAttributes, out CoreTypeMapping? typeMapping, + out Type? elementType, out bool explicitlyConfigured); /// diff --git a/src/EFCore/Metadata/IMutableProperty.cs b/src/EFCore/Metadata/IMutableProperty.cs index f6027246e8c..e57f44b6aa4 100644 --- a/src/EFCore/Metadata/IMutableProperty.cs +++ b/src/EFCore/Metadata/IMutableProperty.cs @@ -278,12 +278,6 @@ void SetProviderValueComparer( /// The configuration for the elements. new IMutableElementType? GetElementType(); - /// - /// Sets the configuration for elements of the primitive collection represented by this property. - /// - /// If , then this is a collection of primitive elements. - void SetElementType(Type? elementType); - /// bool IReadOnlyProperty.IsNullable => IsNullable; diff --git a/src/EFCore/Metadata/IMutableTypeBase.cs b/src/EFCore/Metadata/IMutableTypeBase.cs index f3e9027a662..fe7cc83d96c 100644 --- a/src/EFCore/Metadata/IMutableTypeBase.cs +++ b/src/EFCore/Metadata/IMutableTypeBase.cs @@ -145,6 +145,18 @@ IMutableProperty AddProperty(MemberInfo memberInfo) /// The newly created property. IMutableProperty AddProperty(string name, [DynamicallyAccessedMembers(IProperty.DynamicallyAccessedMemberTypes)] Type propertyType); + /// + /// Adds a primitive collection property to this type. + /// + /// The name of the property to add. + /// The type of value the property will hold. + /// The element type of the primitive collection. + /// The newly created property. + IMutableProperty AddProperty( + string name, + [DynamicallyAccessedMembers(IProperty.DynamicallyAccessedMemberTypes)] Type propertyType, + Type elementType); + /// /// Adds a property to this type. /// diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index a0988a7683e..4a17769cbe6 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -1625,22 +1625,4 @@ bool IConventionPropertyBuilder.CanSetProviderValueComparer( bool fromDataAnnotation) => CanSetProviderValueComparer( comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - - /// - /// 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. - /// - IConventionElementTypeBuilder? IConventionPropertyBuilder.SetElementType(Type? elementType, bool fromDataAnnotation) - => SetElementType(elementType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - - /// - /// 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. - /// - bool IConventionPropertyBuilder.CanSetElementType(Type? elementType, bool fromDataAnnotation) - => CanSetElementType(elementType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index 552966606a2..46b4b1ddafc 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -167,7 +167,8 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing MemberInfo? memberInfo, ConfigurationSource? typeConfigurationSource, ConfigurationSource? configurationSource, - bool skipTypeCheck = false) + bool skipTypeCheck = false, + Type? elementType = null) { var structuralType = Metadata; List? propertiesToDetach = null; @@ -275,7 +276,7 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing } builder = structuralType.AddProperty( - propertyName, propertyType, memberInfo, typeConfigurationSource, configurationSource.Value)!.Builder; + propertyName, propertyType, memberInfo, typeConfigurationSource, configurationSource.Value, elementType)!.Builder; detachedProperties?.Attach(this); } @@ -344,13 +345,26 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing string propertyName, MemberInfo? memberInfo, ConfigurationSource? typeConfigurationSource, - ConfigurationSource? configurationSource) + ConfigurationSource? configurationSource, + Type? elementType = null) { - var builder = Property(propertyType, propertyName, memberInfo, typeConfigurationSource, configurationSource); + var effectiveType = propertyType ?? memberInfo?.GetMemberType(); + if (elementType == null + && effectiveType != null) + { + elementType = effectiveType.TryGetElementType(typeof(IEnumerable<>)); + if (elementType == null) + { + throw new InvalidOperationException(CoreStrings.NotCollection(effectiveType.ShortDisplayName(), propertyName)); + } + } + + var builder = Property( + propertyType, propertyName, memberInfo, typeConfigurationSource, configurationSource, elementType: elementType); if (builder != null) { - var elementClrType = builder.Metadata.ClrType.TryGetElementType(typeof(IEnumerable<>)); + var elementClrType = elementType ?? builder.Metadata.ClrType.TryGetElementType(typeof(IEnumerable<>)); if (elementClrType == null) { throw new InvalidOperationException(CoreStrings.NotCollection(builder.Metadata.ClrType.ShortDisplayName(), propertyName)); @@ -2214,6 +2228,84 @@ bool IConventionTypeBaseBuilder.CanHaveProperty(MemberInfo memberInfo, bool from fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionPropertyBuilder? IConventionTypeBaseBuilder.PrimitiveCollection( + Type propertyType, + string propertyName, + Type? elementType, + bool setTypeConfigurationSource, + bool fromDataAnnotation) + => PrimitiveCollection( + propertyType, + propertyName, + memberInfo: null, + setTypeConfigurationSource + ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention + : null, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + elementType); + + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionPropertyBuilder? IConventionTypeBaseBuilder.PrimitiveCollection( + MemberInfo memberInfo, + Type? elementType, + bool fromDataAnnotation) + => PrimitiveCollection( + memberInfo.GetMemberType(), + memberInfo.GetSimpleMemberName(), + memberInfo, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + elementType); + + /// + /// 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. + /// + [DebuggerStepThrough] + bool IConventionTypeBaseBuilder.CanHavePrimitiveCollection( + Type? propertyType, + string propertyName, + Type? elementType, + bool fromDataAnnotation) + => CanHaveProperty( + propertyType, + propertyName, + null, + propertyType != null + ? fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention + : null, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + bool IConventionTypeBaseBuilder.CanHavePrimitiveCollection(MemberInfo memberInfo, Type? elementType, bool fromDataAnnotation) + => CanHaveProperty( + memberInfo.GetMemberType(), + memberInfo.Name, + memberInfo, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 5f6c0ae2dd8..35e8d986696 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -1445,7 +1445,6 @@ public virtual bool IsPrimitiveCollection { var newElementType = new ElementType(elementType, this, configurationSource); SetAnnotation(CoreAnnotationNames.ElementType, newElementType, configurationSource); - OnElementTypeSet(newElementType, null); return newElementType; } @@ -1454,22 +1453,12 @@ public virtual bool IsPrimitiveCollection { existingElementType.SetRemovedFromModel(); RemoveAnnotation(CoreAnnotationNames.ElementType); - OnElementTypeSet(null, existingElementType); return null; } return existingElementType; } - /// - /// 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. - /// - protected virtual IElementType? OnElementTypeSet(IElementType? newElementType, IElementType? oldElementType) - => DeclaringType.Model.ConventionDispatcher.OnPropertyElementTypeChanged(Builder, newElementType, oldElementType); - /// /// 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 @@ -2212,28 +2201,6 @@ void IMutableProperty.SetJsonValueReaderWriterType(Type? readerWriterType) readerWriterType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - /// - /// 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. - /// - [DebuggerStepThrough] - IConventionElementType? IConventionProperty.SetElementType(Type? elementType, bool fromDataAnnotation) - => SetElementType( - elementType, - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - - /// - /// 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. - /// - [DebuggerStepThrough] - void IMutableProperty.SetElementType(Type? elementType) - => SetElementType(elementType, ConfigurationSource.Explicit); - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 33fd5296817..e62bc4c4d76 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -633,7 +633,8 @@ private void CheckDiscriminatorProperty(Property? property) [DynamicallyAccessedMembers(IProperty.DynamicallyAccessedMemberTypes)] Type propertyType, MemberInfo? memberInfo, ConfigurationSource? typeConfigurationSource, - ConfigurationSource configurationSource) + ConfigurationSource configurationSource, + Type? elementType = null) { Check.NotNull(name); Check.NotNull(propertyType); @@ -691,6 +692,11 @@ private void CheckDiscriminatorProperty(Property? property) Model.AddProperty(property); + if (elementType != null) + { + property.SetElementType(elementType, configurationSource); + } + if (Model.Configuration != null) { using (Model.ConventionDispatcher.DelayConventions()) @@ -2003,6 +2009,25 @@ IMutableProperty IMutableTypeBase.AddProperty( ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + /// + /// 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. + /// + [DebuggerStepThrough] + IMutableProperty IMutableTypeBase.AddProperty( + string name, + [DynamicallyAccessedMembers(IProperty.DynamicallyAccessedMemberTypes)] Type propertyType, + Type elementType) + => AddProperty( + name, + propertyType, + memberInfo: null, + ConfigurationSource.Explicit, + ConfigurationSource.Explicit, + elementType)!; + /// /// 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 diff --git a/src/EFCore/Metadata/MemberClassifier.cs b/src/EFCore/Metadata/MemberClassifier.cs index 934aba4e9d3..ba4146b5127 100644 --- a/src/EFCore/Metadata/MemberClassifier.cs +++ b/src/EFCore/Metadata/MemberClassifier.cs @@ -120,9 +120,11 @@ public virtual bool IsCandidatePrimitiveProperty( IConventionModel model, bool useAttributes, out CoreTypeMapping? typeMapping, + out Type? elementType, out bool explicitlyConfigured) { typeMapping = null; + elementType = null; explicitlyConfigured = false; if (!memberInfo.IsCandidateProperty()) { @@ -131,9 +133,17 @@ public virtual bool IsCandidatePrimitiveProperty( var configurationType = GetConfigurationType(memberInfo.GetMemberType(), model); explicitlyConfigured = configurationType != null; - return configurationType == TypeConfigurationType.Property + var isCandidate = configurationType == TypeConfigurationType.Property || (configurationType == null && (typeMapping = Dependencies.TypeMappingSource.FindMapping(memberInfo, (IModel)model, useAttributes)) != null); + + if (isCandidate + && typeMapping?.ElementTypeMapping != null) + { + elementType = memberInfo.GetMemberType().TryGetElementType(typeof(IEnumerable<>)); + } + + return isCandidate; } /// diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index dc33435177a..0523cd51bdb 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -3784,101 +3784,6 @@ public void ProcessPropertyFieldChanged( } } - [InlineData(false, false), InlineData(true, false), InlineData(false, true), InlineData(true, true), Theory] - public void OnPropertyElementTypeChanged_calls_conventions_in_order(bool useBuilder, bool useScope) - { - var conventions = new ConventionSet(); - - var convention1 = new PropertyElementTypeChangedConvention(terminate: false); - var convention2 = new PropertyElementTypeChangedConvention(terminate: true); - var convention3 = new PropertyElementTypeChangedConvention(terminate: false); - conventions.Add(convention1); - conventions.Add(convention2); - conventions.Add(convention3); - - var builder = new InternalModelBuilder(new Model(conventions)); - var entityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention)!; - var propertyBuilder = entityBuilder.Property(Order.OrderIdsProperty, ConfigurationSource.Convention)!; - - var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; - - ElementType elementType; - - if (useBuilder) - { - Assert.NotNull(propertyBuilder.SetElementType(typeof(int), ConfigurationSource.Convention)); - elementType = propertyBuilder.Metadata.GetElementType()!; - } - else - { - elementType = propertyBuilder.Metadata.SetElementType(typeof(int), ConfigurationSource.Convention); - } - - if (useScope) - { - Assert.Empty(convention1.Calls); - Assert.Empty(convention2.Calls); - scope.Dispose(); - } - - Assert.Equal(new (object, object)[] { (null, elementType) }, convention1.Calls); - Assert.Equal(new (object, object)[] { (null, elementType) }, convention2.Calls); - Assert.Empty(convention3.Calls); - - if (useBuilder) - { - Assert.NotNull(propertyBuilder.SetElementType(typeof(int), ConfigurationSource.Convention)); - elementType = propertyBuilder.Metadata.GetElementType()!; - } - else - { - elementType = propertyBuilder.Metadata.SetElementType(typeof(int), ConfigurationSource.Convention); - } - - Assert.Equal(new (object, object)[] { (null, elementType) }, convention1.Calls); - Assert.Equal(new (object, object)[] { (null, elementType) }, convention2.Calls); - Assert.Empty(convention3.Calls); - - if (useBuilder) - { - Assert.NotNull(propertyBuilder.SetElementType(null, ConfigurationSource.Convention)); - } - else - { - Assert.Null(propertyBuilder.Metadata.SetElementType(null, ConfigurationSource.Convention)); - } - - Assert.Equal(new (object, object)[] { (null, elementType), (elementType, null) }, convention1.Calls); - Assert.Equal(new (object, object)[] { (null, elementType), (elementType, null) }, convention2.Calls); - Assert.Empty(convention3.Calls); - - AssertSetOperations( - new PropertyElementTypeChangedConvention(terminate: true), - conventions, conventions.PropertyElementTypeChangedConventions); - } - - private class PropertyElementTypeChangedConvention(bool terminate) : IPropertyElementTypeChangedConvention - { - private readonly bool _terminate = terminate; - public readonly List<(object, object)> Calls = []; - - public void ProcessPropertyElementTypeChanged( - IConventionPropertyBuilder propertyBuilder, - IElementType newElementType, - IElementType oldElementType, - IConventionContext context) - { - Assert.True(propertyBuilder.Metadata.IsInModel); - - Calls.Add((oldElementType, newElementType)); - - if (_terminate) - { - context.StopProcessing(); - } - } - } - [InlineData(false, false), InlineData(true, false), InlineData(false, true), InlineData(true, true), Theory] public void OnPropertyAnnotationChanged_calls_conventions_in_order(bool useBuilder, bool useScope) { diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs index 1be0026734f..7230151f46a 100644 --- a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs @@ -507,8 +507,7 @@ public void Can_set_element_type_for_primitive_collection() { var model = CreateModel(); var entityType = model.AddEntityType(typeof(object)); - var property = entityType.AddProperty("Random", typeof(IList)); - property.SetElementType(typeof(int)); + var property = entityType.AddProperty("Random", typeof(IList), typeof(int)); Assert.Equal(typeof(int), property.GetElementType()!.ClrType); Assert.True(property.IsPrimitiveCollection); @@ -519,8 +518,7 @@ public void Can_set_derived_element_type_for_primitive_collection() { var model = CreateModel(); var entityType = model.AddEntityType(typeof(object)); - var property = entityType.AddProperty("Random", typeof(IList)); - property.SetElementType(typeof(int)); + var property = entityType.AddProperty("Random", typeof(IList), typeof(int)); Assert.Equal(typeof(int), property.GetElementType()!.ClrType); Assert.True(property.IsPrimitiveCollection); @@ -531,8 +529,7 @@ public void Can_set_element_type_for_non_primitive_collection() { var model = CreateModel(); var entityType = model.AddEntityType(typeof(object)); - var property = entityType.AddProperty("Random", typeof(Random)); - property.SetElementType(typeof(int)); + var property = entityType.AddProperty("Random", typeof(Random), typeof(int)); Assert.Equal(typeof(int), property.GetElementType()!.ClrType); Assert.False(property.IsPrimitiveCollection); From bcba01f063fb0f179f58b72c08f652f1c955265f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:54:07 +0000 Subject: [PATCH 2/6] Address review feedback: simplify PrimitiveCollection element-type resolution, add clarifying comments Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/ElementTypeChangedConvention.cs | 1 + .../Metadata/Internal/InternalTypeBaseBuilder.cs | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs index 4ed3fdd1625..37fb4edfdf5 100644 --- a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs @@ -52,6 +52,7 @@ private static void ProcessForeignKey(IConventionForeignKeyBuilder foreignKeyBui var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties; for (var i = 0; i < foreignKeyProperties.Count; i++) { + // Element type is no longer set via a public convention API, so cast to the internal builder to propagate it. ((InternalPropertyBuilder)foreignKeyProperties[i].Builder).SetElementType( principalKeyProperties[i].GetElementType()?.ClrType, ConfigurationSource.Convention); } diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index 46b4b1ddafc..bace9f9c8b4 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -348,15 +348,13 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing ConfigurationSource? configurationSource, Type? elementType = null) { + // Resolve the element type up front so it can be set before the property-added conventions run. var effectiveType = propertyType ?? memberInfo?.GetMemberType(); + elementType ??= effectiveType?.TryGetElementType(typeof(IEnumerable<>)); if (elementType == null && effectiveType != null) { - elementType = effectiveType.TryGetElementType(typeof(IEnumerable<>)); - if (elementType == null) - { - throw new InvalidOperationException(CoreStrings.NotCollection(effectiveType.ShortDisplayName(), propertyName)); - } + throw new InvalidOperationException(CoreStrings.NotCollection(effectiveType.ShortDisplayName(), propertyName)); } var builder = Property( @@ -364,13 +362,13 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing if (builder != null) { - var elementClrType = elementType ?? builder.Metadata.ClrType.TryGetElementType(typeof(IEnumerable<>)); - if (elementClrType == null) + elementType ??= builder.Metadata.ClrType.TryGetElementType(typeof(IEnumerable<>)); + if (elementType == null) { throw new InvalidOperationException(CoreStrings.NotCollection(builder.Metadata.ClrType.ShortDisplayName(), propertyName)); } - builder.SetElementType(elementClrType, configurationSource!.Value); + builder.SetElementType(elementType, configurationSource!.Value); } return builder; From a46cfe6848cc89f89b42e23efa47cca503c4798b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:08:43 +0000 Subject: [PATCH 3/6] Relocate element-type setter to InternalTypeBaseBuilder; fix doc comment Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Conventions/ElementMappingConvention.cs | 7 +- .../ElementTypeChangedConvention.cs | 7 +- src/EFCore/Metadata/IMemberClassifier.cs | 5 +- .../Internal/InternalPropertyBuilder.cs | 64 ++++++------------ .../Internal/InternalTypeBaseBuilder.cs | 67 ++++++++++++++++++- src/EFCore/Metadata/Internal/Property.cs | 30 --------- src/EFCore/Metadata/Internal/TypeBase.cs | 5 +- .../Conventions/ConventionDispatcherTest.cs | 8 +-- 8 files changed, 104 insertions(+), 89 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs index 6ba4c9007bc..f94bde1fea6 100644 --- a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs @@ -42,8 +42,11 @@ void Validate(IConventionTypeBase typeBase) var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property); if (typeMapping is { ElementTypeMapping: not null }) { - ((InternalPropertyBuilder)property.Builder).SetElementType( - property.ClrType.TryGetElementType(typeof(IEnumerable<>)), ConfigurationSource.Convention); + var collectionProperty = (Property)property; + collectionProperty.DeclaringType.Builder.SetElementType( + collectionProperty.Builder, + property.ClrType.TryGetElementType(typeof(IEnumerable<>)), + ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs index 37fb4edfdf5..c738b7a7725 100644 --- a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs @@ -52,9 +52,10 @@ private static void ProcessForeignKey(IConventionForeignKeyBuilder foreignKeyBui var principalKeyProperties = foreignKeyBuilder.Metadata.PrincipalKey.Properties; for (var i = 0; i < foreignKeyProperties.Count; i++) { - // Element type is no longer set via a public convention API, so cast to the internal builder to propagate it. - ((InternalPropertyBuilder)foreignKeyProperties[i].Builder).SetElementType( - principalKeyProperties[i].GetElementType()?.ClrType, ConfigurationSource.Convention); + // Element type is no longer set via a public convention API, so use the internal type builder to propagate it. + var foreignKeyProperty = (Property)foreignKeyProperties[i]; + foreignKeyProperty.DeclaringType.Builder.SetElementType( + foreignKeyProperty.Builder, principalKeyProperties[i].GetElementType()?.ClrType, ConfigurationSource.Convention); } } } diff --git a/src/EFCore/Metadata/IMemberClassifier.cs b/src/EFCore/Metadata/IMemberClassifier.cs index 2d6616fd1b5..9f5b34f822e 100644 --- a/src/EFCore/Metadata/IMemberClassifier.cs +++ b/src/EFCore/Metadata/IMemberClassifier.cs @@ -144,9 +144,8 @@ bool IsCandidateNavigationProperty( /// Whether attributes found on the member should be considered. /// When this method returns, the type mapping for the member, if one was found. /// - /// When this method returns, the element type for a primitive collection, or when the member - /// is not a primitive collection. This is non- only when has an - /// element type mapping and the member's CLR type has an element type. + /// When this method returns , the element type if the member is a primitive collection; + /// otherwise . /// /// When this method returns, indicates whether the type was explicitly configured. /// if the member is a candidate primitive property; otherwise . diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 4a17769cbe6..6b486c674e2 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -547,7 +547,7 @@ public virtual bool CanSetValueGeneratorFactory( { if (converter != null) { - Metadata.SetElementType(null, configurationSource); + RemoveElementType(); } Metadata.SetProviderClrType(null, configurationSource); @@ -574,7 +574,7 @@ public virtual bool CanSetConversion( || (Metadata[CoreAnnotationNames.ValueConverterType] == null && (ValueConverter?)Metadata[CoreAnnotationNames.ValueConverter] == converter)) && configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) - && (converter == null || CanSetElementType(null, configurationSource)); + && (converter == null || CanRemoveElementType(configurationSource)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -588,7 +588,7 @@ public virtual bool CanSetConversion( { if (providerClrType != null) { - Metadata.SetElementType(null, configurationSource); + RemoveElementType(); } Metadata.SetValueConverter((ValueConverter?)null, configurationSource); @@ -610,7 +610,7 @@ public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? => (configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) || Metadata.GetProviderClrType() == providerClrType) && configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) - && (providerClrType == null || CanSetElementType(null, configurationSource)); + && (providerClrType == null || CanRemoveElementType(configurationSource)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -627,7 +627,7 @@ public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? { if (converterType != null) { - Metadata.SetElementType(null, configurationSource); + RemoveElementType(); } Metadata.SetProviderClrType(null, configurationSource); @@ -652,7 +652,20 @@ public virtual bool CanSetConverter( => (configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) || (Metadata[CoreAnnotationNames.ValueConverter] == null && (Type?)Metadata[CoreAnnotationNames.ValueConverterType] == converterType)) - && (converterType == null || CanSetElementType(null, configurationSource)); + && (converterType == null || CanRemoveElementType(configurationSource)); + + private void RemoveElementType() + { + if (Metadata.GetElementType() is { } elementType) + { + elementType.SetRemovedFromModel(); + Metadata.RemoveAnnotation(CoreAnnotationNames.ElementType); + } + } + + private bool CanRemoveElementType(ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetElementTypeConfigurationSource()) + || Metadata.GetElementType() == null; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -836,45 +849,6 @@ public virtual bool CanSetProviderValueComparer( || (Metadata[CoreAnnotationNames.ProviderValueComparer] == null && (Type?)Metadata[CoreAnnotationNames.ProviderValueComparerType] == comparerType); - /// - /// 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 InternalElementTypeBuilder? SetElementType(Type? elementType, ConfigurationSource configurationSource) - { - if (CanSetElementType(elementType, configurationSource)) - { - Metadata.SetElementType(elementType, configurationSource); - if (elementType != null) - { - Metadata.SetValueConverter((Type?)null, configurationSource); - } - - if (elementType == null - && CanSetConversion((Type?)null, configurationSource)) - { - Metadata.RemoveAnnotation(CoreAnnotationNames.ValueConverter); - } - - return new InternalElementTypeBuilder(Metadata.GetElementType()!, ModelBuilder); - } - - return null; - } - - /// - /// 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 CanSetElementType(Type? elementType, ConfigurationSource? configurationSource) - => (configurationSource.Overrides(Metadata.GetElementTypeConfigurationSource()) - && (elementType == null || CanSetConversion((Type?)null, configurationSource))) - || elementType == Metadata.GetElementType()?.ClrType; - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index bace9f9c8b4..ceccb753589 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -368,12 +368,77 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing throw new InvalidOperationException(CoreStrings.NotCollection(builder.Metadata.ClrType.ShortDisplayName(), propertyName)); } - builder.SetElementType(elementType, configurationSource!.Value); + SetElementType(builder, elementType, configurationSource!.Value); } return builder; } + /// + /// 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 InternalElementTypeBuilder? SetElementType( + InternalPropertyBuilder propertyBuilder, + Type? elementType, + ConfigurationSource configurationSource) + { + if (!CanSetElementType(propertyBuilder, elementType, configurationSource)) + { + return null; + } + + var property = propertyBuilder.Metadata; + var existingElementType = property.GetElementType(); + if (elementType != null) + { + if (elementType != existingElementType?.ClrType) + { + property.SetAnnotation( + CoreAnnotationNames.ElementType, + new ElementType(elementType, property, configurationSource), + configurationSource); + } + + property.SetValueConverter((Type?)null, configurationSource); + } + else + { + if (existingElementType != null) + { + existingElementType.SetRemovedFromModel(); + property.RemoveAnnotation(CoreAnnotationNames.ElementType); + } + + if (propertyBuilder.CanSetConversion((Type?)null, configurationSource)) + { + property.RemoveAnnotation(CoreAnnotationNames.ValueConverter); + } + } + + var newElementType = property.GetElementType(); + return newElementType == null ? null : new InternalElementTypeBuilder(newElementType, ModelBuilder); + } + + /// + /// 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 CanSetElementType( + InternalPropertyBuilder propertyBuilder, + Type? elementType, + ConfigurationSource? configurationSource) + { + var property = propertyBuilder.Metadata; + return (configurationSource.Overrides(property.GetElementTypeConfigurationSource()) + && (elementType == null || propertyBuilder.CanSetConversion((Type?)null, configurationSource))) + || elementType == property.GetElementType()?.ClrType; + } + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 35e8d986696..8215bbe771c 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -1429,36 +1429,6 @@ public virtual bool IsPrimitiveCollection } } - /// - /// 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 ElementType? SetElementType( - Type? elementType, - ConfigurationSource configurationSource) - { - var existingElementType = GetElementType(); - if (elementType != null - && elementType != existingElementType?.ClrType) - { - var newElementType = new ElementType(elementType, this, configurationSource); - SetAnnotation(CoreAnnotationNames.ElementType, newElementType, configurationSource); - return newElementType; - } - - if (elementType == null - && existingElementType != null) - { - existingElementType.SetRemovedFromModel(); - RemoveAnnotation(CoreAnnotationNames.ElementType); - return null; - } - - return existingElementType; - } - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index e62bc4c4d76..3116e930c0c 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -694,7 +694,10 @@ private void CheckDiscriminatorProperty(Property? property) if (elementType != null) { - property.SetElementType(elementType, configurationSource); + property.SetAnnotation( + CoreAnnotationNames.ElementType, + new ElementType(elementType, property, configurationSource), + configurationSource); } if (Model.Configuration != null) diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index 0523cd51bdb..db1d572e3c2 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -5026,8 +5026,8 @@ public void OnElementTypeAnnotationChanged_calls_conventions_in_order(bool useBu var builder = new InternalModelBuilder(new Model(conventions)); var elementTypeBuilder = builder.Entity(typeof(SpecialOrder), ConfigurationSource.Convention)! - .Property(nameof(SpecialOrder.OrderIds), ConfigurationSource.Convention)! - .SetElementType(typeof(int), ConfigurationSource.Convention)!; + .PrimitiveCollection(nameof(SpecialOrder.OrderIds), ConfigurationSource.Convention)! + .Metadata.GetElementType()!.Builder; var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; @@ -5126,8 +5126,8 @@ public void OnElementTypeNullabilityChanged_calls_conventions_in_order(bool useB var builder = new InternalModelBuilder(model); var elementTypeBuilder = builder.Entity(typeof(SpecialOrder), ConfigurationSource.Convention)! - .Property(nameof(SpecialOrder.Notes), ConfigurationSource.Convention)! - .SetElementType(typeof(string), ConfigurationSource.Convention)!; + .PrimitiveCollection(nameof(SpecialOrder.Notes), ConfigurationSource.Convention)! + .Metadata.GetElementType()!.Builder; if (useBuilder) { From 5f5e4c28d6b2f942fbff1cb6a6445c9a40bcc68f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:42:42 +0000 Subject: [PATCH 4/6] Move ElementType instantiation into Property; inline CanSetElementType; keep ElementMappingConvention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/InternalTypeBaseBuilder.cs | 39 +++-------------- src/EFCore/Metadata/Internal/Property.cs | 43 ++++++++++++++++++- src/EFCore/Metadata/Internal/TypeBase.cs | 10 +---- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index ceccb753589..7e32495b247 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -385,32 +385,22 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing Type? elementType, ConfigurationSource configurationSource) { - if (!CanSetElementType(propertyBuilder, elementType, configurationSource)) + var property = propertyBuilder.Metadata; + if (!((configurationSource.Overrides(property.GetElementTypeConfigurationSource()) + && (elementType == null || propertyBuilder.CanSetConversion((Type?)null, configurationSource))) + || elementType == property.GetElementType()?.ClrType)) { return null; } - var property = propertyBuilder.Metadata; - var existingElementType = property.GetElementType(); if (elementType != null) { - if (elementType != existingElementType?.ClrType) - { - property.SetAnnotation( - CoreAnnotationNames.ElementType, - new ElementType(elementType, property, configurationSource), - configurationSource); - } - + property.SetElementType(elementType, configurationSource); property.SetValueConverter((Type?)null, configurationSource); } else { - if (existingElementType != null) - { - existingElementType.SetRemovedFromModel(); - property.RemoveAnnotation(CoreAnnotationNames.ElementType); - } + property.SetElementType(null, configurationSource); if (propertyBuilder.CanSetConversion((Type?)null, configurationSource)) { @@ -422,23 +412,6 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing return newElementType == null ? null : new InternalElementTypeBuilder(newElementType, ModelBuilder); } - /// - /// 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 CanSetElementType( - InternalPropertyBuilder propertyBuilder, - Type? elementType, - ConfigurationSource? configurationSource) - { - var property = propertyBuilder.Metadata; - return (configurationSource.Overrides(property.GetElementTypeConfigurationSource()) - && (elementType == null || propertyBuilder.CanSetConversion((Type?)null, configurationSource))) - || elementType == property.GetElementType()?.ClrType; - } - /// /// 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 diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 8215bbe771c..c58de1e36d7 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -48,13 +48,22 @@ public Property( FieldInfo? fieldInfo, TypeBase declaringType, ConfigurationSource configurationSource, - ConfigurationSource? typeConfigurationSource) + ConfigurationSource? typeConfigurationSource, + Type? elementType = null) : base(name, propertyInfo, fieldInfo, configurationSource) { DeclaringType = declaringType; ClrType = clrType; _typeConfigurationSource = typeConfigurationSource; _builder = new InternalPropertyBuilder(this, declaringType.Model.Builder); + + if (elementType != null) + { + SetAnnotation( + CoreAnnotationNames.ElementType, + new ElementType(elementType, this, configurationSource), + configurationSource); + } } /// @@ -1411,6 +1420,38 @@ public virtual CoreTypeMapping? TypeMapping public virtual ElementType? GetElementType() => (ElementType?)this[CoreAnnotationNames.ElementType]; + /// + /// 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 ElementType? SetElementType( + Type? elementType, + ConfigurationSource configurationSource) + { + var existingElementType = GetElementType(); + if (elementType != null) + { + if (elementType != existingElementType?.ClrType) + { + var newElementType = new ElementType(elementType, this, configurationSource); + SetAnnotation(CoreAnnotationNames.ElementType, newElementType, configurationSource); + return newElementType; + } + + return existingElementType; + } + + if (existingElementType != null) + { + existingElementType.SetRemovedFromModel(); + RemoveAnnotation(CoreAnnotationNames.ElementType); + } + + return null; + } + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 3116e930c0c..c9d6bc73f8d 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -686,20 +686,12 @@ private void CheckDiscriminatorProperty(Property? property) var property = new Property( name, propertyType, memberInfo as PropertyInfo, memberInfo as FieldInfo, this, - configurationSource, typeConfigurationSource); + configurationSource, typeConfigurationSource, elementType); _properties.Add(property.Name, property); Model.AddProperty(property); - if (elementType != null) - { - property.SetAnnotation( - CoreAnnotationNames.ElementType, - new ElementType(elementType, property, configurationSource), - configurationSource); - } - if (Model.Configuration != null) { using (Model.ConventionDispatcher.DelayConventions()) From 88f0619a3463f38c7f81837b816ad0a4f6caaf31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:42:19 +0000 Subject: [PATCH 5/6] Reconcile stale convention element type at model finalization Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Metadata/Conventions/ElementMappingConvention.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs index f94bde1fea6..5885b6a355b 100644 --- a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs @@ -39,15 +39,22 @@ void Validate(IConventionTypeBase typeBase) { foreach (var property in typeBase.GetDeclaredProperties()) { + var collectionProperty = (Property)property; var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property); if (typeMapping is { ElementTypeMapping: not null }) { - var collectionProperty = (Property)property; collectionProperty.DeclaringType.Builder.SetElementType( collectionProperty.Builder, property.ClrType.TryGetElementType(typeof(IEnumerable<>)), ConfigurationSource.Convention); } + else if (collectionProperty.GetElementType() != null) + { + // The element type was discovered eagerly from the CLR type, but the resolved mapping is not a + // collection (e.g. a value converter applies), so remove the now-invalid element type. + collectionProperty.DeclaringType.Builder.SetElementType( + collectionProperty.Builder, null, ConfigurationSource.Convention); + } } foreach (var complexProperty in typeBase.GetDeclaredComplexProperties()) From 9f215710a7acc87c9014afe0f4873cfb4f18247d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 05:14:01 +0000 Subject: [PATCH 6/6] Fold element-type reconciliation into ElementTypeChangedConvention; remove ElementMappingConvention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore/EFCore.baseline.json | 21 ++---- .../Conventions/ElementMappingConvention.cs | 66 ------------------- .../ElementTypeChangedConvention.cs | 33 +++++++++- .../ProviderConventionSetBuilder.cs | 1 - .../ComplexTypesTrackingTestBase.cs | 4 +- 5 files changed, 38 insertions(+), 87 deletions(-) delete mode 100644 src/EFCore/Metadata/Conventions/ElementMappingConvention.cs diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 199e1b8272e..b9f326f3278 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -6967,22 +6967,6 @@ } ] }, - { - "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementMappingConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IModelFinalizingConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention", - "Methods": [ - { - "Member": "ElementMappingConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" - }, - { - "Member": "void ProcessModelFinalizing(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder modelBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" - } - ], - "Properties": [ - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies Dependencies { get; }" - } - ] - }, { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Builders.ElementTypeBuilder : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure", "Methods": [ @@ -7045,7 +7029,7 @@ ] }, { - "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementTypeChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyPropertiesChangedConvention", + "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.ElementTypeChangedConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyPropertiesChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IModelFinalizingConvention", "Methods": [ { "Member": "ElementTypeChangedConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" @@ -7055,6 +7039,9 @@ }, { "Member": "void ProcessForeignKeyPropertiesChanged(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionForeignKeyBuilder relationshipBuilder, System.Collections.Generic.IReadOnlyList oldDependentProperties, Microsoft.EntityFrameworkCore.Metadata.IConventionKey oldPrincipalKey, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext> context);" + }, + { + "Member": "void ProcessModelFinalizing(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder modelBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" } ], "Properties": [ diff --git a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs b/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs deleted file mode 100644 index 5885b6a355b..00000000000 --- a/src/EFCore/Metadata/Conventions/ElementMappingConvention.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; - -/// -/// A convention that ensures property mappings have any ElementMapping discovered by the type mapper. -/// -/// -/// -/// See Model building conventions for more information and examples. -/// -/// -public class ElementMappingConvention : IModelFinalizingConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - public ElementMappingConvention(ProviderConventionSetBuilderDependencies dependencies) - => Dependencies = dependencies; - - /// - /// Dependencies for this service. - /// - protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } - - /// - public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) - { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) - { - Validate(entityType); - } - - void Validate(IConventionTypeBase typeBase) - { - foreach (var property in typeBase.GetDeclaredProperties()) - { - var collectionProperty = (Property)property; - var typeMapping = Dependencies.TypeMappingSource.FindMapping((IProperty)property); - if (typeMapping is { ElementTypeMapping: not null }) - { - collectionProperty.DeclaringType.Builder.SetElementType( - collectionProperty.Builder, - property.ClrType.TryGetElementType(typeof(IEnumerable<>)), - ConfigurationSource.Convention); - } - else if (collectionProperty.GetElementType() != null) - { - // The element type was discovered eagerly from the CLR type, but the resolved mapping is not a - // collection (e.g. a value converter applies), so remove the now-invalid element type. - collectionProperty.DeclaringType.Builder.SetElementType( - collectionProperty.Builder, null, ConfigurationSource.Convention); - } - } - - foreach (var complexProperty in typeBase.GetDeclaredComplexProperties()) - { - Validate(complexProperty.ComplexType); - } - } - } -} diff --git a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs index c738b7a7725..086b127ddeb 100644 --- a/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs +++ b/src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs @@ -13,7 +13,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// public class ElementTypeChangedConvention : IForeignKeyAddedConvention, - IForeignKeyPropertiesChangedConvention + IForeignKeyPropertiesChangedConvention, + IModelFinalizingConvention { /// /// Creates a new instance of . @@ -58,4 +59,34 @@ private static void ProcessForeignKey(IConventionForeignKeyBuilder foreignKeyBui foreignKeyProperty.Builder, principalKeyProperties[i].GetElementType()?.ClrType, ConfigurationSource.Convention); } } + + /// + public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + ReconcileElementTypes(entityType); + } + + void ReconcileElementTypes(IConventionTypeBase typeBase) + { + foreach (var property in typeBase.GetDeclaredProperties()) + { + var collectionProperty = (Property)property; + if (collectionProperty.GetElementType() != null + && Dependencies.TypeMappingSource.FindMapping((IProperty)property) is not { ElementTypeMapping: not null }) + { + // The element type was discovered eagerly from the CLR type, but the resolved mapping is not a + // collection (e.g. an inherited value converter applies), so remove the now-invalid element type. + collectionProperty.DeclaringType.Builder.SetElementType( + collectionProperty.Builder, null, ConfigurationSource.Convention); + } + } + + foreach (var complexProperty in typeBase.GetDeclaredComplexProperties()) + { + ReconcileElementTypes(complexProperty.ComplexType); + } + } + } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index f92e934b0b4..118d74ea0b6 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -100,7 +100,6 @@ public virtual ConventionSet CreateConventionSet() conventionSet.Add(new QueryFilterRewritingConvention(Dependencies)); conventionSet.Add(new AutoLoadConvention(Dependencies)); conventionSet.Add(new RuntimeModelConvention(Dependencies)); - conventionSet.Add(new ElementMappingConvention(Dependencies)); conventionSet.Add(new ElementTypeChangedConvention(Dependencies)); return conventionSet; diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs index 3dfe9ec25e4..a31725cd1e8 100644 --- a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -2394,7 +2394,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con e => e.Teams, "TeamPropertyBag", teamBuilder => { teamBuilder.Property("Name"); - teamBuilder.Property>("Members"); + teamBuilder.PrimitiveCollection>("Members"); teamBuilder.Property("Founded"); teamBuilder.Property("IsActive"); teamBuilder.Property("Rating"); @@ -2404,7 +2404,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con e => e.FeaturedTeam, "FeaturedTeamPropertyBag", featuredTeamBuilder => { featuredTeamBuilder.Property("Name"); - featuredTeamBuilder.Property>("Members"); + featuredTeamBuilder.PrimitiveCollection>("Members"); featuredTeamBuilder.Property("Founded"); featuredTeamBuilder.Property("IsActive"); featuredTeamBuilder.Property("Rating");