Skip to content

Make primitive collection element type a creation-time concern#38484

Draft
Copilot wants to merge 6 commits into
mainfrom
copilot/add-imemberclassifier-elementtype
Draft

Make primitive collection element type a creation-time concern#38484
Copilot wants to merge 6 commits into
mainfrom
copilot/add-imemberclassifier-elementtype

Conversation

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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 implements IEnumerable<> 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 an InvalidCastException when building the provider value comparer.

// CustomId : IEnumerable<byte>, with a key value converter CustomId -> int
modelBuilder.Entity<ValueCategory>().Property(x => x.Id)
    .HasConversion(x => x.Id, x => new CustomId { Id = x });

// The owned FK property inherits the converter from the principal key.
// It was discovered as a primitive collection of byte, and the element type
// was never removed -> ValueConverter<CustomId,int> cast to
// ValueConverter<IEnumerable<byte>,int> at model finalization.
modelBuilder.Entity<QueryResult>().OwnsOne(q => q.Value).Property(x => x.CategoryId);

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 an IModelFinalizingConvention. In ProcessModelFinalizing it removes a convention-set element type when the resolved mapping has no ElementTypeMapping (e.g. a value converter applies). Removal uses ConfigurationSource.Convention, so explicitly configured primitive collections are left untouched (Convention does not override Explicit). Because it only acts on the ElementTypeMapping is null case and never compares element CLR types, the nullable-inner-element case (e.g. List<int?[]>) is a non-issue.
  • Tests: property-bag collection declarations updated from Property<List<string>>(...) to PrimitiveCollection<List<string>>(...), consistent with element types coming from the configured primitive collection rather than a finalization convention.
  • API baseline: EFCore.baseline.json regenerated (removed ElementMappingConvention; ElementTypeChangedConvention now implements IModelFinalizingConvention).

Note: removing RuntimeProperty.SetElementType and 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.

Copilot AI and others added 2 commits June 23, 2026 20:39
… 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>
Comment thread src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs
Comment thread src/EFCore/Metadata/Internal/Property.cs Outdated
Comment thread src/EFCore/Metadata/IMemberClassifier.cs Outdated
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Comment thread src/EFCore/Metadata/Conventions/ElementMappingConvention.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
{
property.SetAnnotation(
CoreAnnotationNames.ElementType,
new ElementType(elementType, property, configurationSource),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Pass elementType to the Property constructor and only instantiate ElementType from Property
Also update RuntimeProperty and related types, like CSharpRuntimeModelCodeGenerator

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Remove RuntimeProperty.SetElementType and pass everything needed in the constructor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Factory parameter — add Func<RuntimeProperty, RuntimeElementType>? elementType = null to the RuntimeProperty constructor and both AddProperty overloads. The ctor invokes it (SetAnnotation(ElementType, elementType(this)); IsPrimitiveCollection = true;). Generated code emits elementType: 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.

  2. Flat facets — thread the element's facets (clrType, nullable, maxLength, unicode, precision, scale, providerClrType, valueConverter, valueComparer, jsonValueReaderWriter, typeMapping) directly through the RuntimeProperty constructor + both AddProperty overloads, building the RuntimeElementType inside 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 the AddProperty call).

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).

Comment thread src/EFCore/Metadata/Conventions/ElementMappingConvention.cs Outdated
…e; keep ElementMappingConvention

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title Make primitive collection element type a creation-time concern Reconcile stale element type at model finalization for converter-backed properties Jun 24, 2026
Comment thread src/EFCore/EFCore.baseline.json Outdated
Comment thread src/EFCore/Metadata/Conventions/ElementMappingConvention.cs Outdated
Comment thread src/EFCore/Metadata/Conventions/ElementMappingConvention.cs Outdated
{
property.SetAnnotation(
CoreAnnotationNames.ElementType,
new ElementType(elementType, property, configurationSource),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Remove RuntimeProperty.SetElementType and pass everything needed in the constructor

…emove ElementMappingConvention

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title Reconcile stale element type at model finalization for converter-backed properties Make primitive collection element type a creation-time concern Jun 24, 2026
Copilot AI requested a review from AndriySvyryd June 24, 2026 05:26
@AndriySvyryd AndriySvyryd requested a review from Copilot June 24, 2026 07:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ElementMappingConvention and 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.

Comment on lines +401 to +409
else
{
property.SetElementType(null, configurationSource);

if (propertyBuilder.CanSetConversion((Type?)null, configurationSource))
{
property.RemoveAnnotation(CoreAnnotationNames.ValueConverter);
}
}
Comment on lines +2316 to +2328
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);
Comment on lines +2337 to +2343
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);
Comment on lines -14556 to 14583
"Stage": "Obsolete"
"Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder ToJson(this Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationBuilder builder, string? jsonColumnName);"
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants