From e318524378674c66bb60134d8746a9344bce3b53 Mon Sep 17 00:00:00 2001 From: lajones Date: Sun, 8 Mar 2020 11:50:04 -0700 Subject: [PATCH] Partial Fix for 6674. Allow inline configuration of Navigations as they are created (#20195) Partial Fix for 6674. Updated methods for all HasOne-WithMany, HasMany-WithOne and HasOne-WithOne. Generic and non-generic. --- .../Builders/CollectionNavigationBuilder.cs | 40 ++++-- .../Builders/CollectionNavigationBuilder`.cs | 25 +++- .../Metadata/Builders/EntityTypeBuilder.cs | 123 ++++++++++++++--- .../Metadata/Builders/EntityTypeBuilder`.cs | 73 +++++++--- .../Builders/IConventionNavigationBuilder.cs | 33 +++++ .../Metadata/Builders/NavigationBuilder.cs | 119 ++++++++++++++++ .../Builders/OwnedNavigationBuilder`.cs | 8 +- .../Builders/ReferenceNavigationBuilder.cs | 77 +++++++++-- .../Builders/ReferenceNavigationBuilder`.cs | 63 +++++++-- .../Internal/InternalNavigationBuilder.cs | 47 ++++++- .../Internal/InternalPropertyBaseBuilder`.cs | 58 ++++++++ .../Internal/InternalPropertyBuilder.cs | 26 +--- .../FieldMappingTestBase.cs | 61 ++++++++ test/EFCore.Tests/ApiConsistencyTest.cs | 1 + .../ModelBuilding/ManyToOneTestBase.cs | 72 ++++++++++ ...delBuilderGenericRelationshipStringTest.cs | 35 +++-- ...ModelBuilderGenericRelationshipTypeTest.cs | 11 +- .../ModelBuilding/ModelBuilderGenericTest.cs | 55 ++++++-- .../ModelBuilderNonGenericStringTest.cs | 21 ++- .../ModelBuilderNonGenericTest.cs | 67 +++++++-- ...lBuilderNonGenericUnqualifiedStringTest.cs | 21 +-- .../ModelBuilding/ModelBuilderTestBase.cs | 37 ++++- .../ModelBuilding/OneToManyTestBase.cs | 42 ++++++ .../ModelBuilding/OneToOneTestBase.cs | 130 ++++++++++++++++++ test/EFCore.Tests/ModelBuilding/TestModel.cs | 47 +++++++ 25 files changed, 1133 insertions(+), 159 deletions(-) create mode 100644 src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/NavigationBuilder.cs create mode 100644 src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs diff --git a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder.cs b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder.cs index 96a2059bf19..1340e4b6317 100644 --- a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder.cs @@ -121,12 +121,19 @@ public CollectionNavigationBuilder( /// The name of the reference navigation property on the other end of this relationship. /// If null or not specified, then there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. - public virtual ReferenceCollectionBuilder WithOne([CanBeNull] string navigationName = null) + public virtual ReferenceCollectionBuilder WithOne( + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) => new ReferenceCollectionBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(Check.NullButNotEmpty(navigationName, nameof(navigationName))).Metadata); + WithOneBuilder( + Check.NullButNotEmpty(navigationName, nameof(navigationName)), + navigationConfiguration).Metadata); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -135,8 +142,10 @@ public virtual ReferenceCollectionBuilder WithOne([CanBeNull] string navigationN /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] string navigationName) - => WithOneBuilder(MemberIdentity.Create(navigationName)); + protected virtual InternalRelationshipBuilder WithOneBuilder( + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) + => WithOneBuilder(MemberIdentity.Create(navigationName), navigationConfiguration); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -145,10 +154,14 @@ protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] string /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] MemberInfo navigationMemberInfo) - => WithOneBuilder(MemberIdentity.Create(navigationMemberInfo)); - - private InternalRelationshipBuilder WithOneBuilder(MemberIdentity reference) + protected virtual InternalRelationshipBuilder WithOneBuilder( + [CanBeNull] MemberInfo navigationMemberInfo, + [CanBeNull] Action navigationConfiguration = null) + => WithOneBuilder(MemberIdentity.Create(navigationMemberInfo), navigationConfiguration); + + private InternalRelationshipBuilder WithOneBuilder( + MemberIdentity reference, + Action navigationConfiguration = null) { if (SkipNavigation != null) { @@ -174,7 +187,7 @@ private InternalRelationshipBuilder WithOneBuilder(MemberIdentity reference) InternalRelationshipBuilder.ThrowForConflictingNavigation(foreignKey, referenceName, newToPrincipal: true); } - return reference.MemberInfo == null || CollectionMember == null + var withOneBuilder = reference.MemberInfo == null || CollectionMember == null ? Builder.HasNavigations( reference.Name, CollectionName, (EntityType)DeclaringEntityType, (EntityType)RelatedEntityType, @@ -183,6 +196,15 @@ private InternalRelationshipBuilder WithOneBuilder(MemberIdentity reference) reference.MemberInfo, CollectionMember, (EntityType)DeclaringEntityType, (EntityType)RelatedEntityType, ConfigurationSource.Explicit); + + if (navigationConfiguration != null + && withOneBuilder.Metadata.DependentToPrincipal != null) + { + navigationConfiguration( + new NavigationBuilder(withOneBuilder.Metadata.DependentToPrincipal)); + } + + return withOneBuilder; } /// diff --git a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs index 6d2018d2227..2b64ca7cfc8 100644 --- a/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/CollectionNavigationBuilder`.cs @@ -50,12 +50,21 @@ public CollectionNavigationBuilder( /// The name of the reference navigation property on the other end of this relationship. /// If null, there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. - public new virtual ReferenceCollectionBuilder WithOne([CanBeNull] string navigationName = null) - => new ReferenceCollectionBuilder( + public new virtual ReferenceCollectionBuilder WithOne( + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) + { + return new ReferenceCollectionBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(Check.NullButNotEmpty(navigationName, nameof(navigationName))).Metadata); + WithOneBuilder( + Check.NullButNotEmpty(navigationName, nameof(navigationName)), + navigationConfiguration).Metadata); + } /// /// @@ -72,13 +81,19 @@ public CollectionNavigationBuilder( /// relationship (post => post.Blog). If no property is specified, the relationship will be /// configured without a navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. public virtual ReferenceCollectionBuilder WithOne( - [CanBeNull] Expression> navigationExpression) + [CanBeNull] Expression> navigationExpression, + [CanBeNull] Action navigationConfiguration = null) => new ReferenceCollectionBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(navigationExpression?.GetPropertyAccess()).Metadata); + WithOneBuilder( + navigationExpression?.GetPropertyAccess(), + navigationConfiguration).Metadata); /// /// Configures this as a many-to-many relationship. diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index 9ec73c36829..4ea309109fc 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -575,23 +575,27 @@ private OwnedNavigationBuilder OwnsManyBuilder(in TypeIdentity ownedType, string /// no property is specified, the relationship will be configured without a navigation property on this /// end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual ReferenceNavigationBuilder HasOne( [NotNull] string relatedTypeName, - [CanBeNull] string navigationName) + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) { Check.NotEmpty(relatedTypeName, nameof(relatedTypeName)); Check.NullButNotEmpty(navigationName, nameof(navigationName)); var relatedEntityType = FindRelatedEntityType(relatedTypeName, navigationName); + var foreignKey = HasOneBuilder( + MemberIdentity.Create(navigationName), relatedEntityType, navigationConfiguration); return new ReferenceNavigationBuilder( Builder.Metadata, relatedEntityType, navigationName, - Builder.HasRelationship( - relatedEntityType, navigationName, ConfigurationSource.Explicit, - targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata); + foreignKey); } /// @@ -618,23 +622,27 @@ public virtual ReferenceNavigationBuilder HasOne( /// no property is specified, the relationship will be configured without a navigation property on this /// end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual ReferenceNavigationBuilder HasOne( [NotNull] Type relatedType, - [CanBeNull] string navigationName = null) + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) { Check.NotNull(relatedType, nameof(relatedType)); Check.NullButNotEmpty(navigationName, nameof(navigationName)); var relatedEntityType = FindRelatedEntityType(relatedType, navigationName); + var foreignKey = HasOneBuilder( + MemberIdentity.Create(navigationName), relatedEntityType, navigationConfiguration); return new ReferenceNavigationBuilder( Builder.Metadata, relatedEntityType, navigationName, - Builder.HasRelationship( - relatedEntityType, navigationName, ConfigurationSource.Explicit, - targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata); + foreignKey); } /// @@ -654,15 +662,61 @@ public virtual ReferenceNavigationBuilder HasOne( /// The name of the reference navigation property on this entity type that represents /// the relationship. The navigation must be a CLR property on the entity type. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual ReferenceNavigationBuilder HasOne( - [NotNull] string navigationName) + [NotNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) { Check.NotEmpty(navigationName, nameof(navigationName)); return Metadata.ClrType == null - ? HasOne(navigationName, null) // Path only used by pre 3.0 snapshots - : HasOne(Metadata.GetNavigationMemberInfo(navigationName).GetMemberType(), navigationName); + ? HasOne(navigationName, null, navigationConfiguration) // Path only used by pre 3.0 snapshots + : HasOne(Metadata.GetNavigationMemberInfo(navigationName).GetMemberType(), navigationName, navigationConfiguration); + } + + /// + /// 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] + protected virtual ForeignKey HasOneBuilder( + MemberIdentity navigationId, + [NotNull] EntityType relatedEntityType, + [CanBeNull] Action navigationConfiguration = null) + { + ForeignKey foreignKey; + if (navigationId.MemberInfo != null) + { + foreignKey = Builder.HasRelationship( + relatedEntityType, navigationId.MemberInfo, ConfigurationSource.Explicit, + targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata; + } + else + { + foreignKey = Builder.HasRelationship( + relatedEntityType, navigationId.Name, ConfigurationSource.Explicit, + targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata; + } + + if (navigationConfiguration != null + && navigationId.Name != null) + { + var navigation = + Builder.Metadata == relatedEntityType + || foreignKey.PrincipalEntityType == relatedEntityType + ? foreignKey.DependentToPrincipal + : foreignKey.PrincipalToDependent; + + navigationConfiguration( + new NavigationBuilder(navigation)); + } + + return foreignKey; } /// @@ -683,15 +737,21 @@ public virtual ReferenceNavigationBuilder HasOne( /// no property is specified, the relationship will be configured without a navigation property on this /// end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual CollectionNavigationBuilder HasMany( [NotNull] string relatedTypeName, - [CanBeNull] string navigationName) + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) { Check.NotEmpty(relatedTypeName, nameof(relatedTypeName)); Check.NullButNotEmpty(navigationName, nameof(navigationName)); - return HasMany(navigationName, FindRelatedEntityType(relatedTypeName, navigationName)); + return HasMany(navigationName, + FindRelatedEntityType(relatedTypeName, navigationName), + navigationConfiguration); } /// @@ -710,15 +770,19 @@ public virtual CollectionNavigationBuilder HasMany( /// The name of the collection navigation property on this entity type that represents the relationship. /// The navigation must be a CLR property on the entity type. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual CollectionNavigationBuilder HasMany( - [NotNull] string navigationName) + [NotNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) { Check.NotEmpty(navigationName, nameof(navigationName)); if (Metadata.ClrType == null) { - return HasMany(navigationName, (string)null); + return HasMany(navigationName, (string)null, navigationConfiguration); } var memberType = Metadata.GetNavigationMemberInfo(navigationName).GetMemberType(); @@ -734,7 +798,7 @@ public virtual CollectionNavigationBuilder HasMany( "T")); } - return HasMany(elementType, navigationName); + return HasMany(elementType, navigationName, navigationConfiguration); } /// @@ -760,18 +824,27 @@ public virtual CollectionNavigationBuilder HasMany( /// no property is specified, the relationship will be configured without a navigation property on this /// end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual CollectionNavigationBuilder HasMany( [NotNull] Type relatedType, - [CanBeNull] string navigationName = null) + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) { Check.NotNull(relatedType, nameof(relatedType)); - Check.NullButNotEmpty(navigationName, nameof(navigationName));; + Check.NullButNotEmpty(navigationName, nameof(navigationName)); - return HasMany(navigationName, FindRelatedEntityType(relatedType, navigationName)); + return HasMany(navigationName, + FindRelatedEntityType(relatedType, navigationName), + navigationConfiguration); } - private CollectionNavigationBuilder HasMany(string navigationName, EntityType relatedEntityType) + private CollectionNavigationBuilder HasMany( + string navigationName, + EntityType relatedEntityType, + Action navigationConfiguration = null) { var skipNavigation = navigationName != null ? Builder.Metadata.FindSkipNavigation(navigationName) : null; @@ -783,11 +856,19 @@ private CollectionNavigationBuilder HasMany(string navigationName, EntityType re .IsUnique(false, ConfigurationSource.Explicit); } + var foreignKey = relationship?.Metadata; + if (navigationConfiguration != null + && foreignKey?.PrincipalToDependent != null) + { + navigationConfiguration( + new NavigationBuilder(foreignKey.PrincipalToDependent)); + } + return new CollectionNavigationBuilder( Builder.Metadata, relatedEntityType, new MemberIdentity(navigationName), - relationship?.Metadata, + foreignKey, skipNavigation); } diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index b7b279258c2..da0a6781dcc 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -8,7 +8,6 @@ using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -503,9 +502,9 @@ private OwnedNavigationBuilder OwnsManyBuilder /// /// After calling this method, you should chain a call to - /// + /// /// or - /// + /// /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// @@ -516,20 +515,24 @@ private OwnedNavigationBuilder OwnsManyBuilder + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual ReferenceNavigationBuilder HasOne( - [CanBeNull] string navigationName) + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) where TRelatedEntity : class { var relatedEntityType = FindRelatedEntityType(typeof(TRelatedEntity), navigationName); + var foreignKey = HasOneBuilder( + MemberIdentity.Create(navigationName), relatedEntityType, navigationConfiguration); return new ReferenceNavigationBuilder( Builder.Metadata, relatedEntityType, navigationName, - Builder.HasRelationship( - relatedEntityType, navigationName, ConfigurationSource.Explicit, - targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata); + foreignKey); } /// @@ -545,10 +548,10 @@ public virtual ReferenceNavigationBuilder HasOne /// After calling this method, you should chain a call to /// + /// cref="ReferenceNavigationBuilder{TEntity,TRelatedEntity}.WithMany(Expression{Func{TRelatedEntity,IEnumerable{TEntity}}}, Action{NavigationBuilder})" /> /// or /// + /// cref="ReferenceNavigationBuilder{TEntity,TRelatedEntity}.WithOne(Expression{Func{TRelatedEntity,TEntity}}, Action{NavigationBuilder})" /> /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// @@ -559,21 +562,25 @@ public virtual ReferenceNavigationBuilder HasOnepost => post.Blog). If no property is specified, the relationship will be /// configured without a navigation property on this end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual ReferenceNavigationBuilder HasOne( - [CanBeNull] Expression> navigationExpression = null) + [CanBeNull] Expression> navigationExpression = null, + [CanBeNull] Action navigationConfiguration = null) where TRelatedEntity : class { - var navigation = navigationExpression?.GetPropertyAccess(); - var relatedEntityType = FindRelatedEntityType(typeof(TRelatedEntity), navigation?.GetSimpleMemberName()); + var navigationMember = navigationExpression?.GetPropertyAccess(); + var relatedEntityType = FindRelatedEntityType(typeof(TRelatedEntity), navigationMember?.GetSimpleMemberName()); + var foreignKey = HasOneBuilder( + MemberIdentity.Create(navigationMember), relatedEntityType, navigationConfiguration); return new ReferenceNavigationBuilder( Builder.Metadata, relatedEntityType, - navigation, - Builder.HasRelationship( - relatedEntityType, navigation, ConfigurationSource.Explicit, - targetIsPrincipal: Builder.Metadata == relatedEntityType ? true : (bool?)null).Metadata); + navigationMember, + foreignKey); } /// @@ -589,7 +596,7 @@ public virtual ReferenceNavigationBuilder HasOne /// After calling this method, you should chain a call to /// + /// cref="CollectionNavigationBuilder{TEntity,TRelatedEntity}.WithOne(Expression{Func{TRelatedEntity,TEntity}}, Action{NavigationBuilder})" /> /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// @@ -600,9 +607,13 @@ public virtual ReferenceNavigationBuilder HasOne + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual CollectionNavigationBuilder HasMany( - [CanBeNull] string navigationName) + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) where TRelatedEntity : class { Check.NullButNotEmpty(navigationName, nameof(navigationName)); @@ -618,6 +629,14 @@ public virtual CollectionNavigationBuilder HasMany( Builder.Metadata, relatedEntityType, @@ -639,7 +658,7 @@ public virtual CollectionNavigationBuilder HasMany /// After calling this method, you should chain a call to /// + /// cref="CollectionNavigationBuilder{TEntity,TRelatedEntity}.WithOne(Expression{Func{TRelatedEntity,TEntity}}, Action{NavigationBuilder})" /> /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// @@ -650,9 +669,13 @@ public virtual CollectionNavigationBuilder HasManyblog => blog.Posts). If no property is specified, the relationship will be /// configured without a navigation property on this end. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object that can be used to configure the relationship. public virtual CollectionNavigationBuilder HasMany( - [CanBeNull] Expression>> navigationExpression = null) + [CanBeNull] Expression>> navigationExpression = null, + [CanBeNull] Action navigationConfiguration = null) where TRelatedEntity : class { var navigationMember = navigationExpression?.GetPropertyAccess(); @@ -667,11 +690,19 @@ public virtual CollectionNavigationBuilder HasMany( Builder.Metadata, relatedEntityType, new MemberIdentity(navigationMember), - relationship?.Metadata, + foreignKey, skipNavigation); } diff --git a/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs new file mode 100644 index 00000000000..5742e2141c6 --- /dev/null +++ b/src/EFCore/Metadata/Builders/IConventionNavigationBuilder.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + public interface IConventionNavigationBuilder : IConventionAnnotatableBuilder + { + /// + /// The navigation being configured. + /// + new IConventionNavigation Metadata { get; } + + /// + /// Returns a value indicating whether the can be set for this navigation + /// from the current configuration source. + /// + /// The to use for this navigation. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the can be set for this navigation. + bool CanSetPropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation = false); + + /// + /// Sets the to use for this navigation. + /// + /// The to use for this navigation. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + IConventionNavigationBuilder UsePropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation = false); + } +} diff --git a/src/EFCore/Metadata/Builders/NavigationBuilder.cs b/src/EFCore/Metadata/Builders/NavigationBuilder.cs new file mode 100644 index 00000000000..24d1e75f452 --- /dev/null +++ b/src/EFCore/Metadata/Builders/NavigationBuilder.cs @@ -0,0 +1,119 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API for configuring a . + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class NavigationBuilder : IInfrastructure + { + /// + /// 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] + public NavigationBuilder([NotNull] IMutableNavigation navigation) + { + Check.NotNull(navigation, nameof(navigation)); + + Builder = ((Navigation)navigation).Builder; + } + + /// + /// The internal builder being used to configure the property. + /// + IConventionNavigationBuilder IInfrastructure.Instance => Builder; + + /// + /// The navigation being configured. + /// + public virtual IMutableNavigation Metadata => Builder.Metadata; + + /// + /// Adds or updates an annotation on the navigation property. If an annotation + /// with the key specified in already exists + /// its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual NavigationBuilder HasAnnotation([NotNull] string annotation, [NotNull] object value) + { + Check.NotEmpty(annotation, nameof(annotation)); + Check.NotNull(value, nameof(value)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + /// + /// + /// Sets the to use for this property. + /// + /// + /// By default, the backing field, if one is found by convention or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. Calling this method will change that behavior + /// for this property as described in the enum. + /// + /// + /// Calling this method overrides for this property any access mode that was set on the + /// entity type or model. + /// + /// + /// The to use for this property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual NavigationBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + { + Builder.UsePropertyAccessMode(propertyAccessMode, ConfigurationSource.Explicit); + + return this; + } + + private InternalNavigationBuilder Builder { get; } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// true if the specified object is equal to the current object; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs index fee84eae5b2..cdf7eff8b29 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs @@ -508,9 +508,9 @@ private OwnedNavigationBuilder OwnsManyBuil /// /// /// After calling this method, you should chain a call to - /// + /// /// or - /// + /// /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// @@ -550,10 +550,10 @@ public virtual ReferenceNavigationBuilder H /// /// After calling this method, you should chain a call to /// + /// .WithMany(Expression{Func{TRelatedEntity,IEnumerable{TEntity}}}, Action{NavigationBuilder})" /> /// or /// + /// .WithOne(Expression{Func{TRelatedEntity,TEntity}}, Action{NavigationBuilder})" /> /// to fully configure the relationship. Calling just this method without the chained call will not /// produce a valid relationship. /// diff --git a/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder.cs b/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder.cs index 68cd5371a96..9ec91328c30 100644 --- a/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder.cs @@ -71,7 +71,14 @@ public ReferenceNavigationBuilder( Builder = ((ForeignKey)foreignKey).Builder; } - private InternalRelationshipBuilder Builder { [DebuggerStepThrough] get; } + /// + /// 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] + protected virtual InternalRelationshipBuilder Builder { [DebuggerStepThrough] get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -128,12 +135,28 @@ public ReferenceNavigationBuilder( /// The name of the collection navigation property on the other end of this relationship. /// If null or not specified, there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. - public virtual ReferenceCollectionBuilder WithMany([CanBeNull] string collection = null) - => new ReferenceCollectionBuilder( + public virtual ReferenceCollectionBuilder WithMany( + [CanBeNull] string collection = null, + [CanBeNull] Action navigationConfiguration = null) + { + var foreignKey = WithManyBuilder(Check.NullButNotEmpty(collection, nameof(collection))).Metadata; + + if (navigationConfiguration != null + && foreignKey.PrincipalToDependent != null) + { + navigationConfiguration( + new NavigationBuilder(foreignKey.PrincipalToDependent)); + } + + return new ReferenceCollectionBuilder( RelatedEntityType, DeclaringEntityType, - WithManyBuilder(Check.NullButNotEmpty(collection, nameof(collection))).Metadata); + foreignKey); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -201,12 +224,20 @@ private InternalRelationshipBuilder WithManyBuilder(MemberIdentity collection) /// The name of the reference navigation property on the other end of this relationship. /// If null or not specified, there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// + /// An object that can be used to configure the relationship. /// An object to further configure the relationship. - public virtual ReferenceReferenceBuilder WithOne([CanBeNull] string reference = null) + public virtual ReferenceReferenceBuilder WithOne( + [CanBeNull] string reference = null, + [CanBeNull] Action navigationConfiguration = null) => new ReferenceReferenceBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(Check.NullButNotEmpty(reference, nameof(reference))).Metadata); + WithOneBuilder( + Check.NullButNotEmpty(reference, nameof(reference)), + navigationConfiguration).Metadata); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -215,8 +246,10 @@ public virtual ReferenceReferenceBuilder WithOne([CanBeNull] string reference = /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] string navigationName) - => WithOneBuilder(MemberIdentity.Create(navigationName)); + protected virtual InternalRelationshipBuilder WithOneBuilder( + [CanBeNull] string navigationName, + [CanBeNull] Action navigationConfiguration = null) + => WithOneBuilder(MemberIdentity.Create(navigationName), navigationConfiguration); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -225,10 +258,14 @@ protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] string /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual InternalRelationshipBuilder WithOneBuilder([CanBeNull] MemberInfo navigationMemberInfo) - => WithOneBuilder(MemberIdentity.Create(navigationMemberInfo)); + protected virtual InternalRelationshipBuilder WithOneBuilder( + [CanBeNull] MemberInfo navigationMemberInfo, + [CanBeNull] Action navigationConfiguration = null) + => WithOneBuilder(MemberIdentity.Create(navigationMemberInfo), navigationConfiguration); - private InternalRelationshipBuilder WithOneBuilder(MemberIdentity reference) + private InternalRelationshipBuilder WithOneBuilder( + MemberIdentity reference, + Action navigationConfiguration = null) { var referenceName = reference.Name; if (!Builder.Metadata.IsUnique @@ -294,7 +331,23 @@ private InternalRelationshipBuilder WithOneBuilder(MemberIdentity reference) (EntityType)RelatedEntityType, (EntityType)DeclaringEntityType, ConfigurationSource.Explicit); } - return batch.Run(builder); + var withOneBuilder = batch.Run(builder); + + if (navigationConfiguration != null) + { + if (pointsToPrincipal) + { + navigationConfiguration( + new NavigationBuilder(withOneBuilder.Metadata.DependentToPrincipal)); + } + else + { + navigationConfiguration( + new NavigationBuilder(withOneBuilder.Metadata.PrincipalToDependent)); + } + } + + return withOneBuilder; } #region Hidden System.Object members diff --git a/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder`.cs index 3b68018fdc4..50c2286de09 100644 --- a/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/ReferenceNavigationBuilder`.cs @@ -73,12 +73,28 @@ public ReferenceNavigationBuilder( /// The name of the collection navigation property on the other end of this relationship. /// If null or not specified, there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. - public new virtual ReferenceCollectionBuilder WithMany([CanBeNull] string navigationName = null) - => new ReferenceCollectionBuilder( + public new virtual ReferenceCollectionBuilder WithMany( + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) + { + var foreignKey = WithManyBuilder( + Check.NullButNotEmpty(navigationName, nameof(navigationName))).Metadata; + if (navigationConfiguration != null + && foreignKey.PrincipalToDependent != null) + { + navigationConfiguration( + new NavigationBuilder(foreignKey.PrincipalToDependent)); + } + + return new ReferenceCollectionBuilder( RelatedEntityType, DeclaringEntityType, - WithManyBuilder(Check.NullButNotEmpty(navigationName, nameof(navigationName))).Metadata); + foreignKey); + } /// /// @@ -95,13 +111,28 @@ public ReferenceNavigationBuilder( /// relationship (blog => blog.Posts). If no property is specified, the relationship will be /// configured without a navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. public virtual ReferenceCollectionBuilder WithMany( - [CanBeNull] Expression>> navigationExpression) - => new ReferenceCollectionBuilder( + [CanBeNull] Expression>> navigationExpression, + [CanBeNull] Action navigationConfiguration = null) + { + var navigationMember = navigationExpression?.GetPropertyAccess(); + var foreignKey = WithManyBuilder(navigationMember).Metadata; + if (navigationConfiguration != null + && foreignKey.PrincipalToDependent != null) + { + navigationConfiguration( + new NavigationBuilder(foreignKey.PrincipalToDependent)); + } + + return new ReferenceCollectionBuilder( RelatedEntityType, DeclaringEntityType, - WithManyBuilder(navigationExpression?.GetPropertyAccess()).Metadata); + foreignKey); + } /// /// @@ -117,12 +148,19 @@ public virtual ReferenceCollectionBuilder WithMany( /// The name of the reference navigation property on the other end of this relationship. /// If null or not specified, there is no navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. - public new virtual ReferenceReferenceBuilder WithOne([CanBeNull] string navigationName = null) + public new virtual ReferenceReferenceBuilder WithOne( + [CanBeNull] string navigationName = null, + [CanBeNull] Action navigationConfiguration = null) => new ReferenceReferenceBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(Check.NullButNotEmpty(navigationName, nameof(navigationName))).Metadata); + WithOneBuilder( + Check.NullButNotEmpty(navigationName, nameof(navigationName)), + navigationConfiguration).Metadata); /// /// @@ -139,12 +177,17 @@ public virtual ReferenceCollectionBuilder WithMany( /// relationship (blog => blog.BlogInfo). If no property is specified, the relationship will be /// configured without a navigation property on the other end of the relationship. /// + /// + /// An optional action which further configures the navigation property. + /// /// An object to further configure the relationship. public virtual ReferenceReferenceBuilder WithOne( - [CanBeNull] Expression> navigationExpression) + [CanBeNull] Expression> navigationExpression, + [CanBeNull] Action navigationConfiguration = null) => new ReferenceReferenceBuilder( DeclaringEntityType, RelatedEntityType, - WithOneBuilder(navigationExpression?.GetPropertyAccess()).Metadata); + WithOneBuilder(navigationExpression?.GetPropertyAccess(), + navigationConfiguration).Metadata); } } diff --git a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs index 9e332369e7d..90c3afa9e37 100644 --- a/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalNavigationBuilder.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Diagnostics; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Microsoft.EntityFrameworkCore.Metadata.Internal { @@ -11,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// 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 class InternalNavigationBuilder : InternalModelItemBuilder + public class InternalNavigationBuilder : InternalPropertyBaseBuilder, IConventionNavigationBuilder { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -23,5 +25,48 @@ public InternalNavigationBuilder([NotNull] Navigation metadata, [NotNull] Intern : base(metadata, 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 new InternalNavigationBuilder UsePropertyAccessMode( + PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) + => (InternalNavigationBuilder)base.UsePropertyAccessMode(propertyAccessMode, configurationSource); + + /// + /// 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. + /// + IConventionNavigation IConventionNavigationBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + /// 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 IConventionNavigationBuilder.CanSetPropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation) + => CanSetPropertyAccessMode( + propertyAccessMode, 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. + /// + IConventionNavigationBuilder IConventionNavigationBuilder.UsePropertyAccessMode( + PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation) + => UsePropertyAccessMode( + propertyAccessMode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs b/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs new file mode 100644 index 00000000000..9e81152a269 --- /dev/null +++ b/src/EFCore/Metadata/Internal/InternalPropertyBaseBuilder`.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// 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 class InternalPropertyBaseBuilder : InternalModelItemBuilder + where TPropertyBase : PropertyBase + { + /// + /// 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 InternalPropertyBaseBuilder([NotNull] TPropertyBase metadata, [NotNull] InternalModelBuilder modelBuilder) + : base(metadata, 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 InternalPropertyBaseBuilder UsePropertyAccessMode( + PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) + { + if (CanSetPropertyAccessMode(propertyAccessMode, configurationSource)) + { + Metadata.SetPropertyAccessMode(propertyAccessMode, configurationSource); + + return this; + } + + 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 CanSetPropertyAccessMode( + PropertyAccessMode? propertyAccessMode, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetPropertyAccessModeConfigurationSource()) + || ((IPropertyBase)Metadata).GetPropertyAccessMode() == propertyAccessMode; + } +} diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 5acecedbf48..bddd10aedc9 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -22,7 +22,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// 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 class InternalPropertyBuilder : InternalModelItemBuilder, IConventionPropertyBuilder + public class InternalPropertyBuilder : InternalPropertyBaseBuilder, IConventionPropertyBuilder { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -230,29 +230,9 @@ public virtual bool CanSetField([CanBeNull] FieldInfo fieldInfo, ConfigurationSo /// 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 InternalPropertyBuilder UsePropertyAccessMode( + public virtual new InternalPropertyBuilder UsePropertyAccessMode( PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) - { - if (CanSetPropertyAccessMode(propertyAccessMode, configurationSource)) - { - Metadata.SetPropertyAccessMode(propertyAccessMode, configurationSource); - - return this; - } - - 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 CanSetPropertyAccessMode( - PropertyAccessMode? propertyAccessMode, ConfigurationSource? configurationSource) - => configurationSource.Overrides(Metadata.GetPropertyAccessModeConfigurationSource()) - || ((IProperty)Metadata).GetPropertyAccessMode() == propertyAccessMode; + => (InternalPropertyBuilder)base.UsePropertyAccessMode(propertyAccessMode, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs index 421fa1c6667..84b30f10a6e 100644 --- a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs +++ b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs @@ -168,6 +168,33 @@ public virtual void Include_collection_hiding_props(bool tracking) AssertGraph(context.Set().Include(e => e.Posts).AsTracking(tracking).ToList()); } + [ConditionalFact] + public virtual void Can_update_and_query_navigation_from_backing_field() + { + using (var context = CreateContext()) + { + var principal = context.Set().First(); + var dependent1 = new NavDependent { Id = 1, Name = "FirstName", OneToOneFieldNavPrincipal = principal }; + context.Set().Add(dependent1); + context.SaveChanges(); + + var dependentName = + context.Set().Select(p => p.Dependent.Name).First(); + + Assert.Equal("FirstName", dependentName); + + // set the backing field directly + var dependent2 = new NavDependent { Id = 2, Name = "SecondName", OneToOneFieldNavPrincipal = principal }; + principal._dependent = dependent2; + context.SaveChanges(); + + dependentName = + context.Set().Select(p => p.Dependent.Name).First(); + + Assert.Equal("SecondName", dependentName); + } + } + [ConditionalTheory] [InlineData(false)] [InlineData(true)] @@ -1900,6 +1927,29 @@ IBlogAccessor IPostAccessor.AccessBlog } } + protected class OneToOneFieldNavPrincipal + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public string Name { get; set; } + + public NavDependent _dependent; + public NavDependent Dependent + { + get => throw new NotImplementedException("Invalid attempt to access Dependent getter"); + set => throw new NotImplementedException("Invalid attempt to access Dependent setter"); + } + } + + protected class NavDependent + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public string Name { get; set; } + + public OneToOneFieldNavPrincipal OneToOneFieldNavPrincipal { get; set; } + } + protected DbContext CreateContext() => Fixture.CreateContext(); public abstract class FieldMappingFixtureBase : SharedStoreFixtureBase @@ -1938,6 +1988,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().UsePropertyAccessMode(PropertyAccessMode.Field); + modelBuilder.Entity() + .HasOne( + e => e.Dependent, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + e => e.OneToOneFieldNavPrincipal) + .HasForeignKey(); + modelBuilder.Entity(); + if (modelBuilder.Model.GetPropertyAccessMode() != PropertyAccessMode.Property) { modelBuilder.Entity( @@ -2112,6 +2171,8 @@ protected override void Seed(PoolableDbContext context) context.Add( new LoginSession { User = new User2(), Users = new List { new User2() } }); + context.Add(new OneToOneFieldNavPrincipal { Id = 1, Name = "OneToOneFieldNavPrincipal1" }); + context.SaveChanges(); } finally diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 5d580f5acf2..1c68f9a28a1 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -41,6 +41,7 @@ public class ApiConsistencyTest : ApiConsistencyTestBase typeof(IndexBuilder), typeof(IndexBuilder), typeof(KeyBuilder), + typeof(NavigationBuilder), typeof(OwnedNavigationBuilder), typeof(OwnedNavigationBuilder), typeof(OwnedEntityTypeBuilder), diff --git a/test/EFCore.Tests/ModelBuilding/ManyToOneTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToOneTestBase.cs index 6f098610bce..6179d2996f9 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToOneTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToOneTestBase.cs @@ -2060,6 +2060,78 @@ public virtual void One_to_many_relationship_has_no_ambiguity_explicit() modelBuilder.Model.FindEntityType(typeof(Omega)).FindNavigation(nameof(Omega.Kappa)).ForeignKey.Properties.Single() .Name); } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_HasMany_WithOne() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasMany( + e => e.Dependents, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + e => e.OneToManyPrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToManyNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependents").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToManyPrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_named_HasMany_WithOne() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasMany( + "Dependents", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + "OneToManyPrincipal", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToManyNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependents").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToManyPrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Access_mode_can_be_overridden_at_entity_and_navigation_property_levels() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + + var principal = modelBuilder.Entity(); + principal.UsePropertyAccessMode(PropertyAccessMode.PreferProperty); + var dependent = modelBuilder.Entity(); + dependent.UsePropertyAccessMode(PropertyAccessMode.Field); + + modelBuilder.Entity() + .HasMany( + e => e.Dependents, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + e => e.OneToManyPrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, model.GetPropertyAccessMode()); + + Assert.Equal(PropertyAccessMode.PreferProperty, principal.Metadata.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Field, principal.Metadata.FindNavigation("Dependents").GetPropertyAccessMode()); + + Assert.Equal(PropertyAccessMode.Field, dependent.Metadata.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.Metadata.FindNavigation("OneToManyPrincipal").GetPropertyAccessMode()); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs index 5de30a5af9c..5d520dcf4f2 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs @@ -106,14 +106,20 @@ public override TestEntityTypeBuilder OwnsMany( r => buildAction(new GenericStringTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new GenericStringTestReferenceNavigationBuilder( - EntityTypeBuilder.HasOne(navigationExpression?.GetPropertyAccess()?.GetSimpleMemberName())); + EntityTypeBuilder.HasOne( + navigationExpression?.GetPropertyAccess()?.GetSimpleMemberName(), + navigationConfiguration)); public override TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) + Expression>> navigationExpression = null, + Action navigationConfiguration = null) => new GenericStringTestCollectionNavigationBuilder( - EntityTypeBuilder.HasMany(navigationExpression?.GetPropertyAccess()?.GetSimpleMemberName())); + EntityTypeBuilder.HasMany( + navigationExpression?.GetPropertyAccess()?.GetSimpleMemberName(), + navigationConfiguration)); } private class GenericStringTestReferenceNavigationBuilder : @@ -128,14 +134,20 @@ public GenericStringTestReferenceNavigationBuilder( } public override TestReferenceCollectionBuilder WithMany( - Expression>> navigationExpression = null) + Expression>> navigationExpression = null, + Action navigationConfiguration = null) => new GenericStringTestReferenceCollectionBuilder( - ReferenceNavigationBuilder.WithMany(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithMany( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); public override TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new GenericStringTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } private class GenericStringTestCollectionNavigationBuilder @@ -150,9 +162,12 @@ public GenericStringTestCollectionNavigationBuilder( } public override TestReferenceCollectionBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new GenericStringTestReferenceCollectionBuilder( - CollectionNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + CollectionNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } private class GenericStringTestReferenceCollectionBuilder diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs index 1452589e2e2..27096860321 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs @@ -71,8 +71,10 @@ public override TestEntityTypeBuilder OwnsOne( r => buildAction(new GenericTypeTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) - => new GenericTypeTestReferenceNavigationBuilder(EntityTypeBuilder.HasOne(navigationExpression)); + Expression> navigationExpression = null, + Action navigationConfiguration = null) + => new GenericTypeTestReferenceNavigationBuilder( + EntityTypeBuilder.HasOne(navigationExpression, navigationConfiguration)); } private class GenericTypeTestReferenceNavigationBuilder : GenericTestReferenceNavigationBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new GenericTypeTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression)); + ReferenceNavigationBuilder.WithOne(navigationExpression, navigationConfiguration)); } private class GenericTypeTestReferenceReferenceBuilder diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 85fe8054d8e..8f792df2b84 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -242,12 +242,26 @@ public override TestEntityTypeBuilder OwnsMany( r => buildAction(new GenericTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) - => new GenericTestReferenceNavigationBuilder(EntityTypeBuilder.HasOne(navigationExpression)); + string navigationName, + Action navigationConfiguration = null) + => new GenericTestReferenceNavigationBuilder( + EntityTypeBuilder.HasOne(navigationName, navigationConfiguration)); + + public override TestReferenceNavigationBuilder HasOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) + => new GenericTestReferenceNavigationBuilder(EntityTypeBuilder.HasOne(navigationExpression, navigationConfiguration)); + + public override TestCollectionNavigationBuilder HasMany( + string navigationName, + Action navigationConfiguration = null) + => new GenericTestCollectionNavigationBuilder( + EntityTypeBuilder.HasMany(navigationName, navigationConfiguration)); public override TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) - => new GenericTestCollectionNavigationBuilder(EntityTypeBuilder.HasMany(navigationExpression)); + Expression>> navigationExpression = null, + Action navigationConfiguration = null) + => new GenericTestCollectionNavigationBuilder(EntityTypeBuilder.HasMany(navigationExpression, navigationConfiguration)); public override TestEntityTypeBuilder HasQueryFilter(Expression> filter) => Wrap(EntityTypeBuilder.HasQueryFilter(filter)); @@ -417,14 +431,28 @@ public GenericTestReferenceNavigationBuilder(ReferenceNavigationBuilder ReferenceNavigationBuilder { get; } public override TestReferenceCollectionBuilder WithMany( - Expression>> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) + => new GenericTestReferenceCollectionBuilder( + ReferenceNavigationBuilder.WithMany(navigationName, navigationConfiguration)); + + public override TestReferenceCollectionBuilder WithMany( + Expression>> navigationExpression = null, + Action navigationConfiguration = null) => new GenericTestReferenceCollectionBuilder( - ReferenceNavigationBuilder.WithMany(navigationExpression)); + ReferenceNavigationBuilder.WithMany(navigationExpression, navigationConfiguration)); public override TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) => new GenericTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression)); + ReferenceNavigationBuilder.WithOne(navigationName, navigationConfiguration)); + + public override TestReferenceReferenceBuilder WithOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) + => new GenericTestReferenceReferenceBuilder( + ReferenceNavigationBuilder.WithOne(navigationExpression, navigationConfiguration)); } protected class GenericTestCollectionNavigationBuilder : @@ -440,9 +468,16 @@ public GenericTestCollectionNavigationBuilder(CollectionNavigationBuilder CollectionNavigationBuilder { get; } public override TestReferenceCollectionBuilder WithOne( - Expression> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) + => new GenericTestReferenceCollectionBuilder( + CollectionNavigationBuilder.WithOne(navigationName, navigationConfiguration)); + + public override TestReferenceCollectionBuilder WithOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new GenericTestReferenceCollectionBuilder( - CollectionNavigationBuilder.WithOne(navigationExpression)); + CollectionNavigationBuilder.WithOne(navigationExpression, navigationConfiguration)); public override TestCollectionCollectionBuilder WithMany( Expression>> navigationExpression) diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs index 2481762eda5..e803b70fdf8 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs @@ -144,16 +144,22 @@ public override TestEntityTypeBuilder OwnsMany( r => buildAction(new NonGenericTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericStringTestReferenceNavigationBuilder( EntityTypeBuilder.HasOne( - typeof(TRelatedEntity).FullName, navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + typeof(TRelatedEntity).FullName, + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); public override TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) + Expression>> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericTestCollectionNavigationBuilder( EntityTypeBuilder.HasMany( - typeof(TRelatedEntity).FullName, navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + typeof(TRelatedEntity).FullName, + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } private class NonGenericStringTestReferenceNavigationBuilder @@ -167,9 +173,12 @@ public NonGenericStringTestReferenceNavigationBuilder(ReferenceNavigationBuilder } public override TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericStringTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } private class NonGenericStringTestReferenceReferenceBuilder diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index a0e1db88765..711eb92eb9f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -189,14 +189,34 @@ public override TestEntityTypeBuilder OwnsMany( r => buildAction(new NonGenericTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) => new NonGenericTestReferenceNavigationBuilder( - EntityTypeBuilder.HasOne(typeof(TRelatedEntity), navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + EntityTypeBuilder.HasOne(navigationName, navigationConfiguration)); + + public override TestReferenceNavigationBuilder HasOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) + => new NonGenericTestReferenceNavigationBuilder( + EntityTypeBuilder.HasOne( + typeof(TRelatedEntity), + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); public override TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) => new NonGenericTestCollectionNavigationBuilder( - EntityTypeBuilder.HasMany(typeof(TRelatedEntity), navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + EntityTypeBuilder.HasMany(typeof(TRelatedEntity), navigationName, navigationConfiguration)); + + public override TestCollectionNavigationBuilder HasMany( + Expression>> navigationExpression = null, + Action navigationConfiguration = null) + => new NonGenericTestCollectionNavigationBuilder( + EntityTypeBuilder.HasMany( + typeof(TRelatedEntity), + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); public override TestEntityTypeBuilder HasQueryFilter(Expression> filter) => Wrap(EntityTypeBuilder.HasQueryFilter(filter)); @@ -380,14 +400,32 @@ public NonGenericTestReferenceNavigationBuilder(ReferenceNavigationBuilder refer protected ReferenceNavigationBuilder ReferenceNavigationBuilder { get; } public override TestReferenceCollectionBuilder WithMany( - Expression>> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) => new NonGenericTestReferenceCollectionBuilder( - ReferenceNavigationBuilder.WithMany(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithMany(navigationName, navigationConfiguration)); + + public override TestReferenceCollectionBuilder WithMany( + Expression>> navigationExpression = null, + Action navigationConfiguration = null) + => new NonGenericTestReferenceCollectionBuilder( + ReferenceNavigationBuilder.WithMany( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); + + public override TestReferenceReferenceBuilder WithOne( + string navigationName, + Action navigationConfiguration = null) + => new NonGenericTestReferenceReferenceBuilder( + ReferenceNavigationBuilder.WithOne(navigationName, navigationConfiguration)); public override TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } protected class NonGenericTestCollectionNavigationBuilder @@ -403,9 +441,18 @@ public NonGenericTestCollectionNavigationBuilder(CollectionNavigationBuilder col private CollectionNavigationBuilder CollectionNavigationBuilder { get; } public override TestReferenceCollectionBuilder WithOne( - Expression> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) + => new NonGenericTestReferenceCollectionBuilder( + CollectionNavigationBuilder.WithOne(navigationName, navigationConfiguration)); + + public override TestReferenceCollectionBuilder WithOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericTestReferenceCollectionBuilder( - CollectionNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + CollectionNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); public override TestCollectionCollectionBuilder WithMany( Expression>> navigationExpression) diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs index ed94646c33c..6c96e4c2763 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs @@ -91,25 +91,27 @@ public override TestEntityTypeBuilder OwnsMany( r => buildAction(new NonGenericTestOwnedNavigationBuilder(r)))); public override TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) { var navigationName = navigationExpression?.GetPropertyAccess().GetSimpleMemberName(); return new NonGenericStringTestReferenceNavigationBuilder( navigationName == null - ? EntityTypeBuilder.HasOne(typeof(TRelatedEntity).FullName, navigationName) - : EntityTypeBuilder.HasOne(navigationName)); + ? EntityTypeBuilder.HasOne(typeof(TRelatedEntity).FullName, navigationName, navigationConfiguration) + : EntityTypeBuilder.HasOne(navigationName, navigationConfiguration)); } public override TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) + Expression>> navigationExpression = null, + Action navigationConfiguration = null) { var navigationName = navigationExpression?.GetPropertyAccess().GetSimpleMemberName(); return new NonGenericTestCollectionNavigationBuilder( navigationName == null - ? EntityTypeBuilder.HasMany(typeof(TRelatedEntity).FullName, navigationName) - : EntityTypeBuilder.HasMany(navigationName)); + ? EntityTypeBuilder.HasMany(typeof(TRelatedEntity).FullName, navigationName, navigationConfiguration) + : EntityTypeBuilder.HasMany(navigationName, navigationConfiguration)); } } @@ -124,9 +126,12 @@ public NonGenericStringTestReferenceNavigationBuilder(ReferenceNavigationBuilder } public override TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null) + Expression> navigationExpression = null, + Action navigationConfiguration = null) => new NonGenericStringTestReferenceReferenceBuilder( - ReferenceNavigationBuilder.WithOne(navigationExpression?.GetPropertyAccess().GetSimpleMemberName())); + ReferenceNavigationBuilder.WithOne( + navigationExpression?.GetPropertyAccess().GetSimpleMemberName(), + navigationConfiguration)); } private class NonGenericStringTestReferenceReferenceBuilder : NonGenericTestReferenceReferenceBuilder< diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 26675e8ce7b..2fcec65bba1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -221,11 +221,23 @@ public abstract TestEntityTypeBuilder OwnsMany( where TRelatedEntity : class; public abstract TestReferenceNavigationBuilder HasOne( - Expression> navigationExpression = null) + string navigationName, + Action navigationConfiguration = null) + where TRelatedEntity : class; + + public abstract TestReferenceNavigationBuilder HasOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null) + where TRelatedEntity : class; + + public abstract TestCollectionNavigationBuilder HasMany( + string navigationName, + Action navigationConfiguration = null) where TRelatedEntity : class; public abstract TestCollectionNavigationBuilder HasMany( - Expression>> navigationExpression = null) + Expression>> navigationExpression = null, + Action navigationConfiguration = null) where TRelatedEntity : class; public abstract TestEntityTypeBuilder HasQueryFilter(Expression> filter); @@ -343,7 +355,12 @@ public abstract class TestCollectionNavigationBuilder where TRelatedEntity : class { public abstract TestReferenceCollectionBuilder WithOne( - Expression> navigationExpression = null); + string navigationName, + Action navigationConfiguration = null); + + public abstract TestReferenceCollectionBuilder WithOne( + Expression> navigationExpression = null, + Action navigationConfiguration = null); public abstract TestCollectionCollectionBuilder WithMany( Expression>> navigationExpression); @@ -354,10 +371,20 @@ public abstract class TestReferenceNavigationBuilder where TRelatedEntity : class { public abstract TestReferenceCollectionBuilder WithMany( - Expression>> navigationExpression = null); + string navigationName, + Action navigationConfiguration = null); + + public abstract TestReferenceCollectionBuilder WithMany( + Expression>> navigationExpression = null, + Action navigationConfiguration = null); + + public abstract TestReferenceReferenceBuilder WithOne( + string navigationName, + Action navigationConfiguration = null); public abstract TestReferenceReferenceBuilder WithOne( - Expression> navigationExpression = null); + Expression> navigationExpression = null, + Action navigationConfiguration = null); } public abstract class TestReferenceCollectionBuilder diff --git a/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs index dabe493e287..5b913a34021 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs @@ -2691,6 +2691,48 @@ public virtual void Do_not_match_non_unique_FK_when_overlap_with_PK() modelBuilder.FinalizeModel(); } + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_HasOne_WithMany() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + e => e.OneToManyPrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)) + .WithMany( + e => e.Dependents, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToManyNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependents").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToManyPrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_named_HasOne_WithMany() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + "OneToManyPrincipal", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)) + .WithMany( + "Dependents", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToManyNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependents").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToManyPrincipal").GetPropertyAccessMode()); + } + private static void AssertGraph( ModifierGroupHeader parent, ModifierGroupHeader child1, diff --git a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs index 9f2516e7d4e..0c1227430d9 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs @@ -4275,6 +4275,136 @@ private class Node public Node PreviousNode { get; set; } public Node NextNode { get; set; } } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_HasOne_WithOne_on_dependent() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + e => e.OneToOnePrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)) + .WithOne( + e => e.Dependent, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToOneNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependent").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToOnePrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_named_HasOne_WithOne_on_dependent() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + "OneToOnePrincipal", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)) + .WithOne( + "Dependent", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToOneNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependent").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToOnePrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_HasOne_WithOne_on_principal() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + e => e.Dependent, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + e => e.OneToOnePrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToOneNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependent").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToOnePrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_set_access_mode_using_named_HasOne_WithOne_on_principal() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne( + "Dependent", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne( + "OneToOnePrincipal", + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToOneNavPrincipal)); + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependent").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToOnePrincipal").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_override_access_mode_on_principal_using_WithOne_then_HasOne() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne() + .WithOne( + e => e.Dependent, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)); + + modelBuilder.Entity() + .HasOne( + e => e.Dependent, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)) + .WithOne(); + + var principal = (IEntityType)model.FindEntityType(typeof(OneToOneNavPrincipal)); + + Assert.Equal(PropertyAccessMode.Field, principal.FindNavigation("Dependent").GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Navigation_properties_can_override_access_mode_on_dependent_using_WithOne_then_HasOne() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity() + .HasOne() + .WithOne( + e => e.OneToOnePrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Field)); + + modelBuilder.Entity() + .HasOne( + e => e.OneToOnePrincipal, + nb => nb.UsePropertyAccessMode(PropertyAccessMode.Property)) + .WithOne(); + + var dependent = (IEntityType)model.FindEntityType(typeof(NavDependent)); + + Assert.Equal(PropertyAccessMode.Property, dependent.FindNavigation("OneToOnePrincipal").GetPropertyAccessMode()); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index 99d29a6cd28..4aed09dd280 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -849,5 +849,52 @@ public object this[string name] set => _indexerData[name] = value; } } + + private class OneToManyNavPrincipal + { + public int Id { get; set; } + public string Name { get; set; } + + public List Dependents { get; set; } + } + + private class OneToOneNavPrincipal + { + public int Id { get; set; } + public string Name { get; set; } + + public NavDependent Dependent { get; set; } + } + + private class NavDependent + { + public int Id { get; set; } + public string Name { get; set; } + + public OneToManyNavPrincipal OneToManyPrincipal { get; set; } + public OneToOneNavPrincipal OneToOnePrincipal { get; set; } + } + + private class OneToManyNavPrincipalOwner + { + public int Id { get; set; } + public string Description { get; set; } + + public List OwnedDependents { get; set; } + } + + private class OneToOneNavPrincipalOwner + { + public int Id { get; set; } + public string Description { get; set; } + + public OwnedNavDependent OwnedDependent { get; set; } + } + + private class OwnedNavDependent + { + public string FirstName { get; set; } + public string LastName { get; set; } + } } }