Make primitive collection element type a creation-time concern#38484
Make primitive collection element type a creation-time concern#38484Copilot wants to merge 6 commits into
Conversation
… and convention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
…solution, add clarifying comments Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
| { | ||
| property.SetAnnotation( | ||
| CoreAnnotationNames.ElementType, | ||
| new ElementType(elementType, property, configurationSource), |
There was a problem hiding this comment.
@copilot Pass elementType to the Property constructor and only instantiate ElementType from Property
Also update RuntimeProperty and related types, like CSharpRuntimeModelCodeGenerator
There was a problem hiding this comment.
elementType is now passed to the Property constructor, and ElementType is only instantiated from Property (constructor + the new Property.SetElementType).
On the runtime side, RuntimeProperty.SetElementType already instantiates RuntimeElementType exclusively within RuntimeProperty, and CSharpRuntimeModelCodeGenerator continues to emit .SetElementType(...) against the runtime property — so no change was required there. If you'd prefer the runtime element type to also be a constructor parameter (threading all element facets through the RuntimeProperty ctor and updating the codegen accordingly), let me know and I'll do that follow-up.
There was a problem hiding this comment.
@copilot Remove RuntimeProperty.SetElementType and pass everything needed in the constructor
There was a problem hiding this comment.
Before I regenerate the ~49 compiled-model baselines (every provider) + the API baselines this entails, I want to confirm the shape, since RuntimeElementType needs a back-reference to its owning RuntimeProperty so it can't simply be passed pre-built.
Two options:
-
Factory parameter — add
Func<RuntimeProperty, RuntimeElementType>? elementType = nullto theRuntimePropertyconstructor and bothAddPropertyoverloads. The ctor invokes it (SetAnnotation(ElementType, elementType(this)); IsPrimitiveCollection = true;). Generated code emitselementType: static p => new RuntimeElementType(typeof(...), p, nullable: ..., ...)and retrieves it via(RuntimeElementType)prop.GetElementType()!for the subsequent converter/comparer/type-mapping/annotation calls. Smallest signature change;RuntimeElementType's ctor already takes everything. -
Flat facets — thread the element's facets (clrType, nullable, maxLength, unicode, precision, scale, providerClrType, valueConverter, valueComparer, jsonValueReaderWriter, typeMapping) directly through the
RuntimePropertyconstructor + bothAddPropertyoverloads, building theRuntimeElementTypeinside the ctor. Most literal reading of "pass everything in the constructor", but adds ~11 params and a larger codegen restructure (weaving the element args into theAddPropertycall).
I'd lean toward (1) as the cleaner, lower-churn option. Which do you prefer? I'll implement it and regenerate all baselines right away.
For context, the rest of this review round is already done and validated (ElementMappingConvention removed/folded, property-bag tests updated, API baseline regenerated).
…e; keep ElementMappingConvention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
| { | ||
| property.SetAnnotation( | ||
| CoreAnnotationNames.ElementType, | ||
| new ElementType(elementType, property, configurationSource), |
There was a problem hiding this comment.
@copilot Remove RuntimeProperty.SetElementType and pass everything needed in the constructor
…emove ElementMappingConvention Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors EF Core’s primitive-collection element type handling to be a creation-time concern (set during discovery/configuration) and removes finalization-time promotion, while adding a finalization-time cleanup to demote stale convention-set element types when the resolved mapping is scalar (e.g., due to an inherited value converter).
Changes:
- Remove
ElementMappingConventionand the property element-type-changed convention pipeline; element types are no longer assigned at model finalization. - Introduce primitive-collection-specific configuration APIs (
PrimitiveCollection(...),AddProperty(..., elementType)) and flow the element type into property creation. - Update conventions and tests to configure primitive collections explicitly (and update API baselines accordingly).
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/EFCore.Tests/Metadata/Internal/PropertyTest.cs | Updates tests to set element type at property creation time via new overload. |
| test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs | Removes coverage for the removed property-element-type-changed convention dispatch; updates element-type tests to use PrimitiveCollection. |
| test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs | Updates property-bag mappings to use PrimitiveCollection for collection properties. |
| src/EFCore/Metadata/MemberClassifier.cs | Extends primitive property candidate classification to also output discovered collection element type when applicable. |
| src/EFCore/Metadata/Internal/TypeBase.cs | Threads optional element type through internal AddProperty creation path and adds an IMutableTypeBase.AddProperty(..., elementType) implementation. |
| src/EFCore/Metadata/Internal/Property.cs | Allows element type annotation to be set during Property construction; adjusts SetElementType semantics and removes old convention hook. |
| src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs | Adds internal element-type setting helper and convention-level PrimitiveCollection plumbing; adds IConventionTypeBaseBuilder implementations. |
| src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs | Ensures setting scalar conversions clears element type; removes obsolete element-type APIs. |
| src/EFCore/Metadata/IMutableTypeBase.cs | Adds public mutable metadata API for adding a primitive collection property with explicit element type. |
| src/EFCore/Metadata/IMutableProperty.cs | Removes mutable property SetElementType API. |
| src/EFCore/Metadata/IMemberClassifier.cs | Updates signature/docs to include out Type? elementType. |
| src/EFCore/Metadata/IConventionProperty.cs | Removes convention property SetElementType API. |
| src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs | Switches discovery to create primitive collections via PrimitiveCollection(...) rather than setting element type later. |
| src/EFCore/Metadata/Conventions/NonNullableReferencePropertyConvention.cs | Removes dependency on the removed property element-type-changed convention. |
| src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs | Deletes the property element-type-changed convention interface. |
| src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs | Removes immediate-scope dispatch for property element type changes. |
| src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs | Removes delayed-node plumbing for property element type changes. |
| src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs | Removes dispatcher entry point for property element type changes. |
| src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs | Removes abstract convention-scope method for property element type changes. |
| src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs | Removes ElementMappingConvention from the default convention set. |
| src/EFCore/Metadata/Conventions/ElementTypeChangedConvention.cs | Reworks element-type propagation to FKs without the removed public convention API; adds finalization-time demotion of stale convention-set element types. |
| src/EFCore/Metadata/Conventions/ElementMappingConvention.cs | Deletes the finalization-time element-type assignment convention. |
| src/EFCore/Metadata/Conventions/ConventionSet.cs | Removes storage/registration plumbing for property element-type-changed conventions. |
| src/EFCore/Metadata/Builders/IConventionTypeBaseBuilder.cs | Adds convention-builder APIs for PrimitiveCollection and CanHavePrimitiveCollection. |
| src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs | Removes convention property builder element-type APIs. |
| src/EFCore/EFCore.baseline.json | Updates API baseline for removed conventions/APIs and added primitive-collection APIs. |
| src/EFCore.Relational/Metadata/RelationalMemberClassifier.cs | Updates override signature for IsCandidatePrimitiveProperty to include out elementType. |
| src/EFCore.Relational/EFCore.Relational.baseline.json | Updates relational API baseline due to signature changes and baseline regeneration effects. |
| else | ||
| { | ||
| property.SetElementType(null, configurationSource); | ||
|
|
||
| if (propertyBuilder.CanSetConversion((Type?)null, configurationSource)) | ||
| { | ||
| property.RemoveAnnotation(CoreAnnotationNames.ValueConverter); | ||
| } | ||
| } |
| 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); |
| 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); |
| "Stage": "Obsolete" | ||
| "Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);" | ||
| } | ||
| ] |
The element-type-as-creation-time refactoring eagerly assigns an element type during property discovery whenever the member's resolved mapping has an
ElementTypeMapping. A property whose CLR type implementsIEnumerable<>but later resolves to a scalar via a value converter kept that stale element type, since the converter is inherited on-demand from the principal key (Property.GetConversion()) with no set-time hook to remove it. At finalization this caused anInvalidCastExceptionwhen building the provider value comparer.The element type is now purely a creation-time concern: it is derived from the property type when the property is configured as a primitive collection (via
PrimitiveCollection/discovery). A property is a primitive collection only if configured as one — there is no finalization-time promotion. The only remaining finalization work is to demote a stale element type that was speculatively assigned during discovery but turns out not to be a collection once the mapping resolves (e.g. an inherited value converter makes the property scalar).Changes
ElementMappingConvention: removed. The convention no longer assigns element types at finalization.ElementTypeChangedConvention: now also anIModelFinalizingConvention. InProcessModelFinalizingit removes a convention-set element type when the resolved mapping has noElementTypeMapping(e.g. a value converter applies). Removal usesConfigurationSource.Convention, so explicitly configured primitive collections are left untouched (Convention does not override Explicit). Because it only acts on theElementTypeMapping is nullcase and never compares element CLR types, the nullable-inner-element case (e.g.List<int?[]>) is a non-issue.Property<List<string>>(...)toPrimitiveCollection<List<string>>(...), consistent with element types coming from the configured primitive collection rather than a finalization convention.EFCore.baseline.jsonregenerated (removedElementMappingConvention;ElementTypeChangedConventionnow implementsIModelFinalizingConvention).Note: removing
RuntimeProperty.SetElementTypeand threading the element type through the constructor is being tracked separately, pending confirmation of the constructor shape, as it requires regenerating the compiled-model baselines across all providers.