Skip to content

Allow to configure properties on non-collection complex types by using chaining in the lambda expression#38154

Merged
AndriySvyryd merged 2 commits into
mainfrom
Issue31236
Apr 29, 2026
Merged

Allow to configure properties on non-collection complex types by using chaining in the lambda expression#38154
AndriySvyryd merged 2 commits into
mainfrom
Issue31236

Conversation

@AndriySvyryd
Copy link
Copy Markdown
Member

Fixes #31236

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR enables configuring nested (non-collection) complex type properties via chained member-access lambdas and dotted-string property paths, adding supporting builder plumbing, error messages, and specification tests (Fixes #31236).

Changes:

  • Added complex-property chain resolution for lambda-based configuration and dotted string names (including auto-creation for lambda intermediates).
  • Introduced new CoreStrings messages for invalid chained/dotted cases (invalid segment, invalid intermediate, collection intermediate, missing intermediate).
  • Expanded model-building tests to validate chained/dotted configuration scenarios and failure modes.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.TestModel.cs Adds new nested complex CLR types used by the new chaining tests.
test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.OwnedTypes.cs Adds tests ensuring owned navigations are rejected as chain intermediates.
test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.NonGeneric.cs Updates non-generic test wrappers to pass dotted names derived from member chains.
test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs Adds extensive tests for chained lambda and dotted-name configuration of nested complex members.
test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexCollections.cs Adds a dotted-name test for configuring a nested complex collection.
src/EFCore/Properties/CoreStrings.resx Adds new error message resources for complex-property chain resolution failures.
src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Adds member-chain overloads for Property/PrimitiveCollection/ComplexProperty/Ignore.
src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs Enables chained lambda member access for generic entity type builder APIs.
src/EFCore/Metadata/Builders/EntityTypeBuilder.cs Adds dotted-name support to non-generic builder overloads.
src/EFCore/Metadata/Builders/ComplexPropertyChainHelper.cs New helper to parse/resolve member chains and dotted paths through complex types.
src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs Enables chained lambda member access for complex property builder APIs.
src/EFCore/Metadata/Builders/ComplexPropertyBuilder.cs Adds dotted-name support to non-generic complex property builder overloads.
src/EFCore/Metadata/Builders/ComplexCollectionBuilder`.cs Enables chained lambda member access for complex collection builder APIs.
src/EFCore/Metadata/Builders/ComplexCollectionBuilder.cs Adds dotted-name support to non-generic complex collection builder overloads.
src/EFCore/Extensions/Internal/ExpressionExtensions.cs Adds MatchMemberAccessChain() to extract chained member accesses from lambdas.
src/EFCore/EFCore.baseline.json Updates API baseline for newly-added CoreStrings members.
Files not reviewed (1)
  • src/EFCore/Properties/CoreStrings.Designer.cs: Language not supported

Comment thread src/EFCore/Metadata/Builders/ComplexPropertyChainHelper.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
Comment thread test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • src/EFCore/Properties/CoreStrings.Designer.cs: Language not supported

Comment thread src/EFCore/Properties/CoreStrings.Designer.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
@AndriySvyryd AndriySvyryd force-pushed the Issue31236 branch 2 times, most recently from 1c9a825 to d94e0d4 Compare April 23, 2026 22:33
@AndriySvyryd AndriySvyryd marked this pull request as ready for review April 23, 2026 23:14
@AndriySvyryd AndriySvyryd requested a review from a team as a code owner April 23, 2026 23:14
Copilot AI review requested due to automatic review settings April 23, 2026 23:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/EFCore/Properties/CoreStrings.Designer.cs: Language not supported

Comment thread src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Comment thread src/EFCore/Properties/CoreStrings.resx
Comment on lines +127 to +128
var (innerBuilder, leafName) = Builder.ResolveComplexChainByName(propertyName);
return new PropertyBuilder(innerBuilder.Property(leafName, ConfigurationSource.Explicit)!.Metadata);
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.

nit: if we want to keep nicer/shorter expression-based code, we can have a method on Builder which accepts the name and a lambda, and internally resolved the inner builder and invokes the lambda on it. Callers would then pass (static) lambdas doing the actual call (Property, PrimitiveCollection).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I thought about different ways to factor this and I think the current approach offers the best balance between clarity for which methods support chained calls, verbosity of the implementation, and performance.

Comment thread src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs Outdated
@AndriySvyryd AndriySvyryd merged commit baa5c13 into main Apr 29, 2026
23 checks passed
@AndriySvyryd AndriySvyryd deleted the Issue31236 branch April 29, 2026 04:41
@github-actions github-actions Bot added the api-review This PR or issue is introducing public API changes that need to be reviewed label Apr 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

API review baseline changes for src/EFCore/EFCore.baseline.json

Show diff

The diff below was generated by ApiChief between the base and the PR.

  static class Microsoft.EntityFrameworkCore.Diagnostics.CoreEventId
- static readonly Microsoft.Extensions.Logging.EventId NonDefiningInverseNavigationWarning
  static class Microsoft.EntityFrameworkCore.Diagnostics.CoreStrings
+ static string ComplexPropertyChainIntermediateNotFound(object? member, object? type);
+ static string ComplexPropertyChainInvalidMember(object? member, object? type);
+ static string ComplexPropertyChainInvalidSegment(object? dottedName);
+ static string ComplexPropertyChainOnCollection(object? member, object? type);
+ static string InvalidMemberAccessChainExpression(object? expression);
  abstract class Microsoft.EntityFrameworkCore.Infrastructure.AnnotatableBuilder<TMetadata, TModelBuilder> : Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionAnnotatableBuilder where TMetadata : Microsoft.EntityFrameworkCore.Infrastructure.ConventionAnnotatable where TModelBuilder : Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder
- virtual AnnotatableBuilder<TMetadata, TModelBuilder>? RemoveAnnotation(string name, ConfigurationSource configurationSource);
  class Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>, Microsoft.EntityFrameworkCore.Storage.IDatabaseFacadeDependenciesAccessor, Microsoft.EntityFrameworkCore.Infrastructure.IResettableService
- virtual bool AutoTransactionsEnabled { get; set; }
  interface Microsoft.EntityFrameworkCore.Metadata.IConstructorBindingFactory
- bool TryBindConstructor(IConventionEntityType entityType, ConstructorInfo constructor, out InstantiationBinding? binding, out IEnumerable<ParameterInfo>? unboundParameters);
- bool TryBindConstructor(IMutableEntityType entityType, ConstructorInfo constructor, out InstantiationBinding? binding, out IEnumerable<ParameterInfo>? unboundParameters);
  interface Microsoft.EntityFrameworkCore.Metadata.IConventionModel : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyModel, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, Microsoft.EntityFrameworkCore.Metadata.IConventionAnnotatable
- IConventionEntityType? AddEntityType(Type type, string definingNavigationName, IConventionEntityType definingEntityType, bool fromDataAnnotation = false);
- IConventionEntityType? AddEntityType(string name, string definingNavigationName, IConventionEntityType definingEntityType, bool fromDataAnnotation = false);
  interface Microsoft.EntityFrameworkCore.Metadata.IConventionProperty : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, Microsoft.EntityFrameworkCore.Metadata.IConventionPropertyBase, Microsoft.EntityFrameworkCore.Metadata.IConventionAnnotatable
- IConventionEntityType DeclaringEntityType { get; }
  interface Microsoft.EntityFrameworkCore.Metadata.IMutableProperty : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, Microsoft.EntityFrameworkCore.Metadata.IMutablePropertyBase, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable
- IMutableEntityType DeclaringEntityType { get; }
  interface Microsoft.EntityFrameworkCore.Metadata.IProperty : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, Microsoft.EntityFrameworkCore.Metadata.IPropertyBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable
- IEntityType DeclaringEntityType { get; }
  interface Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyTypeBase, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable
- LambdaExpression? GetQueryFilter();
  interface Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty : Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable
- IReadOnlyEntityType DeclaringEntityType { get; }
  class Microsoft.EntityFrameworkCore.Metadata.RuntimeModel : Microsoft.EntityFrameworkCore.Infrastructure.RuntimeAnnotatableBase, Microsoft.EntityFrameworkCore.Metadata.Internal.IRuntimeModel, Microsoft.EntityFrameworkCore.Metadata.IModel, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyModel, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable
- virtual void SetSkipDetectChanges(bool skipDetectChanges);
  class Microsoft.EntityFrameworkCore.Query.ParameterQueryRootExpression : Microsoft.EntityFrameworkCore.Query.QueryRootExpression
- virtual ParameterExpression ParameterExpression { get; }
- ParameterQueryRootExpression(IAsyncQueryProvider asyncQueryProvider, Type elementType, ParameterExpression parameterExpression);
- ParameterQueryRootExpression(Type elementType, ParameterExpression parameterExpression);
  abstract class Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping
- virtual Func<IProperty, IEntityType, ValueGenerator>? ValueGeneratorFactory { get; }
  interface Microsoft.EntityFrameworkCore.Storage.ITypeMappingSource
- CoreTypeMapping? FindMapping(MemberInfo member);
  class Microsoft.EntityFrameworkCore.Storage.ValueConversion.ConverterMappingHints
- ConverterMappingHints(int? size = null, int? precision = null, int? scale = null, bool? unicode = null, Func<IProperty, IEntityType, ValueGenerator>? valueGeneratorFactory = null);
+ ConverterMappingHints(int? size = null, int? precision = null, int? scale = null, bool? unicode = null);
- virtual Func<IProperty, IEntityType, ValueGenerator>? ValueGeneratorFactory { get; }
  interface Microsoft.EntityFrameworkCore.ValueGeneration.IValueGeneratorSelector
- ValueGenerator? Select(IProperty property, ITypeBase typeBase);
  class Microsoft.EntityFrameworkCore.ValueGeneration.ValueGeneratorSelector : Microsoft.EntityFrameworkCore.ValueGeneration.IValueGeneratorSelector
- virtual ValueGenerator Create(IProperty property, ITypeBase typeBase);
- virtual ValueGenerator? Select(IProperty property, ITypeBase typeBase);

@AndriySvyryd AndriySvyryd removed the api-review This PR or issue is introducing public API changes that need to be reviewed label May 11, 2026
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.

Allow to configure properties on non-collection complex types by using chaining in the lambda expression

3 participants