From bf24a78c4e5d714814a8867bb6e740769505c6d9 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Fri, 17 Jul 2020 18:34:43 -0700 Subject: [PATCH 1/4] Add public APIs to configure shared type entities (#21552) Resolves #9914 --- .../Extensions/ConventionModelExtensions.cs | 12 ++ .../Extensions/MutableModelExtensions.cs | 10 + .../Builders/CollectionCollectionBuilder.cs | 135 ++++++++++++-- .../Builders/CollectionCollectionBuilder`.cs | 123 +++++++++++- .../Metadata/Builders/EntityTypeBuilder.cs | 8 +- .../Metadata/Builders/EntityTypeBuilder`.cs | 26 ++- .../Builders/IConventionModelBuilder.cs | 26 +++ .../IConventionSharedEntityTypeBuilder.cs | 13 ++ .../Builders/OwnedNavigationBuilder.cs | 8 +- .../Builders/OwnedNavigationBuilder`.cs | 23 +-- .../Builders/SharedEntityTypeBuilder.cs | 54 ++++++ .../Builders/SharedEntityTypeBuilder`.cs | 29 +++ ...nyToManyAssociationEntityTypeConvention.cs | 9 +- .../Internal/InternalEntityTypeBuilder.cs | 26 ++- .../Metadata/Internal/InternalModelBuilder.cs | 104 +++++++++-- .../InternalSharedEntityTypeBuilder.cs | 17 ++ src/EFCore/Metadata/Internal/Model.cs | 41 +++- src/EFCore/ModelBuilder.cs | 176 +++++++++++++++++- src/EFCore/Properties/CoreStrings.Designer.cs | 16 +- src/EFCore/Properties/CoreStrings.resx | 61 +++--- .../Internal/InternalModelBuilderTest.cs | 28 +++ .../ModelBuilding/ManyToManyTestBase.cs | 102 ++++++++++ ...delBuilderGenericRelationshipStringTest.cs | 14 ++ ...ModelBuilderGenericRelationshipTypeTest.cs | 14 ++ .../ModelBuilding/ModelBuilderGenericTest.cs | 57 ++++++ .../ModelBuilderNonGenericStringTest.cs | 15 ++ .../ModelBuilderNonGenericTest.cs | 60 ++++++ ...lBuilderNonGenericUnqualifiedStringTest.cs | 15 ++ .../ModelBuilding/ModelBuilderTestBase.cs | 30 +++ .../ModelBuilding/NonRelationshipTestBase.cs | 38 ++++ test/EFCore.Tests/ModelBuilding/TestModel.cs | 72 +++++-- 31 files changed, 1222 insertions(+), 140 deletions(-) create mode 100644 src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs create mode 100644 src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs diff --git a/src/EFCore/Extensions/ConventionModelExtensions.cs b/src/EFCore/Extensions/ConventionModelExtensions.cs index 05686a1f247..5ca00de8c6c 100644 --- a/src/EFCore/Extensions/ConventionModelExtensions.cs +++ b/src/EFCore/Extensions/ConventionModelExtensions.cs @@ -300,6 +300,18 @@ public static string AddIgnored([NotNull] this IConventionModel model, [NotNull] Check.NotNull(clrType, nameof(clrType)), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + /// Marks the given entity type as shared, indicating that when discovered matching entity types + /// should be configured as shared type entity type. + /// + /// The model to add the shared type to. + /// The type of the entity type that should be shared. + /// Indicates whether the configuration was specified using a data annotation. + public static Type AddShared([NotNull] this IConventionModel model, [NotNull] Type clrType, bool fromDataAnnotation = false) + => Check.NotNull((Model)model, nameof(model)).AddShared( + Check.NotNull(clrType, nameof(clrType)), + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// Forces post-processing on the model such that it is ready for use by the runtime. This post /// processing happens automatically when using ; this method allows it to be run diff --git a/src/EFCore/Extensions/MutableModelExtensions.cs b/src/EFCore/Extensions/MutableModelExtensions.cs index 2f77272b42b..a8a831cb11b 100644 --- a/src/EFCore/Extensions/MutableModelExtensions.cs +++ b/src/EFCore/Extensions/MutableModelExtensions.cs @@ -243,6 +243,16 @@ public static string RemoveOwned([NotNull] this IMutableModel model, [NotNull] T => Check.NotNull((Model)model, nameof(model)).RemoveOwned( Check.NotNull(clrType, nameof(clrType))); + /// + /// Marks the given entity type as shared, indicating that when discovered matching entity types + /// should be configured as shared type entity type. + /// + /// The model to add the shared type to. + /// The type of the entity type that should be shared. + public static Type AddShared([NotNull] this IMutableModel model, [NotNull] Type clrType) + => Check.NotNull((Model)model, nameof(model)).AddShared( + Check.NotNull(clrType, nameof(clrType)), ConfigurationSource.Explicit); + /// /// Forces post-processing on the model such that it is ready for use by the runtime. This post /// processing happens automatically when using ; this method allows it to be run diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs index e8a11ceb312..bb23ed53441 100644 --- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs +++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs @@ -86,34 +86,108 @@ public CollectionCollectionBuilder( /// /// Configures the relationships to the entity types participating in the many-to-many relationship. /// - /// The type of the join entity. + /// The CLR type of the join entity. /// The configuration for the relationship to the right entity type. /// The configuration for the relationship to the left entity type. /// The builder for the association type. public virtual EntityTypeBuilder UsingEntity( - [NotNull] Type joinEntity, + [NotNull] Type joinEntityType, [NotNull] Func configureRight, [NotNull] Func configureLeft) { - if (((Model)LeftEntityType.Model).IsShared(joinEntity)) + Check.NotNull(joinEntityType, nameof(joinEntityType)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + + var existingAssociationEntityType = (EntityType) + (LeftNavigation.ForeignKey?.DeclaringEntityType + ?? RightNavigation.ForeignKey?.DeclaringEntityType); + EntityType associationEntityType = null; + if (existingAssociationEntityType != null) { - //TODO #9914 - when the generic version of "please use the shared-type entity type version of this API" - // is available then update to use that. - throw new InvalidOperationException( - CoreStrings.DoNotUseUsingEntityOnSharedClrType(joinEntity.GetType().Name)); + if (existingAssociationEntityType.ClrType == joinEntityType + && !existingAssociationEntityType.HasSharedClrType) + { + associationEntityType = existingAssociationEntityType; + } + else + { + ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( + existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + } } + if (associationEntityType == null) + { + associationEntityType = ModelBuilder.Entity(joinEntityType, ConfigurationSource.Explicit).Metadata; + } + + var entityTypeBuilder = new EntityTypeBuilder(associationEntityType); + + var leftForeignKey = configureLeft(entityTypeBuilder).Metadata; + var rightForeignKey = configureRight(entityTypeBuilder).Metadata; + + Using(rightForeignKey, leftForeignKey); + + return entityTypeBuilder; + } + + /// + /// Configures the relationships to the entity types participating in the many-to-many relationship. + /// + /// The name of the join entity. + /// The CLR type of the join entity. + /// The configuration for the relationship to the right entity type. + /// The configuration for the relationship to the left entity type. + /// The builder for the association type. + public virtual EntityTypeBuilder UsingEntity( + [NotNull] string joinEntityName, + [NotNull] Type joinEntityType, + [NotNull] Func configureRight, + [NotNull] Func configureLeft) + { + Check.NotEmpty(joinEntityName, nameof(joinEntityName)); + Check.NotNull(joinEntityType, nameof(joinEntityType)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + var existingAssociationEntityType = (EntityType) (LeftNavigation.ForeignKey?.DeclaringEntityType ?? RightNavigation.ForeignKey?.DeclaringEntityType); + EntityType associationEntityType = null; if (existingAssociationEntityType != null) { - ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( - existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + if (existingAssociationEntityType.ClrType == joinEntityType + && string.Equals(existingAssociationEntityType.Name, joinEntityName, StringComparison.Ordinal)) + { + associationEntityType = existingAssociationEntityType; + } + else + { + ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( + existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + } } - var entityTypeBuilder = new EntityTypeBuilder( - ModelBuilder.Entity(joinEntity, ConfigurationSource.Explicit).Metadata); + if (associationEntityType == null) + { + var existingEntityType = ModelBuilder.Metadata.FindEntityType(joinEntityName); + if (existingEntityType?.ClrType == joinEntityType) + { + associationEntityType = existingEntityType; + } + else + { + if (!ModelBuilder.Metadata.IsShared(joinEntityType)) + { + throw new InvalidOperationException(CoreStrings.TypeNotMarkedAsShared(joinEntityType.DisplayName())); + } + + associationEntityType = ModelBuilder.SharedEntity(joinEntityName, joinEntityType, ConfigurationSource.Explicit).Metadata; + } + } + + var entityTypeBuilder = new EntityTypeBuilder(associationEntityType); var leftForeignKey = configureLeft(entityTypeBuilder).Metadata; var rightForeignKey = configureRight(entityTypeBuilder).Metadata; @@ -126,18 +200,51 @@ public virtual EntityTypeBuilder UsingEntity( /// /// Configures the relationships to the entity types participating in the many-to-many relationship. /// - /// The type of the join entity. + /// The CLR type of the join entity. /// The configuration for the relationship to the right entity type. /// The configuration for the relationship to the left entity type. /// The configuration of the association type. /// The builder for the originating entity type so that multiple configuration calls can be chained. public virtual EntityTypeBuilder UsingEntity( - [NotNull] Type joinEntity, + [NotNull] Type joinEntityType, [NotNull] Func configureRight, [NotNull] Func configureLeft, [NotNull] Action configureAssociation) { - var entityTypeBuilder = UsingEntity(joinEntity, configureRight, configureLeft); + Check.NotNull(joinEntityType, nameof(joinEntityType)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + Check.NotNull(configureAssociation, nameof(configureAssociation)); + + var entityTypeBuilder = UsingEntity(joinEntityType, configureRight, configureLeft); + configureAssociation(entityTypeBuilder); + + return new EntityTypeBuilder(RightEntityType); + } + + /// + /// Configures the relationships to the entity types participating in the many-to-many relationship. + /// + /// The name of the join entity. + /// The CLR type of the join entity. + /// The configuration for the relationship to the right entity type. + /// The configuration for the relationship to the left entity type. + /// The configuration of the association type. + /// The builder for the originating entity type so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder UsingEntity( + [NotNull] string joinEntityName, + [NotNull] Type joinEntityType, + [NotNull] Func configureRight, + [NotNull] Func configureLeft, + [NotNull] Action configureAssociation) + { + Check.NotEmpty(joinEntityName, nameof(joinEntityName)); + Check.NotNull(joinEntityType, nameof(joinEntityType)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + Check.NotNull(configureAssociation, nameof(configureAssociation)); + + var entityTypeBuilder = UsingEntity(joinEntityName, joinEntityType, configureRight, configureLeft); configureAssociation(entityTypeBuilder); return new EntityTypeBuilder(RightEntityType); diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs index b785a0efe2a..ed657f0c53b 100644 --- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs +++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Builders { @@ -52,25 +53,97 @@ public virtual EntityTypeBuilder UsingEntity, ReferenceCollectionBuilder> configureLeft) where TAssociationEntity : class { - if (((Model)LeftEntityType.Model).IsShared(typeof(TAssociationEntity))) + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + + var existingAssociationEntityType = (EntityType) + (LeftNavigation.ForeignKey?.DeclaringEntityType + ?? RightNavigation.ForeignKey?.DeclaringEntityType); + EntityType associationEntityType = null; + if (existingAssociationEntityType != null) + { + if (existingAssociationEntityType.ClrType == typeof(TAssociationEntity) + && !existingAssociationEntityType.HasSharedClrType) + { + associationEntityType = existingAssociationEntityType; + } + else + { + ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( + existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + } + } + + if (associationEntityType == null) { - //TODO #9914 - when the generic version of "please use the shared-type entity type version of this API" - // is available then update to use that. - throw new InvalidOperationException( - CoreStrings.DoNotUseUsingEntityOnSharedClrType(typeof(TAssociationEntity).Name)); + associationEntityType = ModelBuilder.Entity(typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata; } + var entityTypeBuilder = new EntityTypeBuilder(associationEntityType); + + var leftForeignKey = configureLeft(entityTypeBuilder).Metadata; + var rightForeignKey = configureRight(entityTypeBuilder).Metadata; + + Using(rightForeignKey, leftForeignKey); + + return entityTypeBuilder; + } + + /// + /// Configures the relationships to the entity types participating in the many-to-many relationship. + /// + /// The name of the association entity. + /// The configuration for the relationship to the right entity type. + /// The configuration for the relationship to the left entity type. + /// The type of the association entity. + /// The builder for the association type. + public virtual EntityTypeBuilder UsingEntity( + [NotNull] string joinEntityName, + [NotNull] Func, ReferenceCollectionBuilder> configureRight, + [NotNull] Func, ReferenceCollectionBuilder> configureLeft) + where TAssociationEntity : class + { + Check.NotEmpty(joinEntityName, nameof(joinEntityName)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + var existingAssociationEntityType = (EntityType) (LeftNavigation.ForeignKey?.DeclaringEntityType ?? RightNavigation.ForeignKey?.DeclaringEntityType); + EntityType associationEntityType = null; if (existingAssociationEntityType != null) { - ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( - existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + if (existingAssociationEntityType.ClrType == typeof(TAssociationEntity) + && string.Equals(existingAssociationEntityType.Name, joinEntityName, StringComparison.Ordinal)) + { + associationEntityType = existingAssociationEntityType; + } + else + { + ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly( + existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit); + } } - var entityTypeBuilder = new EntityTypeBuilder( - ModelBuilder.Entity(typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata); + if (associationEntityType == null) + { + var existingEntityType = ModelBuilder.Metadata.FindEntityType(joinEntityName); + if (existingEntityType?.ClrType == typeof(TAssociationEntity)) + { + associationEntityType = existingEntityType; + } + else + { + if (!ModelBuilder.Metadata.IsShared(typeof(TAssociationEntity))) + { + throw new InvalidOperationException(CoreStrings.TypeNotMarkedAsShared(typeof(TAssociationEntity).DisplayName())); + } + + associationEntityType = ModelBuilder.SharedEntity(joinEntityName, typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata; + } + } + + var entityTypeBuilder = new EntityTypeBuilder(associationEntityType); var leftForeignKey = configureLeft(entityTypeBuilder).Metadata; var rightForeignKey = configureRight(entityTypeBuilder).Metadata; @@ -94,7 +167,37 @@ public virtual EntityTypeBuilder UsingEntity( [NotNull] Action> configureAssociation) where TAssociationEntity : class { - var entityTypeBuilder = UsingEntity(configureRight, configureLeft); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + + var entityTypeBuilder = UsingEntity(configureRight, configureLeft); + configureAssociation(entityTypeBuilder); + + return new EntityTypeBuilder(RightEntityType); + } + + /// + /// Configures the relationships to the entity types participating in the many-to-many relationship. + /// + /// The name of the association entity. + /// The configuration for the relationship to the right entity type. + /// The configuration for the relationship to the left entity type. + /// The configuration of the association type. + /// The type of the association entity. + /// The builder for the originating entity type so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder UsingEntity( + [NotNull] string joinEntityName, + [NotNull] Func, ReferenceCollectionBuilder> configureRight, + [NotNull] Func, ReferenceCollectionBuilder> configureLeft, + [NotNull] Action> configureAssociation) + where TAssociationEntity : class + { + Check.NotEmpty(joinEntityName, nameof(joinEntityName)); + Check.NotNull(configureRight, nameof(configureRight)); + Check.NotNull(configureLeft, nameof(configureLeft)); + Check.NotNull(configureAssociation, nameof(configureAssociation)); + + var entityTypeBuilder = UsingEntity(joinEntityName, configureRight, configureLeft); configureAssociation(entityTypeBuilder); return new EntityTypeBuilder(RightEntityType); diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index 7ccb3d02088..8e3ac1a2cff 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -376,7 +376,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } @@ -412,7 +412,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); return this; } @@ -523,7 +523,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } @@ -559,7 +559,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); return this; } diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index d5a00ae7610..e3339372c96 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -346,8 +346,7 @@ public virtual OwnedNavigationBuilder OwnsOne OwnsOne( [NotNull] Expression> navigationExpression) where TRelatedEntity : class - => OwnsOneBuilder( - new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + => OwnsOneBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -380,7 +379,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotNull(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationName))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationName))); return this; } @@ -416,7 +415,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotNull(navigationExpression, nameof(navigationExpression)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -426,9 +425,8 @@ private OwnedNavigationBuilder OwnsOneBuilder OwnsMany OwnsMany( [NotNull] Expression>> navigationExpression) where TRelatedEntity : class - => OwnsManyBuilder( - new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + => OwnsManyBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -523,10 +520,10 @@ public virtual EntityTypeBuilder OwnsMany( [NotNull] Action> buildAction) where TRelatedEntity : class { - Check.NotNull(navigationName, nameof(navigationName)); + Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationName))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationName))); return this; } @@ -562,7 +559,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotNull(navigationExpression, nameof(navigationExpression)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -572,9 +569,8 @@ private OwnedNavigationBuilder OwnsManyBuilder IConventionEntityTypeBuilder Entity([NotNull] string name, bool? shouldBeOwned = false, bool fromDataAnnotation = false); + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// The name of the entity type to be configured. + /// The type of the entity type to be configured. + /// + /// if the entity type should be owned, + /// if the entity type should not be owned + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the entity type if the entity type was added or already part of the model, + /// otherwise. + /// + IConventionEntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type type, bool? shouldBeOwned = false, bool fromDataAnnotation = false); + /// /// Returns an object that can be used to configure a given entity type in the model. /// If an entity type with the provided type is not already part of the model, diff --git a/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..7674d56ac2a --- /dev/null +++ b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs @@ -0,0 +1,13 @@ +// 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 +{ + /// + /// This interface is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + public interface IConventionSharedEntityTypeBuilder + { + } +} diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs index b35f18ae957..d56be9f980a 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs @@ -397,7 +397,7 @@ public virtual OwnedNavigationBuilder OwnsOne( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } } @@ -437,7 +437,7 @@ public virtual OwnedNavigationBuilder OwnsOne( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName)); return this; } } @@ -551,7 +551,7 @@ public virtual OwnedNavigationBuilder OwnsMany( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } } @@ -590,7 +590,7 @@ public virtual OwnedNavigationBuilder OwnsMany( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName)); return this; } } diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs index 33b4177954a..22d4f130972 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs @@ -284,8 +284,7 @@ public virtual OwnedNavigationBuilder Own [NotNull] Expression> navigationExpression) where TNewDependentEntity : class => OwnsOneBuilder( - new MemberIdentity( - Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -319,7 +318,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationName))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationName))); return this; } @@ -356,7 +355,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -367,10 +366,8 @@ private OwnedNavigationBuilder OwnsOneBui InternalForeignKeyBuilder relationship; using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - relationship = navigation.MemberInfo == null - ? DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation.Name, ConfigurationSource.Explicit) - : DependentEntityType.Builder.HasOwnership( - typeof(TNewDependentEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit); + relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation, ConfigurationSource.Explicit); + relationship.IsUnique(true, ConfigurationSource.Explicit); relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder; } @@ -470,7 +467,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationName))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationName))); return this; } } @@ -509,7 +506,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } } @@ -520,10 +517,8 @@ private OwnedNavigationBuilder OwnsManyBuil InternalForeignKeyBuilder relationship; using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - relationship = navigation.MemberInfo == null - ? DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation.Name, ConfigurationSource.Explicit) - : DependentEntityType.Builder.HasOwnership( - typeof(TNewRelatedEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit); + relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation, ConfigurationSource.Explicit); + relationship.IsUnique(false, ConfigurationSource.Explicit); relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder; } diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..13fe5a83bbf --- /dev/null +++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs @@ -0,0 +1,54 @@ +// 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 Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// 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 SharedEntityTypeBuilder + { + /// + /// 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 SharedEntityTypeBuilder() + { + } + + #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. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + 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)] + public override int GetHashCode() => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs new file mode 100644 index 00000000000..8abb3b920a5 --- /dev/null +++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// 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. + /// + /// + /// The entity type being configured. + // ReSharper disable once UnusedTypeParameter + public class SharedEntityTypeBuilder : SharedEntityTypeBuilder + { + /// + /// 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 SharedEntityTypeBuilder() + { + } + } +} diff --git a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs index 02026872d57..6e766f8dd67 100644 --- a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs +++ b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs @@ -106,12 +106,9 @@ private void CreateAssociationEntityType( inverseEntityType.ShortName()), otherIdentifiers, int.MaxValue); - //TODO #9914 - when the shared-type entity type version of model.Entity() is available call that instead - var associationEntityTypeBuilder = - model.AddEntityType( - associationEntityTypeName, - Model.DefaultPropertyBagType, - ConfigurationSource.Convention).Builder; + + var associationEntityTypeBuilder = model.Builder.SharedEntity( + associationEntityTypeName, Model.DefaultPropertyBagType, ConfigurationSource.Convention); // Create left and right foreign keys from the outer entity types to // the association entity type and configure the skip navigations. diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 1882acc1b65..b3b3eb7493c 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -2952,8 +2952,7 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] string navigationName, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName), - inverse: null, configurationSource); + new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName), inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2966,8 +2965,7 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] string navigationName, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName), - inverse: null, configurationSource); + new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName), inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2980,8 +2978,20 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] MemberInfo navigationMember, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember), - inverse: null, configurationSource); + new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember), inverse: null, 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. + /// + public virtual InternalForeignKeyBuilder HasOwnership( + [NotNull] Type targetEntityType, + MemberIdentity navigation, + ConfigurationSource configurationSource) + => HasOwnership( + new TypeIdentity(targetEntityType, Metadata.Model), navigation, inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3033,6 +3043,7 @@ private InternalForeignKeyBuilder HasOwnership( if (existingNavigation.TargetEntityType.Name == targetEntityType.Name) { var existingOwnedEntityType = existingNavigation.ForeignKey.DeclaringEntityType; + // Upgrade configurationSource for existing entity type if (existingOwnedEntityType.HasDefiningNavigation()) { if (targetEntityType.Type != null) @@ -4244,7 +4255,8 @@ private bool CanAddDiscriminatorProperty( /// IConventionEntityType IConventionEntityTypeBuilder.Metadata { - [DebuggerStepThrough] get => Metadata; + [DebuggerStepThrough] + get => Metadata; } /// diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs index 95fbf275a8c..54706f1c342 100644 --- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs @@ -48,7 +48,17 @@ public InternalModelBuilder([NotNull] Model metadata) /// public virtual InternalEntityTypeBuilder Entity( [NotNull] string name, ConfigurationSource configurationSource, bool? shouldBeOwned = false) - => Entity(new TypeIdentity(name), configurationSource, shouldBeOwned); + => Entity(new TypeIdentity(name), null, configurationSource, shouldBeOwned); + + /// + /// 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 InternalEntityTypeBuilder SharedEntity( + [NotNull] string name, [NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false) + => Entity(new TypeIdentity(name), Check.NotNull(type, nameof(type)), configurationSource, shouldBeOwned); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -58,10 +68,10 @@ public virtual InternalEntityTypeBuilder Entity( /// public virtual InternalEntityTypeBuilder Entity( [NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false) - => Entity(new TypeIdentity(type, Metadata), configurationSource, shouldBeOwned); + => Entity(new TypeIdentity(type, Metadata), null, configurationSource, shouldBeOwned); private InternalEntityTypeBuilder Entity( - in TypeIdentity type, ConfigurationSource configurationSource, bool? shouldBeOwned) + in TypeIdentity type, Type sharedTypeClrType, ConfigurationSource configurationSource, bool? shouldBeOwned) { if (IsIgnored(type, configurationSource)) { @@ -69,14 +79,35 @@ private InternalEntityTypeBuilder Entity( } var clrType = type.Type; - var entityType = clrType == null - ? Metadata.FindEntityType(type.Name) - : Metadata.FindEntityType(clrType); + EntityType entityType; + if (clrType != null) + { + if (Metadata.IsShared(clrType)) + { + return configurationSource == ConfigurationSource.Explicit + ? throw new InvalidOperationException(CoreStrings.ClashingSharedType(clrType.DisplayName())) + : (InternalEntityTypeBuilder)null; + } + + entityType = Metadata.FindEntityType(clrType); + } + else + { + if (sharedTypeClrType != null && Metadata.FindEntityType(Metadata.GetDisplayName(sharedTypeClrType)) != null) + { + return configurationSource == ConfigurationSource.Explicit + ? throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(type.Name)) + : (InternalEntityTypeBuilder)null; + } + + entityType = Metadata.FindEntityType(type.Name); + } if (shouldBeOwned == false - && (ShouldBeOwnedType(type) - || entityType != null && entityType.IsOwned())) + && (ShouldBeOwnedType(type) // Marked in model as owned + || entityType != null && entityType.IsOwned())) // Created using Owns* API { + // We always throw as configuring a type as owned is always comes from user (through Explicit/DataAnnotation) throw new InvalidOperationException( CoreStrings.ClashingOwnedEntityType( clrType == null ? type.Name : clrType.ShortDisplayName())); @@ -106,14 +137,24 @@ private InternalEntityTypeBuilder Entity( if (entityType != null) { + if (sharedTypeClrType != null) + { + if (entityType.ClrType != sharedTypeClrType) + { + throw new InvalidOperationException(CoreStrings.ClashingMismatchedSharedType(type.Name)); + } + } + entityType.UpdateConfigurationSource(configurationSource); return entityType.Builder; } Metadata.RemoveIgnored(type.Name); - entityType = clrType == null - ? Metadata.AddEntityType(type.Name, configurationSource) - : Metadata.AddEntityType(clrType, configurationSource); + entityType = clrType != null + ? Metadata.AddEntityType(clrType, configurationSource) + : sharedTypeClrType != null + ? Metadata.AddEntityType(type.Name, sharedTypeClrType, configurationSource) + : Metadata.AddEntityType(type.Name, configurationSource); return entityType?.Builder; } @@ -307,6 +348,34 @@ public virtual IConventionOwnedEntityTypeBuilder Owned( private bool ShouldBeOwnedType(in TypeIdentity type) => type.Type != null && Metadata.IsOwned(type.Type); + /// + /// 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 IConventionSharedEntityTypeBuilder SharedEntity( + [NotNull] Type type, ConfigurationSource configurationSource) + { + if (IsIgnored(type, configurationSource)) + { + return null; + } + + Metadata.RemoveIgnored(type); + + foreach (var entityType in Metadata.GetEntityTypes() + .Where(et => !et.HasSharedClrType && et.ClrType == type && configurationSource.Overrides(et.GetConfigurationSource())) + .ToList()) + { + HasNoEntityType(entityType, configurationSource); + } + + Metadata.AddShared(type); + + return new InternalSharedEntityTypeBuilder(); + } + /// /// 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 @@ -560,7 +629,8 @@ public virtual bool CanSetPropertyAccessMode( /// IConventionModel IConventionModelBuilder.Metadata { - [DebuggerStepThrough] get => Metadata; + [DebuggerStepThrough] + get => Metadata; } /// @@ -573,6 +643,16 @@ IConventionModel IConventionModelBuilder.Metadata IConventionEntityTypeBuilder IConventionModelBuilder.Entity(string name, bool? shouldBeOwned, bool fromDataAnnotation) => Entity(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + IConventionEntityTypeBuilder IConventionModelBuilder.SharedEntity(string name, Type type, bool? shouldBeOwned, bool fromDataAnnotation) + => SharedEntity(name, type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..849619e4f46 --- /dev/null +++ b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.EntityFrameworkCore.Metadata.Builders; + +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 InternalSharedEntityTypeBuilder : IConventionSharedEntityTypeBuilder + { + } +} diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 922e797674d..beec03812b4 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -58,7 +58,7 @@ private readonly SortedDictionary> _entityTypesWit private readonly Dictionary _ignoredTypeNames = new Dictionary(StringComparer.Ordinal); - private readonly HashSet _sharedEntityClrTypes = new HashSet(); + private readonly Dictionary _sharedEntityClrTypes = new Dictionary(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -235,9 +235,17 @@ private EntityType AddEntityType(EntityType entityType) throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(entityType.DisplayName())); } - _sharedEntityClrTypes.Add(entityType.ClrType); + if (_sharedEntityClrTypes.TryGetValue(entityType.ClrType, out var existingConfigurationSource)) + { + _sharedEntityClrTypes[entityType.ClrType] = entityType.GetConfigurationSource().Max(existingConfigurationSource); + } + else + { + _sharedEntityClrTypes.Add(entityType.ClrType, entityType.GetConfigurationSource()); + } } - else if (_sharedEntityClrTypes.Contains(entityType.ClrType)) + else if (entityType.ClrType != null + && _sharedEntityClrTypes.ContainsKey(entityType.ClrType)) { throw new InvalidOperationException(CoreStrings.ClashingSharedType(entityType.DisplayName())); } @@ -772,7 +780,7 @@ public virtual bool IsIgnored([NotNull] Type type) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool IsShared([NotNull] Type type) - => _sharedEntityClrTypes.Contains(type); + => _sharedEntityClrTypes.ContainsKey(type); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -877,6 +885,31 @@ public virtual string RemoveOwned([NotNull] Type clrType) return ownedTypes.Remove(name) ? name : 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 Type AddShared([NotNull] Type clrType, ConfigurationSource configurationSource) + { + if (_entityTypes.Any(et => !et.Value.HasSharedClrType && et.Value.ClrType == clrType)) + { + throw new InvalidOperationException(CoreStrings.CannotMarkShared(clrType.ShortDisplayName())); + } + + if (_sharedEntityClrTypes.TryGetValue(clrType, out var existingConfigurationSource)) + { + _sharedEntityClrTypes[clrType] = configurationSource.Max(existingConfigurationSource); + } + else + { + _sharedEntityClrTypes.Add(clrType, configurationSource); + } + + return clrType; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/ModelBuilder.cs b/src/EFCore/ModelBuilder.cs index 111e7e0be7a..4725a3c4246 100644 --- a/src/EFCore/ModelBuilder.cs +++ b/src/EFCore/ModelBuilder.cs @@ -115,6 +115,30 @@ public virtual EntityTypeBuilder Entity() where TEntity : class => new EntityTypeBuilder(Builder.Entity(typeof(TEntity), ConfigurationSource.Explicit).Metadata); + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// The CLR type of the entity type to be configured. + /// The name of the entity type to be configured. + /// An object that can be used to configure the entity type. + public virtual EntityTypeBuilder SharedEntity([NotNull] string name) + where TEntity : class + { + Check.NotEmpty(name, nameof(name)); + + return new EntityTypeBuilder(Builder.SharedEntity(name, typeof(TEntity), ConfigurationSource.Explicit).Metadata); + } + /// /// Returns an object that can be used to configure a given entity type in the model. /// If the entity type is not already part of the model, it will be added to the model. @@ -142,6 +166,30 @@ public virtual EntityTypeBuilder Entity([NotNull] string name) return new EntityTypeBuilder(Builder.Entity(name, ConfigurationSource.Explicit).Metadata); } + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// The name of the entity type to be configured. + /// The CLR type of the entity type to be configured. + /// An object that can be used to configure the entity type. + public virtual EntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + + return new EntityTypeBuilder(Builder.SharedEntity(name, clrType, ConfigurationSource.Explicit).Metadata); + } + /// /// /// Performs configuration of a given entity type in the model. If the entity type is not already part @@ -168,6 +216,41 @@ public virtual ModelBuilder Entity([NotNull] Action + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// This overload allows configuration of the entity type to be done inline in the method call rather + /// than being chained after a call to . This allows additional + /// configuration at the model level to be chained after configuration for the entity type. + /// + /// + /// The CLR type of the entity type to be configured. + /// The name of the entity type to be configured. + /// An action that performs configuration of the entity type. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Action> buildAction) + where TEntity : class + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(buildAction, nameof(buildAction)); + + buildAction(SharedEntity(name)); + + return this; + } + /// /// /// Performs configuration of a given entity type in the model. If the entity type is not already part @@ -221,11 +304,46 @@ public virtual ModelBuilder Entity([NotNull] string name, [NotNull] Action + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// This overload allows configuration of the entity type to be done in line in the method call rather + /// than being chained after a call to . This allows additional + /// configuration at the model level to be chained after configuration for the entity type. + /// + /// + /// The name of the entity type to be configured. + /// The CLR type of the entity type to be configured. + /// An action that performs configuration of the entity type. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType, [NotNull] Action buildAction) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + Check.NotNull(buildAction, nameof(buildAction)); + + buildAction(SharedEntity(name, clrType)); + + return this; + } + /// /// Excludes the given entity type from the model. This method is typically used to remove types from /// the model that were added by convention. /// - /// The entity type to be removed from the model. + /// The entity type to be removed from the model. /// /// The same instance so that additional configuration calls can be chained. /// @@ -234,7 +352,7 @@ public virtual ModelBuilder Ignore() => Ignore(typeof(TEntity)); /// - /// Excludes the given entity type from the model. This method is typically used to remove types from + /// Excludes an entity type with given CLR type from the model. This method is typically used to remove types from /// the model that were added by convention. /// /// The entity type to be removed from the model. @@ -250,6 +368,23 @@ public virtual ModelBuilder Ignore([NotNull] Type type) return this; } + /// + /// Excludes an entity type with the given name from the model. This method is typically used to remove types from + /// the model that were added by convention. + /// + /// The name of the entity type to be removed from the model. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder Ignore([NotNull] string name) + { + Check.NotEmpty(name, nameof(name)); + + Builder.Ignore(name, ConfigurationSource.Explicit); + + return this; + } + /// /// Applies configuration that is defined in an instance. /// @@ -342,6 +477,43 @@ public virtual OwnedEntityTypeBuilder Owned([NotNull] Type type) return new OwnedEntityTypeBuilder(); } + /// + /// + /// Marks an entity type as shared type. All references to this type will be configured as separate entity types. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// The entity type to be configured. + public virtual SharedEntityTypeBuilder SharedEntity() + where T : class + { + Builder.SharedEntity(typeof(T), ConfigurationSource.Explicit); + + return new SharedEntityTypeBuilder(); + } + + /// + /// + /// Marks an entity type as shared type. All references to this type will be configured as separate entity types. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// + /// + /// The entity type to be configured. + public virtual SharedEntityTypeBuilder SharedEntity([NotNull] Type type) + { + Check.NotNull(type, nameof(type)); + + Builder.SharedEntity(type, ConfigurationSource.Explicit); + + return new SharedEntityTypeBuilder(); + } + /// /// Configures the default to be used for this model. /// This strategy indicates how the context detects changes to properties for an instance of an entity type. diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 827ed1689b6..3e41dc691bd 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2733,12 +2733,20 @@ public static string InvalidSetSharedType([CanBeNull] object typeName) typeName); /// - /// Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type. + /// Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model. /// - public static string DoNotUseUsingEntityOnSharedClrType([CanBeNull] object clrType) + public static string CannotMarkShared([CanBeNull] object type) => string.Format( - GetString("DoNotUseUsingEntityOnSharedClrType", nameof(clrType)), - clrType); + GetString("CannotMarkShared", nameof(type)), + type); + + /// + /// Type '{type}' is not been configured as shared type in the model. Before calling 'UsingEntity', please mark the type as shared or add the entity type in the model as shared entity. + /// + public static string TypeNotMarkedAsShared([CanBeNull] object type) + => string.Format( + GetString("TypeNotMarkedAsShared", nameof(type)), + type); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index f4ce86df6a5..55fb9e1342f 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -1438,7 +1438,10 @@ Cannot create a DbSet for '{typeName}' because it is configured as an shared type entity type and should be accessed through entity type name based Set method. - - Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type. + + Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model. + + + Type '{type}' is not been configured as shared type in the model. Before calling 'UsingEntity', please mark the type as shared or add the entity type in the model as shared entity. \ No newline at end of file diff --git a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs index 1faa3027bdd..eb444c1c398 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs @@ -477,6 +477,34 @@ public void Cannot_remove_manually_created_association_entity_type(bool removeSk Assert.Same(manyToManyJoin.Metadata, leftSkipNav.AssociationEntityType); } + [ConditionalFact] + public void Can_add_shared_type() + { + var model = new Model(); + var modelBuilder = CreateModelBuilder(model); + + var entityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + var sharedTypeName = "SpecialDetails"; + + Assert.NotNull(modelBuilder.SharedEntity(sharedTypeName, typeof(Details), ConfigurationSource.Convention)); + + Assert.True(model.FindEntityType(sharedTypeName).HasSharedClrType); + + Assert.Equal( + CoreStrings.ClashingMismatchedSharedType("SpecialDetails"), + Assert.Throws(() => modelBuilder.SharedEntity(sharedTypeName, typeof(Product), ConfigurationSource.DataAnnotation)).Message); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.Null(modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.Explicit)); + + Assert.Equal( + CoreStrings.ClashingNonSharedType(typeof(Product).DisplayName()), + Assert.Throws(() => modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.Explicit)).Message); + } + private static void Cleanup(InternalModelBuilder modelBuilder) { new ModelCleanupConvention(CreateDependencies()) diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index ad4c4b67d1b..1fe4340310b 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; @@ -356,6 +357,107 @@ public virtual void Navigation_properties_can_set_access_mode_using_navigation_n Assert.Equal(PropertyAccessMode.Field, principal.FindSkipNavigation("Dependents").GetPropertyAccessMode()); Assert.Equal(PropertyAccessMode.Property, dependent.FindSkipNavigation("ManyToManyPrincipals").GetPropertyAccessMode()); } + + [ConditionalFact] + public virtual void Can_use_shared_Type_as_join_entity() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared1", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany()); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared2", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany(), + e => e.IndexerProperty("Payload")); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.Equal(2, shared1.GetForeignKeys().Count()); + Assert.True(shared1.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared1.ClrType); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.Equal(2, shared2.GetForeignKeys().Count()); + Assert.True(shared2.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared2.ClrType); + Assert.NotNull(shared2.FindProperty("Payload")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_fails_when_not_marked() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + CoreStrings.TypeNotMarkedAsShared(typeof(ManyToManyJoinWithFields).DisplayName()), + Assert.Throws( + () => modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany())).Message); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_passed_when_marked_as_shared_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.SharedEntity(); + + var associationEntityType = modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany()).Metadata; + + Assert.True(associationEntityType.HasSharedClrType); + Assert.Equal("Shared", associationEntityType.Name); + Assert.Equal(typeof(ManyToManyJoinWithFields), associationEntityType.ClrType); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_passes_when_configured_as_shared() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.SharedEntity("Shared"); + + var associationEntityType = modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany()).Metadata; + + Assert.True(associationEntityType.HasSharedClrType); + Assert.Equal("Shared", associationEntityType.Name); + Assert.Equal(typeof(ManyToManyJoinWithFields), associationEntityType.ClrType); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs index a26a57a3d29..9f81aa0d2d7 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs @@ -48,6 +48,9 @@ public GenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericStringTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -56,8 +59,19 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); public override TestModelBuilder Ignore() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs index d3b08524a5b..37527c5178b 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs @@ -29,6 +29,9 @@ public GenericTypeTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTypeTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTypeTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -37,8 +40,19 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTypeTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); public override TestModelBuilder Ignore() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 67be90429d6..cfde1fa3e94 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -127,6 +127,11 @@ public GenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -135,6 +140,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); @@ -349,6 +363,20 @@ public GenericTestOwnedEntityTypeBuilder(OwnedEntityTypeBuilder ownedEn public OwnedEntityTypeBuilder Instance => OwnedEntityTypeBuilder; } + protected class GenericTestSharedEntityTypeBuilder : TestSharedEntityTypeBuilder, + IInfrastructure> + where TEntity : class + { + public GenericTestSharedEntityTypeBuilder(SharedEntityTypeBuilder sharedEntityTypeBuilder) + { + SharedEntityTypeBuilder = sharedEntityTypeBuilder; + } + + protected SharedEntityTypeBuilder SharedEntityTypeBuilder { get; } + + public SharedEntityTypeBuilder Instance => SharedEntityTypeBuilder; + } + protected class GenericTestPropertyBuilder : TestPropertyBuilder, IInfrastructure> { public GenericTestPropertyBuilder(PropertyBuilder propertyBuilder) @@ -652,7 +680,35 @@ public override TestEntityTypeBuilder UsingEntity ((GenericTestReferenceCollectionBuilder)configureLeft( new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new GenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -660,6 +716,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, l => ((GenericTestReferenceCollectionBuilder)configureRight( new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, r => ((GenericTestReferenceCollectionBuilder)configureLeft( diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs index 4c491d10c51..3dc031a1748 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs @@ -81,6 +81,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -89,9 +92,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), + e => buildAction(new NonGenericStringTestEntityTypeBuilder(e))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index c01283d2765..8dc7fe1aa4f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -75,6 +75,9 @@ public NonGenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -83,9 +86,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); @@ -332,6 +347,20 @@ public NonGenericTestOwnedEntityTypeBuilder(OwnedEntityTypeBuilder ownedEntityTy public OwnedEntityTypeBuilder Instance => OwnedEntityTypeBuilder; } + protected class NonGenericTestSharedEntityTypeBuilder : TestSharedEntityTypeBuilder, + IInfrastructure + where TEntity : class + { + public NonGenericTestSharedEntityTypeBuilder(SharedEntityTypeBuilder sharedEntityTypeBuilder) + { + SharedEntityTypeBuilder = sharedEntityTypeBuilder; + } + + protected SharedEntityTypeBuilder SharedEntityTypeBuilder { get; } + + public SharedEntityTypeBuilder Instance => SharedEntityTypeBuilder; + } + protected class NonGenericTestPropertyBuilder : TestPropertyBuilder, IInfrastructure { public NonGenericTestPropertyBuilder(PropertyBuilder propertyBuilder) @@ -646,7 +675,37 @@ public override TestEntityTypeBuilder UsingEntity ((NonGenericTestReferenceCollectionBuilder)configureLeft( new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new NonGenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -654,6 +713,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, typeof(TAssociationEntity), l => ((NonGenericTestReferenceCollectionBuilder)configureRight( new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs index bba78ade724..7bcb6edeff0 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs @@ -30,6 +30,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -38,9 +41,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 6784adb0b5b..6367163c998 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -150,12 +150,21 @@ public TestModelBuilder HasAnnotation(string annotation, object value) public abstract TestEntityTypeBuilder Entity() where TEntity : class; + public abstract TestEntityTypeBuilder SharedEntity(string name) + where TEntity : class; + public abstract TestOwnedEntityTypeBuilder Owned() where TEntity : class; + public abstract TestSharedEntityTypeBuilder SharedEntity() + where TEntity : class; + public abstract TestModelBuilder Entity(Action> buildAction) where TEntity : class; + public abstract TestModelBuilder SharedEntity(string name, Action> buildAction) + where TEntity : class; + public abstract TestModelBuilder Ignore() where TEntity : class; @@ -292,6 +301,10 @@ public abstract class TestOwnedEntityTypeBuilder where TEntity : class { } + public abstract class TestSharedEntityTypeBuilder + where TEntity : class + { + } public abstract class TestKeyBuilder { @@ -446,7 +459,24 @@ public abstract TestEntityTypeBuilder UsingEntity> configureLeft) where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + where TAssociationEntity : class; + + public abstract TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index d80328bb7d8..707ffbe6316 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1540,6 +1540,44 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property_diction Assert.Equal(2, data["Required"]); Assert.False(data.ContainsKey("Optional")); } + + [ConditionalFact] + public virtual void Can_add_shared_type_entity_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.SharedEntity>("Shared1"); + + modelBuilder.SharedEntity>("Shared2", b => b.IndexerProperty("Id")); + + var model = modelBuilder.Model; + Assert.Equal(2, model.GetEntityTypes().Count()); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.True(shared1.HasSharedClrType); + Assert.Null(shared1.FindProperty("Id")); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.True(shared2.HasSharedClrType); + Assert.NotNull(shared2.FindProperty("Id")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void Cannot_add_shared_type_when_non_shared_exists() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.ClashingNonSharedType("Shared1"), + Assert.Throws(() => modelBuilder.SharedEntity("Shared1")).Message); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index a30f95f0d6d..ec74f9c8180 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -144,7 +144,6 @@ protected class Order : INotifyPropertyChanged public OrderCombination OrderCombination { get; set; } public OrderDetails Details { get; set; } public ICollection Products { get; set; } - public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { @@ -930,7 +929,7 @@ private class OwnedOneToManyNavDependent public OneToManyNavPrincipalOwner OneToManyOwner { get; set; } } - public class OwnerOfOwnees + protected class OwnerOfOwnees { public string Id { get; private set; } @@ -938,22 +937,22 @@ public class OwnerOfOwnees public Ownee1 Ownee1 { get; private set; } } - public class Ownee1 + protected class Ownee1 { public Ownee3 NewOwnee3 { get; private set; } } - public class Ownee2 + protected class Ownee2 { public Ownee3 Ownee3 { get; private set; } } - public class Ownee3 + protected class Ownee3 { public string Name { get; private set; } } - public class OneToManyPrincipalWithField + protected class OneToManyPrincipalWithField { public int Id; public Guid AlternateKey; @@ -962,7 +961,7 @@ public class OneToManyPrincipalWithField public IEnumerable Dependents; } - public class OneToOnePrincipalWithField + protected class OneToOnePrincipalWithField { public int Id; public string Name; @@ -970,7 +969,7 @@ public class OneToOnePrincipalWithField public DependentWithField Dependent; } - public class ManyToManyPrincipalWithField + protected class ManyToManyPrincipalWithField { public int Id; public string Name; @@ -987,7 +986,7 @@ protected class ManyToManyJoinWithFields public DependentWithField DependentWithField { get; set; } } - public class DependentWithField + protected class DependentWithField { public int DependentWithFieldId; @@ -999,7 +998,7 @@ public class DependentWithField public List ManyToManyPrincipals { get; set; } } - public class OneToManyOwnerWithField + protected class OneToManyOwnerWithField { public int Id; public Guid AlternateKey; @@ -1008,7 +1007,7 @@ public class OneToManyOwnerWithField public List OwnedDependents { get; set; } } - public class OneToManyOwnedWithField + protected class OneToManyOwnedWithField { public string FirstName; public string LastName; @@ -1017,7 +1016,7 @@ public class OneToManyOwnedWithField public OneToManyOwnerWithField OneToManyOwner { get; set; } } - public class OneToOneOwnerWithField + protected class OneToOneOwnerWithField { public int Id; public Guid AlternateKey; @@ -1026,7 +1025,7 @@ public class OneToOneOwnerWithField public OneToOneOwnedWithField OwnedDependent { get; set; } } - public class OneToOneOwnedWithField + protected class OneToOneOwnedWithField { public string FirstName; public string LastName; @@ -1036,7 +1035,7 @@ public class OneToOneOwnedWithField } - public class ImplicitManyToManyA + protected class ImplicitManyToManyA { public int Id { get; set; } public string Name { get; set; } @@ -1045,12 +1044,55 @@ public class ImplicitManyToManyA } - public class ImplicitManyToManyB + protected class ImplicitManyToManyB { public int Id { get; set; } public string Name { get; set; } public List As { get; set; } } + + protected class SharedHolderAlpha + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedHolderBeta + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedTypeEntityType + { + [NotMapped] + public int Random { get; set; } + [NotMapped] + public SharedNestedOwnedEntityType NestedReference { get; set; } + [NotMapped] + public List NestedCollection { get; set; } + [NotMapped] + public NestedReference ReferenceNavigation { get; set; } + } + + protected class SharedNestedOwnedEntityType + { + [NotMapped] + public int NestedRandom { get; set; } + } + + protected class NestedReference + { + public int Id { get; set; } + [NotMapped] + public string Value { get; set; } + } } } From 2c0ee81c4f2aaaeab3366df522056bc16283cc23 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 15 Jul 2020 16:09:04 -0400 Subject: [PATCH 2/4] Deprecate defining query at Core level Fixes #18903 --- .../InMemoryEntityTypeBuilderExtensions.cs | 77 +++++++++++++++++++ .../InMemoryEntityTypeExtensions.cs | 68 ++++++++++++++++ .../InMemoryServiceCollectionExtensions.cs | 4 +- .../Internal/InMemoryModelValidator.cs | 68 ++++++++++++++++ .../DefiningQueryRewritingConvention.cs | 44 +++++++++++ .../InMemoryConventionSetBuilder.cs | 10 +++ .../RelationalEntityTypeExtensions.cs | 4 + .../RelationalConventionSetBuilder.cs | 2 +- ...lationalQueryFilterRewritingConvention.cs} | 30 +++++++- .../ConventionEntityTypeExtensions.cs | 2 + src/EFCore/Extensions/EntityTypeExtensions.cs | 1 + .../Extensions/MutableEntityTypeExtensions.cs | 1 + src/EFCore/Infrastructure/ModelValidator.cs | 52 ------------- .../Metadata/Builders/EntityTypeBuilder`.cs | 1 + .../Builders/IConventionEntityTypeBuilder.cs | 8 +- .../ProviderConventionSetBuilder.cs | 2 +- ...n.cs => QueryFilterRewritingConvention.cs} | 12 +-- src/EFCore/Metadata/Internal/EntityType.cs | 1 + .../Internal/InternalEntityTypeBuilder.cs | 4 + src/EFCore/Properties/CoreStrings.Designer.cs | 8 -- src/EFCore/Properties/CoreStrings.resx | 3 - .../NavigationExpandingExpressionVisitor.cs | 4 + ...NorthwindKeylessEntitiesQueryCosmosTest.cs | 8 +- .../Query/NorthwindQueryCosmosFixture.cs | 33 ++++++++ .../Query/InheritanceQueryInMemoryFixture.cs | 8 +- .../Query/QueryBugsInMemoryTest.cs | 2 + .../WithConstructorsInMemoryTest.cs | 7 +- .../InMemoryModelValidatorTest.cs | 29 +++++++ .../InheritanceQueryRelationalFixture.cs | 2 + .../Northwind/NorthwindRelationalContext.cs | 2 + .../Internal/MigrationsModelDifferTest.cs | 2 + .../TestModels/Northwind/NorthwindContext.cs | 2 + .../Query/QueryBugsTest.cs | 6 ++ .../WithConstructorsSqlServerTest.cs | 2 + .../WithConstructorsSqliteTest.cs | 2 + .../Infrastructure/ModelValidatorTest.cs | 22 ------ .../ModelBuilding/ModelBuilderTestBase.cs | 6 +- .../ModelBuilding/OneToManyTestBase.cs | 8 +- .../ModelBuilding/OneToOneTestBase.cs | 12 +-- 39 files changed, 425 insertions(+), 134 deletions(-) create mode 100644 src/EFCore.InMemory/Extensions/InMemoryEntityTypeBuilderExtensions.cs create mode 100644 src/EFCore.InMemory/Extensions/InMemoryEntityTypeExtensions.cs create mode 100644 src/EFCore.InMemory/Internal/InMemoryModelValidator.cs create mode 100644 src/EFCore.InMemory/Metadata/Conventions/DefiningQueryRewritingConvention.cs rename src/EFCore.Relational/Metadata/Conventions/{RelationalQueryFilterDefiningQueryRewritingConvention.cs => RelationalQueryFilterRewritingConvention.cs} (74%) rename src/EFCore/Metadata/Conventions/{QueryFilterDefiningQueryRewritingConvention.cs => QueryFilterRewritingConvention.cs} (91%) create mode 100644 test/EFCore.InMemory.Tests/Infrastructure/InMemoryModelValidatorTest.cs diff --git a/src/EFCore.InMemory/Extensions/InMemoryEntityTypeBuilderExtensions.cs b/src/EFCore.InMemory/Extensions/InMemoryEntityTypeBuilderExtensions.cs new file mode 100644 index 00000000000..86745b72f80 --- /dev/null +++ b/src/EFCore.InMemory/Extensions/InMemoryEntityTypeBuilderExtensions.cs @@ -0,0 +1,77 @@ +// 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; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Extension methods for for the in-memory provider. + /// + public static class InMemoryEntityTypeBuilderExtensions + { + /// + /// Configures a query used to provide data for an entity type. + /// + /// The builder for the entity type being configured. + /// The query that will provide the underlying data for the entity type. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToQuery( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [NotNull] Expression>> query) + where TEntity : class + { + Check.NotNull(query, nameof(query)); + + InMemoryEntityTypeExtensions.SetDefiningQuery(entityTypeBuilder.Metadata, query); + + return entityTypeBuilder; + } + + /// + /// Configures a query used to provide data for an entity type. + /// + /// The builder for the entity type being configured. + /// The query that will provide the underlying data for the entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the query was set, otherwise. + /// + public static IConventionEntityTypeBuilder HasDefiningQuery( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + [CanBeNull] LambdaExpression query, + bool fromDataAnnotation = false) + { + if (CanSetDefiningQuery(entityTypeBuilder, query, fromDataAnnotation)) + { + InMemoryEntityTypeExtensions.SetDefiningQuery(entityTypeBuilder.Metadata, query, fromDataAnnotation); + + return entityTypeBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether the given defining query can be set from the current configuration source. + /// + /// The builder for the entity type being configured. + /// The query that will provide the underlying data for the keyless entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given defining query can be set. + public static bool CanSetDefiningQuery( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + [CanBeNull] LambdaExpression query, + bool fromDataAnnotation = false) +#pragma warning disable EF1001 // Internal EF Core API usage. + => entityTypeBuilder.CanSetAnnotation(CoreAnnotationNames.DefiningQuery, query, fromDataAnnotation); +#pragma warning restore EF1001 // Internal EF Core API usage. + } +} diff --git a/src/EFCore.InMemory/Extensions/InMemoryEntityTypeExtensions.cs b/src/EFCore.InMemory/Extensions/InMemoryEntityTypeExtensions.cs new file mode 100644 index 00000000000..751fffe0c72 --- /dev/null +++ b/src/EFCore.InMemory/Extensions/InMemoryEntityTypeExtensions.cs @@ -0,0 +1,68 @@ +// 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.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Extension methods for for the in-memory provider. + /// + public static class InMemoryEntityTypeExtensions + { + /// + /// Gets the LINQ query used as the default source for queries of this type. + /// + /// The entity type to get the defining query for. + /// The LINQ query used as the default source. + public static LambdaExpression GetDefiningQuery([NotNull] this IEntityType entityType) +#pragma warning disable EF1001 // Internal EF Core API usage. + => (LambdaExpression)Check.NotNull(entityType, nameof(entityType))[CoreAnnotationNames.DefiningQuery]; +#pragma warning restore EF1001 // Internal EF Core API usage. + + /// + /// Sets the LINQ query used as the default source for queries of this type. + /// + /// The entity type. + /// The LINQ query used as the default source. + public static void SetDefiningQuery( + [NotNull] this IMutableEntityType entityType, + [CanBeNull] LambdaExpression definingQuery) + => Check.NotNull(entityType, nameof(entityType)) +#pragma warning disable EF1001 // Internal EF Core API usage. + .SetOrRemoveAnnotation(CoreAnnotationNames.DefiningQuery, definingQuery); +#pragma warning restore EF1001 // Internal EF Core API usage. + + /// + /// Sets the LINQ query used as the default source for queries of this type. + /// + /// The entity type. + /// The LINQ query used as the default source. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured entity type. + public static LambdaExpression SetDefiningQuery( + [NotNull] this IConventionEntityType entityType, + [CanBeNull] LambdaExpression definingQuery, + bool fromDataAnnotation = false) + => (LambdaExpression)Check.NotNull(entityType, nameof(entityType)) +#pragma warning disable EF1001 // Internal EF Core API usage. + .SetOrRemoveAnnotation(CoreAnnotationNames.DefiningQuery, definingQuery, fromDataAnnotation) +#pragma warning restore EF1001 // Internal EF Core API usage. + ?.Value; + + /// + /// Returns the configuration source for . + /// + /// The entity type. + /// The configuration source for . + public static ConfigurationSource? GetDefiningQueryConfigurationSource([NotNull] this IConventionEntityType entityType) +#pragma warning disable EF1001 // Internal EF Core API usage. + => entityType.FindAnnotation(CoreAnnotationNames.DefiningQuery)?.GetConfigurationSource(); +#pragma warning restore EF1001 // Internal EF Core API usage. + } +} diff --git a/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs b/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs index bf6354621b4..f225b922012 100644 --- a/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs +++ b/src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.InMemory.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.InMemory.Internal; using Microsoft.EntityFrameworkCore.InMemory.Metadata.Conventions; using Microsoft.EntityFrameworkCore.InMemory.Query.Internal; using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; @@ -55,9 +56,8 @@ public static IServiceCollection AddEntityFrameworkInMemoryDatabase([NotNull] th .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() - - // New Query pipeline .TryAdd() .TryAdd() .TryAdd(p => p.GetService()) diff --git a/src/EFCore.InMemory/Internal/InMemoryModelValidator.cs b/src/EFCore.InMemory/Internal/InMemoryModelValidator.cs new file mode 100644 index 00000000000..fbb692ae0c3 --- /dev/null +++ b/src/EFCore.InMemory/Internal/InMemoryModelValidator.cs @@ -0,0 +1,68 @@ +// 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; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.InMemory.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 InMemoryModelValidator : ModelValidator + { + /// + /// 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 InMemoryModelValidator([NotNull] ModelValidatorDependencies dependencies) + : base(dependencies) + { + } + + /// + /// 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 override void Validate(IModel model, IDiagnosticsLogger logger) + { + base.Validate(model, logger); + + ValidateDefiningQuery(model, logger); + } + + /// + /// Validates the configuration of defining queries in the model. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateDefiningQuery( + [NotNull] IModel model, [NotNull] IDiagnosticsLogger logger) + { + Check.NotNull(model, nameof(model)); + + foreach (var entityType in model.GetEntityTypes()) + { + if (InMemoryEntityTypeExtensions.GetDefiningQuery(entityType) != null) + { + if (entityType.BaseType != null) + { + throw new InvalidOperationException( + CoreStrings.DerivedTypeDefiningQuery(entityType.DisplayName(), entityType.BaseType.DisplayName())); + } + } + } + } + } +} diff --git a/src/EFCore.InMemory/Metadata/Conventions/DefiningQueryRewritingConvention.cs b/src/EFCore.InMemory/Metadata/Conventions/DefiningQueryRewritingConvention.cs new file mode 100644 index 00000000000..bdc72132fd5 --- /dev/null +++ b/src/EFCore.InMemory/Metadata/Conventions/DefiningQueryRewritingConvention.cs @@ -0,0 +1,44 @@ +// 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.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// Convention that converts accesses of inside query filters and defining queries into . + /// This makes them consistent with how DbSet accesses in the actual queries are represented, which allows for easier processing in the + /// query pipeline. + /// + public class DefiningQueryRewritingConvention : QueryFilterRewritingConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public DefiningQueryRewritingConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) + { + } + + /// + public override void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var definingQuery = InMemoryEntityTypeExtensions.GetDefiningQuery(entityType); + if (definingQuery != null) + { + InMemoryEntityTypeExtensions.SetDefiningQuery( + entityType, (LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, definingQuery)); + } + } + } + } +} diff --git a/src/EFCore.InMemory/Metadata/Conventions/InMemoryConventionSetBuilder.cs b/src/EFCore.InMemory/Metadata/Conventions/InMemoryConventionSetBuilder.cs index 2c7033e7379..2b8d875715f 100644 --- a/src/EFCore.InMemory/Metadata/Conventions/InMemoryConventionSetBuilder.cs +++ b/src/EFCore.InMemory/Metadata/Conventions/InMemoryConventionSetBuilder.cs @@ -33,6 +33,16 @@ public InMemoryConventionSetBuilder( { } + /// + public override ConventionSet CreateConventionSet() + { + var conventionSet = base.CreateConventionSet(); + + conventionSet.ModelFinalizingConventions.Add(new DefiningQueryRewritingConvention(Dependencies)); + + return conventionSet; + } + /// /// /// Call this method to build a for the in-memory provider when using diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 92143b3c165..b5c7d3c3849 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -39,7 +39,9 @@ public static string GetTableName([NotNull] this IEntityType entityType) return (entityType as IConventionEntityType)?.GetViewNameConfigurationSource() == null && ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null) +#pragma warning disable CS0618 // Type or member is obsolete && ((entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null) +#pragma warning restore CS0618 // Type or member is obsolete ? GetDefaultTableName(entityType) : null; } @@ -248,7 +250,9 @@ public static string GetViewName([NotNull] this IEntityType entityType) } return ((entityType as IConventionEntityType)?.GetFunctionNameConfigurationSource() == null) +#pragma warning disable CS0618 // Type or member is obsolete && (entityType as IConventionEntityType)?.GetDefiningQueryConfigurationSource() == null +#pragma warning restore CS0618 // Type or member is obsolete ? GetDefaultViewName(entityType) : null; } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index b83e9e58c4d..07620b60970 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -112,7 +112,7 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(new DbFunctionTypeMappingConvention(Dependencies, RelationalDependencies)); ReplaceConvention( conventionSet.ModelFinalizingConventions, - (QueryFilterDefiningQueryRewritingConvention)new RelationalQueryFilterDefiningQueryRewritingConvention( + (QueryFilterRewritingConvention)new RelationalQueryFilterRewritingConvention( Dependencies, RelationalDependencies)); ConventionSet.AddAfter( diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterDefiningQueryRewritingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs similarity index 74% rename from src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterDefiningQueryRewritingConvention.cs rename to src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs index 9a0fdcc5e10..06f95e54293 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterDefiningQueryRewritingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalQueryFilterRewritingConvention.cs @@ -4,6 +4,7 @@ using System; using System.Linq.Expressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; @@ -12,14 +13,14 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { /// - public class RelationalQueryFilterDefiningQueryRewritingConvention : QueryFilterDefiningQueryRewritingConvention + public class RelationalQueryFilterRewritingConvention : QueryFilterRewritingConvention { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. /// Parameter object containing relational dependencies for this convention. - public RelationalQueryFilterDefiningQueryRewritingConvention( + public RelationalQueryFilterRewritingConvention( [NotNull] ProviderConventionSetBuilderDependencies dependencies, [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) : base(dependencies) @@ -27,6 +28,29 @@ public RelationalQueryFilterDefiningQueryRewritingConvention( DbSetAccessRewriter = new RelationalDbSetAccessRewritingExpressionVisitor(Dependencies.ContextType); } + /// + public override void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var queryFilter = entityType.GetQueryFilter(); + if (queryFilter != null) + { + entityType.SetQueryFilter((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + var definingQuery = entityType.GetDefiningQuery(); + if (definingQuery != null) + { + entityType.SetDefiningQuery((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, definingQuery)); + } +#pragma warning restore CS0618 // Type or member is obsolete + } + } + /// protected class RelationalDbSetAccessRewritingExpressionVisitor : DbSetAccessRewritingExpressionVisitor { diff --git a/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs b/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs index 0f2d955f941..fe70512a738 100644 --- a/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs +++ b/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs @@ -634,6 +634,7 @@ public static LambdaExpression SetQueryFilter( /// The LINQ query used as the default source. /// Indicates whether the configuration was specified using a data annotation. /// The configured entity type. + [Obsolete("Use InMemoryEntityTypeExtensions.SetDefiningQuery")] public static LambdaExpression SetDefiningQuery( [NotNull] this IConventionEntityType entityType, [CanBeNull] LambdaExpression definingQuery, @@ -648,6 +649,7 @@ public static LambdaExpression SetDefiningQuery( /// /// The entity type. /// The configuration source for . + [Obsolete("Use InMemoryEntityTypeExtensions.GetDefiningQueryConfigurationSource")] public static ConfigurationSource? GetDefiningQueryConfigurationSource([NotNull] this IConventionEntityType entityType) => entityType.FindAnnotation(CoreAnnotationNames.DefiningQuery)?.GetConfigurationSource(); diff --git a/src/EFCore/Extensions/EntityTypeExtensions.cs b/src/EFCore/Extensions/EntityTypeExtensions.cs index e5ff9206136..f9030e1d49d 100644 --- a/src/EFCore/Extensions/EntityTypeExtensions.cs +++ b/src/EFCore/Extensions/EntityTypeExtensions.cs @@ -721,6 +721,7 @@ public static LambdaExpression GetQueryFilter([NotNull] this IEntityType entityT /// /// The entity type to get the defining query for. /// The LINQ query used as the default source. + [Obsolete("Use InMemoryEntityTypeExtensions.GetDefiningQuery")] public static LambdaExpression GetDefiningQuery([NotNull] this IEntityType entityType) { Check.NotNull(entityType, nameof(entityType)); diff --git a/src/EFCore/Extensions/MutableEntityTypeExtensions.cs b/src/EFCore/Extensions/MutableEntityTypeExtensions.cs index a2245d61dc1..882412c6546 100644 --- a/src/EFCore/Extensions/MutableEntityTypeExtensions.cs +++ b/src/EFCore/Extensions/MutableEntityTypeExtensions.cs @@ -556,6 +556,7 @@ public static void SetQueryFilter( /// /// The entity type. /// The LINQ query used as the default source. + [Obsolete("Use InMemoryEntityTypeExtensions.SetDefiningQuery")] public static void SetDefiningQuery( [NotNull] this IMutableEntityType entityType, [CanBeNull] LambdaExpression definingQuery) diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index aed28fa485a..ea5873c5a97 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -69,9 +69,7 @@ public virtual void Validate(IModel model, IDiagnosticsLogger - /// Validates the mapping/configuration of entity types without keys in the model. - /// - /// The model to validate. - /// The logger to use. - protected virtual void ValidateKeylessTypes( - [NotNull] IModel model, [NotNull] IDiagnosticsLogger logger) - { - Check.NotNull(model, nameof(model)); - - foreach (var entityType in model.GetEntityTypes()) - { - if (entityType.GetDefiningQuery() != null) - { - if (entityType.BaseType != null) - { - throw new InvalidOperationException( - CoreStrings.DerivedTypeDefiningQuery(entityType.DisplayName(), entityType.BaseType.DisplayName())); - } - - if (entityType.FindPrimaryKey() != null) - { - throw new InvalidOperationException( - CoreStrings.DefiningQueryWithKey(entityType.DisplayName())); - } - } - } - } - /// /// Validates the mapping/configuration of query filters in the model. /// @@ -999,27 +968,6 @@ protected virtual void ValidateQueryFilters( } } - /// - /// Validates the mapping/configuration of defining queries in the model. - /// - /// The model to validate. - /// The logger to use. - protected virtual void ValidateDefiningQuery( - [NotNull] IModel model, [NotNull] IDiagnosticsLogger logger) - { - Check.NotNull(model, nameof(model)); - - foreach (var entityType in model.GetEntityTypes()) - { - if (entityType.GetDefiningQuery() != null - && entityType.FindPrimaryKey() != null) - { - throw new InvalidOperationException( - CoreStrings.DefiningQueryWithKey(entityType.DisplayName())); - } - } - } - /// /// Validates the mapping/configuration of data (e.g. seed data) in the model. /// diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index e3339372c96..5c9ba6df4bf 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -206,6 +206,7 @@ public virtual EntityTypeBuilder HasQueryFilter([CanBeNull] Expression< /// /// The query that will provide the underlying data for the keyless entity type. /// The same builder instance so that multiple calls can be chained. + [Obsolete("Use InMemoryEntityTypeBuilderExtensions.ToQuery")] public virtual EntityTypeBuilder ToQuery([NotNull] Expression>> query) { Check.NotNull(query, nameof(query)); diff --git a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs index 647d2086d7e..5b35718dee1 100644 --- a/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs @@ -658,11 +658,10 @@ IConventionSkipNavigationBuilder HasSkipNavigation( /// The query that will provide the underlying data for the keyless entity type. /// Indicates whether the configuration was specified using a data annotation. /// - /// The same builder instance if the query was set, - /// otherwise. + /// The same builder instance if the query was set, otherwise. /// - IConventionEntityTypeBuilder HasDefiningQuery( - [CanBeNull] LambdaExpression query, bool fromDataAnnotation = false); + [Obsolete("Use InMemoryEntityTypeBuilderExtensions.HasDefiningQuery")] + IConventionEntityTypeBuilder HasDefiningQuery([CanBeNull] LambdaExpression query, bool fromDataAnnotation = false); /// /// Returns a value indicating whether the given defining query can be set from the current configuration source. @@ -670,6 +669,7 @@ IConventionEntityTypeBuilder HasDefiningQuery( /// The query that will provide the underlying data for the keyless entity type. /// Indicates whether the configuration was specified using a data annotation. /// if the given defining query can be set. + [Obsolete("Use InMemoryEntityTypeBuilderExtensions.CanSetDefiningQuery")] bool CanSetDefiningQuery([CanBeNull] LambdaExpression query, bool fromDataAnnotation = false); /// diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 16ff813e211..25be69e5c9e 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -183,7 +183,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(servicePropertyDiscoveryConvention); conventionSet.ModelFinalizingConventions.Add(nonNullableReferencePropertyConvention); conventionSet.ModelFinalizingConventions.Add(nonNullableNavigationConvention); - conventionSet.ModelFinalizingConventions.Add(new QueryFilterDefiningQueryRewritingConvention(Dependencies)); + conventionSet.ModelFinalizingConventions.Add(new QueryFilterRewritingConvention(Dependencies)); conventionSet.ModelFinalizingConventions.Add(inversePropertyAttributeConvention); conventionSet.ModelFinalizingConventions.Add(backingFieldConvention); diff --git a/src/EFCore/Metadata/Conventions/QueryFilterDefiningQueryRewritingConvention.cs b/src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs similarity index 91% rename from src/EFCore/Metadata/Conventions/QueryFilterDefiningQueryRewritingConvention.cs rename to src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs index ea217da15ae..55a4d65626d 100644 --- a/src/EFCore/Metadata/Conventions/QueryFilterDefiningQueryRewritingConvention.cs +++ b/src/EFCore/Metadata/Conventions/QueryFilterRewritingConvention.cs @@ -16,13 +16,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// This makes them consistent with how DbSet accesses in the actual queries are represented, which allows for easier processing in the /// query pipeline. /// - public class QueryFilterDefiningQueryRewritingConvention : IModelFinalizingConvention + public class QueryFilterRewritingConvention : IModelFinalizingConvention { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. - public QueryFilterDefiningQueryRewritingConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) + public QueryFilterRewritingConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) { Dependencies = dependencies; DbSetAccessRewriter = new DbSetAccessRewritingExpressionVisitor(dependencies.ContextType); @@ -50,12 +50,6 @@ public virtual void ProcessModelFinalizing( { entityType.SetQueryFilter((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, queryFilter)); } - - var definingQuery = entityType.GetDefiningQuery(); - if (definingQuery != null) - { - entityType.SetDefiningQuery((LambdaExpression)DbSetAccessRewriter.Rewrite(modelBuilder.Metadata, definingQuery)); - } } } diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index b1efff19c61..4d43ea5c78c 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -2996,6 +2996,7 @@ public virtual string CheckQueryFilter([CanBeNull] LambdaExpression queryFilter) /// 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. /// + [Obsolete] public virtual LambdaExpression SetDefiningQuery([CanBeNull] LambdaExpression definingQuery, ConfigurationSource configurationSource) { this.SetOrRemoveAnnotation(CoreAnnotationNames.DefiningQuery, definingQuery, configurationSource); diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index b3b3eb7493c..c4894254bca 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -1295,6 +1295,7 @@ public virtual bool CanSetQueryFilter([CanBeNull] LambdaExpression filter, Confi /// 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. /// + [Obsolete] public virtual InternalEntityTypeBuilder HasDefiningQuery( [CanBeNull] LambdaExpression query, ConfigurationSource configurationSource) { @@ -1314,6 +1315,7 @@ public virtual InternalEntityTypeBuilder HasDefiningQuery( /// 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. /// + [Obsolete] public virtual bool CanSetDefiningQuery([CanBeNull] LambdaExpression query, ConfigurationSource configurationSource) => configurationSource.Overrides(Metadata.GetDefiningQueryConfigurationSource()) || Metadata.GetDefiningQuery() == query; @@ -4860,6 +4862,7 @@ bool IConventionEntityTypeBuilder.CanSetQueryFilter(LambdaExpression filter, boo /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] + [Obsolete] IConventionEntityTypeBuilder IConventionEntityTypeBuilder.HasDefiningQuery(LambdaExpression query, bool fromDataAnnotation) => HasDefiningQuery(query, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -4870,6 +4873,7 @@ IConventionEntityTypeBuilder IConventionEntityTypeBuilder.HasDefiningQuery(Lambd /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] + [Obsolete] bool IConventionEntityTypeBuilder.CanSetDefiningQuery(LambdaExpression query, bool fromDataAnnotation) => CanSetDefiningQuery(query, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 3e41dc691bd..080e322abde 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1686,14 +1686,6 @@ public static string BadFilterOwnedType([CanBeNull] object filter, [CanBeNull] o GetString("BadFilterOwnedType", nameof(filter), nameof(entityType)), filter, entityType); - /// - /// The entity type '{entityType}' cannot use 'ToQuery' to create a defining query because it also defines a primary key. Defining queries can only be used to back entity types without keys. - /// - public static string DefiningQueryWithKey([CanBeNull] object entityType) - => string.Format( - GetString("DefiningQueryWithKey", nameof(entityType)), - entityType); - /// /// Converter for model type '{converterType}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 55fb9e1342f..b6f8e401e7c 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -898,9 +898,6 @@ The filter expression '{filter}' cannot be specified for entity type '{entityType}'. A filter may only be applied to the entity that is not owned. - - The entity type '{entityType}' cannot use 'ToQuery' to create a defining query because it also defines a primary key. Defining queries can only be used to back entity types without keys. - Converter for model type '{converterType}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'. diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 14ab2385596..8cb7aa1c5e8 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -150,7 +150,9 @@ protected override Expression VisitExtension(Expression extensionExpression) { case QueryRootExpression queryRootExpression: var entityType = queryRootExpression.EntityType; +#pragma warning disable CS0618 // Type or member is obsolete var definingQuery = entityType.GetDefiningQuery(); +#pragma warning restore CS0618 // Type or member is obsolete NavigationExpansionExpression navigationExpansionExpression; if (definingQuery != null // Apply defining query only when it is not custom query root @@ -799,11 +801,13 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi if (source.PendingSelector is NavigationTreeExpression navigationTree && navigationTree.Value is EntityReference entityReference) { +#pragma warning disable CS0618 // Type or member is obsolete if (entityReference.EntityType.GetDefiningQuery() != null) { throw new InvalidOperationException( CoreStrings.IncludeOnEntityWithDefiningQueryNotSupported(entityReference.EntityType.DisplayName())); } +#pragma warning restore CS0618 // Type or member is obsolete if (expression is ConstantExpression includeConstant && includeConstant.Value is string navigationChain) diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindKeylessEntitiesQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindKeylessEntitiesQueryCosmosTest.cs index 25d295107ad..6a7f1f96f6a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindKeylessEntitiesQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindKeylessEntitiesQueryCosmosTest.cs @@ -27,7 +27,7 @@ public override async Task KeylessEntity_simple(bool async) await base.KeylessEntity_simple(async); AssertSql( - @"SELECT c[""Address""], c[""City""], c[""CompanyName""], c[""ContactName""], c[""ContactTitle""] + @"SELECT c FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } @@ -38,11 +38,11 @@ public override async Task KeylessEntity_where_simple(bool async) await base.KeylessEntity_where_simple(async); AssertSql( - @"SELECT c[""Address""], c[""City""], c[""CompanyName""], c[""ContactName""], c[""ContactTitle""] + @"SELECT c FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""City""] = ""London""))"); } - + [ConditionalFact] // views are not supported public override void KeylessEntity_by_database_view() { @@ -93,7 +93,7 @@ public override async Task KeylessEntity_with_defining_query(bool async) await base.KeylessEntity_with_defining_query(async); AssertSql( - @"SELECT c[""CustomerID""] + @"SELECT c FROM root c WHERE ((c[""Discriminator""] = ""Order"") AND (c[""CustomerID""] = ""ALFKI""))"); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs index cd56091aba1..dd7146e7711 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,5 +18,37 @@ public class NorthwindQueryCosmosFixture : NorthwindQueryFixtu public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder + .Entity() + .HasDiscriminator("Discriminator").HasValue("Customer"); + + modelBuilder + .Entity() + .HasDiscriminator("Discriminator").HasValue("Order"); + + modelBuilder + .Entity() + .HasDiscriminator("Discriminator").HasValue("Product"); + + modelBuilder + .Entity() + .HasDiscriminator("Discriminator").HasValue("Customer"); + +#pragma warning disable CS0618 // Type or member is obsolete + modelBuilder + .Entity().Metadata.SetDefiningQuery(null); + modelBuilder + .Entity().Metadata.SetDefiningQuery(null); + modelBuilder + .Entity().Metadata.SetDefiningQuery(null); + modelBuilder + .Entity().Metadata.SetDefiningQuery(null); +#pragma warning restore CS0618 // Type or member is obsolete + } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/InheritanceQueryInMemoryFixture.cs b/test/EFCore.InMemory.FunctionalTests/Query/InheritanceQueryInMemoryFixture.cs index 17cc34add13..08641a3d4d3 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/InheritanceQueryInMemoryFixture.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/InheritanceQueryInMemoryFixture.cs @@ -21,11 +21,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity() - .HasNoKey() - .ToQuery( - () => context.Set() - .Select(b => MaterializeView(b))); + InMemoryEntityTypeBuilderExtensions.ToQuery( + modelBuilder.Entity().HasNoKey(), + () => context.Set().Select(b => MaterializeView(b))); } private static AnimalQuery MaterializeView(Bird bird) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs index 7faeb15e4e1..09d2e0da933 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs @@ -709,7 +709,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery(Build_Customers_Sql_View_InMemory()); +#pragma warning restore CS0618 // Type or member is obsolete } private Expression>> Build_Customers_Sql_View_InMemory() diff --git a/test/EFCore.InMemory.FunctionalTests/WithConstructorsInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/WithConstructorsInMemoryTest.cs index 43ceedaba15..29c90ed5bc9 100644 --- a/test/EFCore.InMemory.FunctionalTests/WithConstructorsInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/WithConstructorsInMemoryTest.cs @@ -32,10 +32,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder + InMemoryEntityTypeBuilderExtensions.ToQuery( + modelBuilder .Entity() - .HasNoKey() - .ToQuery(() => context.Set().Select(b => new BlogQuery(b.Title, b.MonthlyRevenue))); + .HasNoKey(), + () => context.Set().Select(b => new BlogQuery(b.Title, b.MonthlyRevenue))); } } } diff --git a/test/EFCore.InMemory.Tests/Infrastructure/InMemoryModelValidatorTest.cs b/test/EFCore.InMemory.Tests/Infrastructure/InMemoryModelValidatorTest.cs new file mode 100644 index 00000000000..57ac3e1c60d --- /dev/null +++ b/test/EFCore.InMemory.Tests/Infrastructure/InMemoryModelValidatorTest.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Infrastructure +{ + public class InMemoryModelValidatorTest : ModelValidatorTestBase + { + [ConditionalFact] + public virtual void Detects_ToQuery_on_derived_keyless_types() + { + var modelBuilder = base.CreateConventionalModelBuilder(); + var context = new DbContext(new DbContextOptions()); + InMemoryEntityTypeBuilderExtensions.ToQuery( + modelBuilder.Entity().HasNoKey(), () => context.Set()); + InMemoryEntityTypeBuilderExtensions.ToQuery( + modelBuilder.Entity>(), () => context.Set>()); + + VerifyError( + CoreStrings.DerivedTypeDefiningQuery("Generic", nameof(Abstract)), + modelBuilder.Model); + } + + protected override TestHelpers TestHelpers => InMemoryTestHelpers.Instance; + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/InheritanceQueryRelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/Query/InheritanceQueryRelationalFixture.cs index a6b9990b5ad..ec7e9c2dccf 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/InheritanceQueryRelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/InheritanceQueryRelationalFixture.cs @@ -32,8 +32,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().Property(e => e.SugarGrams).HasColumnName("SugarGrams"); modelBuilder.Entity().Property(e => e.CaffeineGrams).HasColumnName("CaffeineGrams"); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery( () => context.Set().FromSqlRaw("SELECT * FROM Animals")); +#pragma warning restore CS0618 // Type or member is obsolete modelBuilder.Entity().HasDiscriminator().HasValue("Kiwi"); modelBuilder.Entity().HasDiscriminator().HasValue("Eagle"); } diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/Northwind/NorthwindRelationalContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/Northwind/NorthwindRelationalContext.cs index c39e435d787..ff0decc59bd 100644 --- a/test/EFCore.Relational.Specification.Tests/TestModels/Northwind/NorthwindRelationalContext.cs +++ b/test/EFCore.Relational.Specification.Tests/TestModels/Northwind/NorthwindRelationalContext.cs @@ -27,6 +27,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasKey(coh => coh.ProductName); modelBuilder.Entity().HasKey(mep => mep.TenMostExpensiveProducts); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery( () => CustomerQueries.FromSqlInterpolated( $"SELECT [c].[CustomerID] + {_empty} as [CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c]" @@ -40,6 +41,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .FromSqlRaw(@"select * from ""Orders""") .Select( o => new OrderQuery { CustomerID = o.CustomerID })); +#pragma warning restore CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToView("Alphabetical list of products"); } diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index f0d3dc4b26c..a9f543fe9fd 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -65,8 +65,10 @@ public void Model_differ_does_not_detect_queries() DbContext context = null; Execute( _ => { }, +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder => modelBuilder.Entity().HasNoKey().ToQuery( () => context.Set().FromSqlRaw("SELECT * FROM Vista")), +#pragma warning restore CS0618 // Type or member is obsolete result => Assert.Empty(result)); } diff --git a/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs b/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs index 81a202bba8e..a7b8ba2f898 100644 --- a/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/Northwind/NorthwindContext.cs @@ -74,6 +74,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) od => new { od.OrderID, od.ProductID }); }); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder .Entity() .HasNoKey() @@ -127,6 +128,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) OrderCount = c.Orders.Count(), SearchTerm = _searchTerm })); +#pragma warning restore CS0618 // Type or member is obsolete } public string TenantPrefix { get; set; } = "B"; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 0deb69e1633..bfab233df86 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -3390,6 +3390,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasQueryFilter(l => l.Name.StartsWith("Bran")); // this one is ignored modelBuilder.Entity().HasQueryFilter(f => Leaders.Any(l => l.Name == "Crach an Craite")); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder .Entity() .HasNoKey() @@ -3407,6 +3408,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Where(fq => fq.Name != "Bar") .Select( fq => new LeaderQuery { Name = "Not Bar" })); +#pragma warning restore CS0618 // Type or member is obsolete } } @@ -4429,11 +4431,13 @@ public MyContext13346(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity() .HasNoKey() .ToQuery( () => Set() .FromSqlRaw("SELECT o.Amount From Orders AS o")); +#pragma warning restore CS0618 // Type or member is obsolete } } @@ -7027,7 +7031,9 @@ from cm in g.DefaultIfEmpty() select a).Count() > 0) .HasKey(e => e.CustomerId); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery(Build_Customers_Sql_View_InMemory()); +#pragma warning restore CS0618 // Type or member is obsolete } private Expression>> Build_Customers_Sql_View_InMemory() diff --git a/test/EFCore.SqlServer.FunctionalTests/WithConstructorsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/WithConstructorsSqlServerTest.cs index 8a2697f0313..33df5078bfd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/WithConstructorsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/WithConstructorsSqlServerTest.cs @@ -25,8 +25,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery( () => context.Set().FromSqlRaw("SELECT * FROM Blog")); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/WithConstructorsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/WithConstructorsSqliteTest.cs index edbe63dd83c..0c67c27e844 100644 --- a/test/EFCore.Sqlite.FunctionalTests/WithConstructorsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/WithConstructorsSqliteTest.cs @@ -25,8 +25,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); +#pragma warning disable CS0618 // Type or member is obsolete modelBuilder.Entity().HasNoKey().ToQuery( () => context.Set().FromSqlRaw("SELECT * FROM Blog")); +#pragma warning restore CS0618 // Type or member is obsolete } } } diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index c6042e6be84..b301c5b52f9 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -187,15 +187,6 @@ public virtual void Detects_filter_on_owned_type() VerifyError(CoreStrings.BadFilterOwnedType(queryFilter, nameof(ReferencedEntity)), modelBuilder.Model); } - [ConditionalFact] - public virtual void Detects_defining_query_on_keyed_entity_type() - { - var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().ToQuery(() => new List().AsQueryable()); - - VerifyError(CoreStrings.DefiningQueryWithKey("A"), modelBuilder.Model); - } - [ConditionalFact] public virtual void Detects_shadow_entities() { @@ -914,19 +905,6 @@ public virtual void Passes_ForeignKey_on_inherited_generated_key_property_abstra Validate(modelBuilder.Model); } - [ConditionalFact] - public virtual void Detects_ToQuery_on_derived_keyless_types() - { - var modelBuilder = base.CreateConventionalModelBuilder(); - var context = new DbContext(new DbContextOptions()); - modelBuilder.Entity().HasNoKey().ToQuery(() => context.Set()); - modelBuilder.Entity>().ToQuery(() => context.Set>()); - - VerifyError( - CoreStrings.DerivedTypeDefiningQuery("Generic", nameof(Abstract)), - modelBuilder.Model); - } - [ConditionalTheory] [InlineData(ChangeTrackingStrategy.ChangedNotifications)] [InlineData(ChangeTrackingStrategy.ChangingAndChangedNotifications)] diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 6367163c998..dca02421aa1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -100,10 +100,8 @@ protected TestModelBuilder HobNobBuilder() { var builder = CreateModelBuilder(); - builder.Entity().HasKey( - e => new { e.Id1, e.Id2 }); - builder.Entity().HasKey( - e => new { e.Id1, e.Id2 }); + builder.Entity().HasKey(e => new { e.Id1, e.Id2 }); + builder.Entity().HasKey(e => new { e.Id1, e.Id2 }); return builder; } diff --git a/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs index e9303de9584..b056465fa13 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToManyTestBase.cs @@ -2026,14 +2026,13 @@ public virtual void Nullable_FK_can_be_made_required() } [ConditionalFact] - public virtual void Non_nullable_FK_cannot_be_made_optional() + public virtual void Non_nullable_FK_can_be_made_optional() { var modelBuilder = HobNobBuilder(); modelBuilder .Entity().HasMany(e => e.Hobs).WithOne(e => e.Nob) - .HasForeignKey( - e => new { e.NobId1, e.NobId2 }) + .HasForeignKey(e => new { e.NobId1, e.NobId2 }) .IsRequired(false); modelBuilder.FinalizeModel(); @@ -2056,8 +2055,7 @@ public virtual void Non_nullable_FK_can_be_made_optional_separately() modelBuilder .Entity().HasMany(e => e.Hobs).WithOne(e => e.Nob) - .HasForeignKey( - e => new { e.NobId1, e.NobId2 }); + .HasForeignKey(e => new { e.NobId1, e.NobId2 }); modelBuilder .Entity().HasMany(e => e.Hobs).WithOne(e => e.Nob) diff --git a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs index 7fb4445744b..b33bcad610b 100644 --- a/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/OneToOneTestBase.cs @@ -3269,8 +3269,7 @@ public virtual void Non_nullable_FK_can_be_made_optional() .Entity() .Ignore(e => e.Hobs) .HasOne(e => e.Hob).WithOne(e => e.Nob) - .HasForeignKey( - e => new { e.NobId1, e.NobId2 }) + .HasForeignKey(e => new { e.NobId1, e.NobId2 }) .IsRequired(false); modelBuilder.FinalizeModel(); @@ -3300,8 +3299,7 @@ public virtual void Non_nullable_FK_can_be_made_optional_separately() .Entity() .Ignore(e => e.Hobs) .HasOne(e => e.Hob).WithOne(e => e.Nob) - .HasForeignKey( - e => new { e.NobId1, e.NobId2 }); + .HasForeignKey(e => new { e.NobId1, e.NobId2 }); modelBuilder .Entity() @@ -3334,8 +3332,7 @@ public virtual void Unspecified_FK_can_be_made_optional() modelBuilder .Entity().HasOne(e => e.Nob).WithOne(e => e.Hob) .IsRequired(false) - .HasPrincipalKey( - e => new { e.Id1, e.Id2 }); + .HasPrincipalKey(e => new { e.Id1, e.Id2 }); var fk = dependentType.GetForeignKeys().Single(); Assert.False(fk.IsRequired); @@ -3356,8 +3353,7 @@ public virtual void Unspecified_FK_can_be_made_optional_in_any_order() modelBuilder .Entity().HasOne(e => e.Nob).WithOne(e => e.Hob) - .HasPrincipalKey( - e => new { e.Id1, e.Id2 }) + .HasPrincipalKey(e => new { e.Id1, e.Id2 }) .IsRequired(false); var fk = dependentType.GetForeignKeys().Single(); From 3ebfde7e51aa08692ed1278232e3f1d4beb779be Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Sat, 18 Jul 2020 05:40:43 -0700 Subject: [PATCH 3/4] Query: Don't read default value from databases (#21668) In 3.x when reading values from database, we started returning default value of expected type if the value was null in the database. That means if we needed to read int then database value was null then we got 0 back. Which is not correct as we should throw exception for the case. The fix here is, - All value read from databases are of nullable type as database can return null. - Since all the bindings with database are nullable now, - When doing member access we add null check for value types and return nullable of return type - When doing method call we add null check for instance. All parameters we convert to original types. - All other expressions, where they need to match the type to reconstruct the original tree, adds convert nodes. - For translation of Single/SingleOrDefault (and friends) - For scalar subquery - We add coalesce with default value for SingleOrDefault - We don't add coalesce for Single case and assume there will be non-null value coming out of database which we will try to assign. - For non-scalar subquery - We add default value in client side in shaper for SingleOrDefault - We throw exception Sequence contains no elements for Single case - Above rules are only when not returning entity type. When returning entity type, we don't have enough information to differentiate the case. Also above behavior is just side-effect of assigning values to properties which cannot take null values. - When doing DefaultIfEmpty over a non-reference type, navigation expansion will add client side coalesce so that we can move Selector after DefaultIfEmpty Resolves #20633 Resolves #20959 Resolves #21002 --- .../CosmosQueryTranslationPostprocessor.cs | 43 --- .../CosmosQueryTranslationPostprocessor.cs | 52 ++++ ...jectionBindingRemovingExpressionVisitor.cs | 2 +- ...yExpressionTranslatingExpressionVisitor.cs | 11 +- ...emoryProjectionBindingExpressionVisitor.cs | 279 +++++++++++++----- .../Query/Internal/InMemoryQueryExpression.cs | 25 +- ...yableMethodTranslatingExpressionVisitor.cs | 28 +- ...jectionBindingRemovingExpressionVisitor.cs | 12 +- .../Properties/RelationalStrings.Designer.cs | 6 + .../Properties/RelationalStrings.resx | 3 + .../Internal/GetValueOrDefaultTranslator.cs | 14 +- ...ionalProjectionBindingExpressionVisitor.cs | 254 +++++++++++++--- .../Query/RelationalEntityShaperExpression.cs | 4 +- ...yableMethodTranslatingExpressionVisitor.cs | 42 ++- ...sitor.ShaperProcessingExpressionVisitor.cs | 108 ++++--- ...lationalSqlTranslatingExpressionVisitor.cs | 16 +- .../Query/SqlExpressions/SelectExpression.cs | 35 ++- .../Infrastructure/ExpressionExtensions.cs | 2 +- .../NavigationExpandingExpressionVisitor.cs | 10 +- src/Shared/ExpressionExtensions.cs | 6 - src/Shared/SharedTypeExtensions.cs | 16 +- .../BuiltInDataTypesCosmosTest.cs | 6 + .../CustomConvertersCosmosTest.cs | 12 + .../NorthwindMiscellaneousQueryCosmosTest.cs | 12 + .../Query/OwnedQueryCosmosTest.cs | 5 +- .../BuiltInDataTypesInMemoryTest.cs | 7 + .../ConvertToProviderTypesInMemoryTest.cs | 7 + .../CustomConvertersInMemoryTest.cs | 6 + .../ComplexNavigationsQueryInMemoryTest.cs | 6 + ...ComplexNavigationsWeakQueryInMemoryTest.cs | 6 + ...windAggregateOperatorsQueryInMemoryTest.cs | 22 +- ...NorthwindMiscellaneousQueryInMemoryTest.cs | 14 + .../Query/NullSemanticsQueryTestBase.cs | 4 +- .../BuiltInDataTypesTestBase.cs | 34 +++ .../CustomConvertersTestBase.cs | 36 +++ .../Query/ComplexNavigationsQueryTestBase.cs | 4 +- .../Query/GearsOfWarQueryTestBase.cs | 24 +- ...orthwindAggregateOperatorsQueryTestBase.cs | 21 +- .../NorthwindMiscellaneousQueryTestBase.cs | 29 +- .../Query/NorthwindSelectQueryTestBase.cs | 9 +- .../Query/OwnedQueryTestBase.cs | 27 +- .../Query/SpatialQueryTestBase.cs | 35 --- .../BuiltInDataTypesSqlServerTest.cs | 2 + .../ConvertToProviderTypesSqlServerTest.cs | 2 + .../CustomConvertersSqlServerTest.cs | 4 + .../EverythingIsBytesSqlServerTest.cs | 2 + .../EverythingIsStringsSqlServerTest.cs | 2 + .../{Query => }/NavigationTest.cs | 3 +- .../ComplexNavigationsQuerySqlServerTest.cs | 4 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 112 +++---- ...orthwindMiscellaneousQuerySqlServerTest.cs | 2 +- .../NorthwindNavigationsQuerySqlServerTest.cs | 4 +- .../NorthwindSelectQuerySqlServerTest.cs | 28 +- .../Query/OwnedQuerySqlServerTest.cs | 4 +- .../Query/QueryBugsTest.cs | 4 +- .../SpatialQuerySqlServerGeographyTest.cs | 22 -- .../SpatialQuerySqlServerGeometryTest.cs | 22 -- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 116 +++----- .../Query/SpatialQuerySqliteTest.cs | 24 -- 59 files changed, 1046 insertions(+), 605 deletions(-) delete mode 100644 src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs create mode 100644 src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs rename test/EFCore.SqlServer.FunctionalTests/{Query => }/NavigationTest.cs (98%) diff --git a/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs b/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs deleted file mode 100644 index 0d64f18fe35..00000000000 --- a/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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.Linq.Expressions; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Utilities; - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query -{ - /// - public class CosmosQueryTranslationPostprocessor : QueryTranslationPostprocessor - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - - /// - /// Creates a new instance of the class. - /// - /// Parameter object containing dependencies for this class. - /// The SqlExpressionFactory object to use. - /// The query compilation context object to use. - public CosmosQueryTranslationPostprocessor( - [NotNull] QueryTranslationPostprocessorDependencies dependencies, - [NotNull] ISqlExpressionFactory sqlExpressionFactory, - [NotNull] QueryCompilationContext queryCompilationContext) - : base(dependencies, queryCompilationContext) - { - Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); - - _sqlExpressionFactory = sqlExpressionFactory; - } - - /// - public override Expression Process(Expression query) - { - query = base.Process(query); - query = new CosmosValueConverterCompensatingExpressionVisitor(_sqlExpressionFactory).Visit(query); - - return query; - } - } -} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs new file mode 100644 index 00000000000..07341c85951 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs @@ -0,0 +1,52 @@ +// 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.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.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 CosmosQueryTranslationPostprocessor : QueryTranslationPostprocessor + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// 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 CosmosQueryTranslationPostprocessor( + [NotNull] QueryTranslationPostprocessorDependencies dependencies, + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] QueryCompilationContext queryCompilationContext) + : base(dependencies, queryCompilationContext) + { + Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// 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 override Expression Process(Expression query) + { + query = base.Process(query); + query = new CosmosValueConverterCompensatingExpressionVisitor(_sqlExpressionFactory).Visit(query); + + return query; + } + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs index 6d2ca88555e..147b0ee190e 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs @@ -23,7 +23,7 @@ public CosmosProjectionBindingRemovingExpressionVisitor( { _selectExpression = selectExpression; } - + protected override ProjectionExpression GetProjection(ProjectionBindingExpression projectionBindingExpression) => _selectExpression.Projection[GetProjectionIndex(projectionBindingExpression)]; diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index a583b0b30d4..266f5aea8e3 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -483,8 +483,16 @@ MethodInfo GetMethod() return new EntityReferenceExpression(subqueryTranslation); } + var shaperExpression = subqueryTranslation.ShaperExpression; + if (shaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert + && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type) + { + shaperExpression = unaryExpression.Operand; + } + #pragma warning disable IDE0046 // Convert to conditional expression - if (!(subqueryTranslation.ShaperExpression is ProjectionBindingExpression projectionBindingExpression)) + if (!(shaperExpression is ProjectionBindingExpression projectionBindingExpression)) #pragma warning restore IDE0046 // Convert to conditional expression { return null; @@ -869,7 +877,6 @@ private Expression TryBindMember(Expression source, MemberIdentity member, Type if (property != null) { return BindProperty(entityReferenceExpression, property, type); - } AddTranslationErrorDetails( diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs index 41844ab73a6..dcbc3cec828 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -79,6 +80,8 @@ public virtual Expression Translate([NotNull] InMemoryQueryExpression queryExpre _projectionMapping.Clear(); _projectionMembers.Clear(); + result = MatchTypes(result, expression.Type); + return result; } @@ -164,18 +167,10 @@ public override Expression Visit(Expression expression) } var translation = _expressionTranslatingExpressionVisitor.Translate(expression); - if (translation == null) - { - return base.Visit(expression); - } - - if (translation.Type != expression.Type) - { - translation = NullSafeConvert(translation, expression.Type); - } - - return new ProjectionBindingExpression( - _queryExpression, _queryExpression.AddToProjection(translation), expression.Type); + return translation == null + ? base.Visit(expression) + : new ProjectionBindingExpression( + _queryExpression, _queryExpression.AddToProjection(translation), expression.Type.MakeNullable()); } else { @@ -185,37 +180,48 @@ public override Expression Visit(Expression expression) return null; } - if (translation.Type != expression.Type) - { - translation = NullSafeConvert(translation, expression.Type); - } - _projectionMapping[_projectionMembers.Peek()] = translation; - return new ProjectionBindingExpression(_queryExpression, _projectionMembers.Peek(), expression.Type); + return new ProjectionBindingExpression(_queryExpression, _projectionMembers.Peek(), expression.Type.MakeNullable()); } } return base.Visit(expression); } - private Expression NullSafeConvert(Expression expression, Type convertTo) - => expression.Type.IsNullableType() && !convertTo.IsNullableType() && expression.Type.UnwrapNullableType() == convertTo - ? (Expression)Expression.Coalesce(expression, Expression.Default(convertTo)) - : Expression.Convert(expression, convertTo); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitBinary(BinaryExpression binaryExpression) + { + var left = MatchTypes(Visit(binaryExpression.Left), binaryExpression.Left.Type); + var right = MatchTypes(Visit(binaryExpression.Right), binaryExpression.Right.Type); - private CollectionShaperExpression AddCollectionProjection( - ShapedQueryExpression subquery, INavigationBase navigation, Type elementType) - => new CollectionShaperExpression( - new ProjectionBindingExpression( - _queryExpression, - _queryExpression.AddSubqueryProjection( - subquery, - out var innerShaper), - typeof(IEnumerable)), - innerShaper, - navigation, - elementType); + return binaryExpression.Update(left, VisitAndConvert(binaryExpression.Conversion, "VisitBinary"), right); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitConditional(ConditionalExpression conditionalExpression) + { + var test = Visit(conditionalExpression.Test); + var ifTrue = Visit(conditionalExpression.IfTrue); + var ifFalse = Visit(conditionalExpression.IfFalse); + + if (test.Type == typeof(bool?)) + { + test = Expression.Equal(test, Expression.Constant(true, typeof(bool?))); + } + + return conditionalExpression.Update(test, ifTrue, ifFalse); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -269,43 +275,69 @@ protected override Expression VisitExtension(Expression extensionExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitNew(NewExpression newExpression) + protected override ElementInit VisitElementInit(ElementInit elementInit) + => elementInit.Update(elementInit.Arguments.Select(e => MatchTypes(Visit(e), e.Type))); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitMember(MemberExpression memberExpression) { - Check.NotNull(newExpression, nameof(newExpression)); + var expression = Visit(memberExpression.Expression); + Expression updatedMemberExpression = memberExpression.Update( + expression != null ? MatchTypes(expression, memberExpression.Expression.Type) : expression); - if (newExpression.Arguments.Count == 0) + if (expression?.Type.IsNullableValueType() == true) { - return newExpression; + var nullableReturnType = memberExpression.Type.MakeNullable(); + if (!memberExpression.Type.IsNullableType()) + { + updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType); + } + + updatedMemberExpression = Expression.Condition( + Expression.Equal(expression, Expression.Default(expression.Type)), + Expression.Constant(null, nullableReturnType), + updatedMemberExpression); } - if (!_clientEval - && newExpression.Members == null) + return updatedMemberExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + { + var expression = memberAssignment.Expression; + Expression visitedExpression; + if (_clientEval) { - return null; + visitedExpression = Visit(memberAssignment.Expression); } - - var newArguments = new Expression[newExpression.Arguments.Count]; - for (var i = 0; i < newArguments.Length; i++) + else { - if (_clientEval) + var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member); + _projectionMembers.Push(projectionMember); + + visitedExpression = Visit(memberAssignment.Expression); + if (visitedExpression == null) { - newArguments[i] = Visit(newExpression.Arguments[i]); + return null; } - else - { - var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]); - _projectionMembers.Push(projectionMember); - newArguments[i] = Visit(newExpression.Arguments[i]); - if (newArguments[i] == null) - { - return null; - } - _projectionMembers.Pop(); - } + _projectionMembers.Pop(); } - return newExpression.Update(newArguments); + visitedExpression = MatchTypes(visitedExpression, expression.Type); + + return memberAssignment.Update(visitedExpression); } /// @@ -348,24 +380,111 @@ protected override Expression VisitMemberInit(MemberInitExpression memberInitExp /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { - if (_clientEval) + var @object = Visit(methodCallExpression.Object); + var arguments = new Expression[methodCallExpression.Arguments.Count]; + for (var i = 0; i < methodCallExpression.Arguments.Count; i++) { - return memberAssignment.Update(Visit(memberAssignment.Expression)); + var argument = methodCallExpression.Arguments[i]; + arguments[i] = MatchTypes(Visit(argument), argument.Type); + } + + Expression updatedMethodCallExpression = methodCallExpression.Update( + @object != null ? MatchTypes(@object, methodCallExpression.Object.Type) : @object, + arguments); + + if (@object?.Type.IsNullableType() == true + && !methodCallExpression.Object.Type.IsNullableType()) + { + var nullableReturnType = methodCallExpression.Type.MakeNullable(); + if (!methodCallExpression.Type.IsNullableType()) + { + updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType); + } + + return Expression.Condition( + Expression.Equal(@object, Expression.Default(@object.Type)), + Expression.Constant(null, nullableReturnType), + updatedMethodCallExpression); } - var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member); - _projectionMembers.Push(projectionMember); + return updatedMethodCallExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitNew(NewExpression newExpression) + { + Check.NotNull(newExpression, nameof(newExpression)); + + if (newExpression.Arguments.Count == 0) + { + return newExpression; + } - var visitedExpression = Visit(memberAssignment.Expression); - if (visitedExpression == null) + if (!_clientEval + && newExpression.Members == null) { return null; } - _projectionMembers.Pop(); - return memberAssignment.Update(visitedExpression); + var newArguments = new Expression[newExpression.Arguments.Count]; + for (var i = 0; i < newArguments.Length; i++) + { + var argument = newExpression.Arguments[i]; + Expression visitedArgument; + if (_clientEval) + { + visitedArgument = Visit(argument); + } + else + { + var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]); + _projectionMembers.Push(projectionMember); + visitedArgument = Visit(argument); + if (visitedArgument == null) + { + return null; + } + + _projectionMembers.Pop(); + } + + newArguments[i] = MatchTypes(visitedArgument, argument.Type); + } + + return newExpression.Update(newArguments); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitNewArray(NewArrayExpression newArrayExpression) + => newArrayExpression.Update(newArrayExpression.Expressions.Select(e => MatchTypes(Visit(e), e.Type))); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitUnary(UnaryExpression unaryExpression) + { + var operand = Visit(unaryExpression.Operand); + + return (unaryExpression.NodeType == ExpressionType.Convert + || unaryExpression.NodeType == ExpressionType.ConvertChecked) + && unaryExpression.Type == operand.Type + ? operand + : unaryExpression.Update(MatchTypes(operand, unaryExpression.Operand.Type)); } // TODO: Debugging @@ -376,5 +495,31 @@ private void VerifyQueryExpression(ProjectionBindingExpression projectionBinding throw new InvalidOperationException(CoreStrings.QueryFailed(projectionBindingExpression.Print(), GetType().Name)); } } + + private CollectionShaperExpression AddCollectionProjection( + ShapedQueryExpression subquery, INavigationBase navigation, Type elementType) + => new CollectionShaperExpression( + new ProjectionBindingExpression( + _queryExpression, + _queryExpression.AddSubqueryProjection( + subquery, + out var innerShaper), + typeof(IEnumerable)), + innerShaper, + navigation, + elementType); + + private static Expression MatchTypes(Expression expression, Type targetType) + { + if (targetType != expression.Type + && targetType.TryGetElementType(typeof(IQueryable<>)) == null) + { + Check.DebugAssert(targetType.MakeNullable() == expression.Type, "Not a nullable to non-nullable conversion"); + + expression = Expression.Convert(expression, targetType); + } + + return expression; + } } } diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index ea56be5271f..9d14460626f 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -88,25 +88,30 @@ public InMemoryQueryExpression([NotNull] IEntityType entityType) _valueBufferParameter = Parameter(typeof(ValueBuffer), "valueBuffer"); ServerQueryExpression = new InMemoryTableExpression(entityType); var readExpressionMap = new Dictionary(); + var discriminatorProperty = entityType.GetDiscriminatorProperty(); foreach (var property in entityType.GetAllBaseTypesInclusive().SelectMany(et => et.GetDeclaredProperties())) { readExpressionMap[property] = CreateReadValueExpression(property.ClrType, property.GetIndex(), property); } - foreach (var property in entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties())) + foreach (var derivedEntityType in entityType.GetDerivedTypes()) { - readExpressionMap[property] = Condition( - LessThan( - Constant(property.GetIndex()), - MakeMemberAccess( - _valueBufferParameter, - _valueBufferCountMemberInfo)), - CreateReadValueExpression(property.ClrType, property.GetIndex(), property), - Default(property.ClrType)); + var entityCheck = derivedEntityType.GetConcreteDerivedTypesInclusive() + .Select(e => Equal(readExpressionMap[discriminatorProperty], Constant(e.GetDiscriminatorValue()))) + .Aggregate((l, r) => OrElse(l, r)); + + foreach (var property in derivedEntityType.GetDeclaredProperties()) + { + readExpressionMap[property] = Condition( + entityCheck, + CreateReadValueExpression(property.ClrType, property.GetIndex(), property), + Default(property.ClrType)); + } } var entityProjection = new EntityProjectionExpression(entityType, readExpressionMap); _projectionMapping[new ProjectionMember()] = entityProjection; + } /// @@ -123,7 +128,7 @@ public virtual Expression GetSingleScalarProjection() ConvertToEnumerable(); - return new ProjectionBindingExpression(this, new ProjectionMember(), expression.Type); + return new ProjectionBindingExpression(this, new ProjectionMember(), expression.Type.MakeNullable()); } /// diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index 3543db62faf..6bba55266e9 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -141,7 +141,7 @@ protected override ShapedQueryExpression TranslateAll(ShapedQueryExpression sour EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type), inMemoryQueryExpression.ServerQueryExpression))); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool))); } /// @@ -170,11 +170,11 @@ protected override ShapedQueryExpression TranslateAny(ShapedQueryExpression sour } inMemoryQueryExpression.UpdateServerQueryExpression( - Expression.Call( - EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type), - inMemoryQueryExpression.ServerQueryExpression)); + Expression.Call( + EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type), + inMemoryQueryExpression.ServerQueryExpression)); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool))); } /// @@ -188,7 +188,7 @@ protected override ShapedQueryExpression TranslateAverage(ShapedQueryExpression Check.NotNull(source, nameof(source)); Check.NotNull(resultType, nameof(resultType)); - return TranslateScalarAggregate(source, selector, nameof(Enumerable.Average)); + return TranslateScalarAggregate(source, selector, nameof(Enumerable.Average), resultType); } /// @@ -249,7 +249,7 @@ protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression inMemoryQueryExpression.GetMappedProjection(new ProjectionMember()), inMemoryQueryExpression.CurrentParameter)), item)); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool))); } /// @@ -284,7 +284,7 @@ protected override ShapedQueryExpression TranslateCount(ShapedQueryExpression so EnumerableMethods.CountWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type), inMemoryQueryExpression.ServerQueryExpression)); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(int))); } /// @@ -751,7 +751,7 @@ protected override ShapedQueryExpression TranslateLongCount(ShapedQueryExpressio inMemoryQueryExpression.CurrentParameter.Type), inMemoryQueryExpression.ServerQueryExpression)); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(long))); } /// @@ -765,7 +765,7 @@ protected override ShapedQueryExpression TranslateMax( { Check.NotNull(source, nameof(source)); - return TranslateScalarAggregate(source, selector, nameof(Enumerable.Max)); + return TranslateScalarAggregate(source, selector, nameof(Enumerable.Max), resultType); } /// @@ -778,7 +778,7 @@ protected override ShapedQueryExpression TranslateMin(ShapedQueryExpression sour { Check.NotNull(source, nameof(source)); - return TranslateScalarAggregate(source, selector, nameof(Enumerable.Min)); + return TranslateScalarAggregate(source, selector, nameof(Enumerable.Min), resultType); } /// @@ -1095,7 +1095,7 @@ protected override ShapedQueryExpression TranslateSum(ShapedQueryExpression sour Check.NotNull(source, nameof(source)); Check.NotNull(resultType, nameof(resultType)); - return TranslateScalarAggregate(source, selector, nameof(Enumerable.Sum)); + return TranslateScalarAggregate(source, selector, nameof(Enumerable.Sum), resultType); } /// @@ -1435,7 +1435,7 @@ ProjectionBindingExpression projectionBindingExpression } private ShapedQueryExpression TranslateScalarAggregate( - ShapedQueryExpression source, LambdaExpression selector, string methodName) + ShapedQueryExpression source, LambdaExpression selector, string methodName, Type returnType) { var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression; @@ -1459,7 +1459,7 @@ private ShapedQueryExpression TranslateScalarAggregate( inMemoryQueryExpression.UpdateServerQueryExpression( Expression.Call(method, inMemoryQueryExpression.ServerQueryExpression, selector)); - return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection()); + return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), returnType)); MethodInfo GetMethod() => methodName switch diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs index 677f64cbe07..fce4c902944 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -69,6 +70,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp _materializationContextBindings[ (ParameterExpression)((MethodCallExpression)methodCallExpression.Arguments[0]).Object]; + Check.DebugAssert(property != null || methodCallExpression.Type.IsNullableType(), "Must read nullable value without property"); + return Expression.Call( methodCallExpression.Method, valueBuffer, @@ -88,11 +91,12 @@ protected override Expression VisitExtension(Expression extensionExpression) var queryExpression = (InMemoryQueryExpression)projectionBindingExpression.QueryExpression; var projectionIndex = (int)GetProjectionIndex(queryExpression, projectionBindingExpression); var valueBuffer = queryExpression.CurrentParameter; + var property = InferPropertyFromInner(queryExpression.Projection[projectionIndex]); + + Check.DebugAssert(property != null || projectionBindingExpression.Type.IsNullableType() + || projectionBindingExpression.Type == typeof(ValueBuffer), "Must read nullable value without property"); - return valueBuffer.CreateValueBufferReadValueExpression( - projectionBindingExpression.Type, - projectionIndex, - InferPropertyFromInner(queryExpression.Projection[projectionIndex])); + return valueBuffer.CreateValueBufferReadValueExpression(projectionBindingExpression.Type, projectionIndex, property); } return base.VisitExtension(extensionExpression); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 162407c7534..9d669ea6e33 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -943,6 +943,12 @@ public static string DefaultValueSqlUnspecified([CanBeNull] object column, [CanB GetString("DefaultValueSqlUnspecified", nameof(column), nameof(table)), column, table); + /// + /// Sequence contains no elements. + /// + public static string SequenceContainsNoElements + => GetString("SequenceContainsNoElements"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 7c822104ecd..4b7e25267ce 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -688,4 +688,7 @@ The column '{column}' on table {table} has unspecified default value SQL. Specify the SQL before using EF Core to create the database schema. + + Sequence contains no elements. + \ No newline at end of file diff --git a/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs b/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs index 13d858fcfdb..58f25daf294 100644 --- a/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs +++ b/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs @@ -49,24 +49,12 @@ public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method return _sqlExpressionFactory.Coalesce( instance, arguments.Count == 0 - ? GetDefaultConstant(method.ReturnType) + ? new SqlConstantExpression(method.ReturnType.GetDefaultValueConstant(), null) : arguments[0], instance.TypeMapping); } return null; } - - private SqlConstantExpression GetDefaultConstant(Type type) - { - return (SqlConstantExpression)_generateDefaultValueConstantMethod - .MakeGenericMethod(type).Invoke(null, Array.Empty()); - } - - private static readonly MethodInfo _generateDefaultValueConstantMethod = - typeof(GetValueOrDefaultTranslator).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); - - private static SqlConstantExpression GenerateDefaultValueConstant() - => new SqlConstantExpression(Expression.Constant(default(TDefault)), null); } } diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 789eab9c1d8..7659ac1847e 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -24,6 +24,10 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// public class RelationalProjectionBindingExpressionVisitor : ExpressionVisitor { + private static readonly MethodInfo _getParameterValueMethodInfo + = typeof(RelationalProjectionBindingExpressionVisitor) + .GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue)); + private readonly RelationalQueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor; private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; @@ -83,6 +87,8 @@ public virtual Expression Translate([NotNull] SelectExpression selectExpression, _projectionMembers.Clear(); _projectionMapping.Clear(); + result = MatchTypes(result, expression.Type); + return result; } @@ -200,6 +206,10 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) } if (!(subquery.ShaperExpression is ProjectionBindingExpression + || (subquery.ShaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert + && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type + && unaryExpression.Operand is ProjectionBindingExpression) || IsAggregateResultWithCustomShaper(methodCallExpression.Method))) { return _selectExpression.AddSingleProjection(subquery); @@ -215,7 +225,7 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) return translation == null ? base.Visit(expression) : new ProjectionBindingExpression( - _selectExpression, _selectExpression.AddToProjection(translation), expression.Type); + _selectExpression, _selectExpression.AddToProjection(translation), expression.Type.MakeNullable()); } else { @@ -227,21 +237,46 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) _projectionMapping[_projectionMembers.Peek()] = translation; - return new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), expression.Type); + return new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), expression.Type.MakeNullable()); } } return base.Visit(expression); } - private static readonly MethodInfo _getParameterValueMethodInfo - = typeof(RelationalProjectionBindingExpressionVisitor) - .GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue)); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitBinary(BinaryExpression binaryExpression) + { + var left = MatchTypes(Visit(binaryExpression.Left), binaryExpression.Left.Type); + var right = MatchTypes(Visit(binaryExpression.Right), binaryExpression.Right.Type); -#pragma warning disable IDE0052 // Remove unread private members - private static T GetParameterValue(QueryContext queryContext, string parameterName) -#pragma warning restore IDE0052 // Remove unread private members - => (T)queryContext.ParameterValues[parameterName]; + return binaryExpression.Update(left, VisitAndConvert(binaryExpression.Conversion, "VisitBinary"), right); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitConditional(ConditionalExpression conditionalExpression) + { + var test = Visit(conditionalExpression.Test); + var ifTrue = Visit(conditionalExpression.IfTrue); + var ifFalse = Visit(conditionalExpression.IfFalse); + + if (test.Type == typeof(bool?)) + { + test = Expression.Equal(test, Expression.Constant(true, typeof(bool?))); + } + + return conditionalExpression.Update(test, ifTrue, ifFalse); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -255,7 +290,6 @@ protected override Expression VisitExtension(Expression extensionExpression) switch (extensionExpression) { - case EntityShaperExpression entityShaperExpression: { // TODO: Make this easier to understand some day. @@ -318,43 +352,69 @@ protected override Expression VisitExtension(Expression extensionExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override Expression VisitNew(NewExpression newExpression) + protected override ElementInit VisitElementInit(ElementInit elementInit) + => elementInit.Update(elementInit.Arguments.Select(e => MatchTypes(Visit(e), e.Type))); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitMember(MemberExpression memberExpression) { - Check.NotNull(newExpression, nameof(newExpression)); + var expression = Visit(memberExpression.Expression); + Expression updatedMemberExpression = memberExpression.Update( + expression != null ? MatchTypes(expression, memberExpression.Expression.Type) : expression); - if (newExpression.Arguments.Count == 0) + if (expression?.Type.IsNullableValueType() == true) { - return newExpression; + var nullableReturnType = memberExpression.Type.MakeNullable(); + if (!memberExpression.Type.IsNullableType()) + { + updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType); + } + + updatedMemberExpression = Expression.Condition( + Expression.Equal(expression, Expression.Default(expression.Type)), + Expression.Constant(null, nullableReturnType), + updatedMemberExpression); } - if (!_clientEval - && newExpression.Members == null) + return updatedMemberExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + { + var expression = memberAssignment.Expression; + Expression visitedExpression; + if (_clientEval) { - return null; + visitedExpression = Visit(memberAssignment.Expression); } - - var newArguments = new Expression[newExpression.Arguments.Count]; - for (var i = 0; i < newArguments.Length; i++) + else { - if (_clientEval) + var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member); + _projectionMembers.Push(projectionMember); + + visitedExpression = Visit(memberAssignment.Expression); + if (visitedExpression == null) { - newArguments[i] = Visit(newExpression.Arguments[i]); + return null; } - else - { - var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]); - _projectionMembers.Push(projectionMember); - newArguments[i] = Visit(newExpression.Arguments[i]); - if (newArguments[i] == null) - { - return null; - } - _projectionMembers.Pop(); - } + _projectionMembers.Pop(); } - return newExpression.Update(newArguments); + visitedExpression = MatchTypes(visitedExpression, expression.Type); + + return memberAssignment.Update(visitedExpression); } /// @@ -398,24 +458,111 @@ protected override Expression VisitMemberInit(MemberInitExpression memberInitExp /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { - if (_clientEval) + var @object = Visit(methodCallExpression.Object); + var arguments = new Expression[methodCallExpression.Arguments.Count]; + for (var i = 0; i < methodCallExpression.Arguments.Count; i++) + { + var argument = methodCallExpression.Arguments[i]; + arguments[i] = MatchTypes(Visit(argument), argument.Type); + } + + Expression updatedMethodCallExpression = methodCallExpression.Update( + @object != null ? MatchTypes(@object, methodCallExpression.Object.Type) : @object, + arguments); + + if (@object?.Type.IsNullableType() == true + && !methodCallExpression.Object.Type.IsNullableType()) { - return memberAssignment.Update(Visit(memberAssignment.Expression)); + var nullableReturnType = methodCallExpression.Type.MakeNullable(); + if (!methodCallExpression.Type.IsNullableType()) + { + updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType); + } + + return Expression.Condition( + Expression.Equal(@object, Expression.Default(@object.Type)), + Expression.Constant(null, nullableReturnType), + updatedMethodCallExpression); } - var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member); - _projectionMembers.Push(projectionMember); + return updatedMethodCallExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitNew(NewExpression newExpression) + { + Check.NotNull(newExpression, nameof(newExpression)); + + if (newExpression.Arguments.Count == 0) + { + return newExpression; + } - var visitedExpression = Visit(memberAssignment.Expression); - if (visitedExpression == null) + if (!_clientEval + && newExpression.Members == null) { return null; } - _projectionMembers.Pop(); - return memberAssignment.Update(visitedExpression); + var newArguments = new Expression[newExpression.Arguments.Count]; + for (var i = 0; i < newArguments.Length; i++) + { + var argument = newExpression.Arguments[i]; + Expression visitedArgument; + if (_clientEval) + { + visitedArgument = Visit(argument); + } + else + { + var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]); + _projectionMembers.Push(projectionMember); + visitedArgument = Visit(argument); + if (visitedArgument == null) + { + return null; + } + + _projectionMembers.Pop(); + } + + newArguments[i] = MatchTypes(visitedArgument, argument.Type); + } + + return newExpression.Update(newArguments); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitNewArray(NewArrayExpression newArrayExpression) + => newArrayExpression.Update(newArrayExpression.Expressions.Select(e => MatchTypes(Visit(e), e.Type))); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitUnary(UnaryExpression unaryExpression) + { + var operand = Visit(unaryExpression.Operand); + + return (unaryExpression.NodeType == ExpressionType.Convert + || unaryExpression.NodeType == ExpressionType.ConvertChecked) + && unaryExpression.Type == operand.Type + ? operand + : unaryExpression.Update(MatchTypes(operand, unaryExpression.Operand.Type)); } // TODO: Debugging @@ -426,5 +573,26 @@ private void VerifySelectExpression(ProjectionBindingExpression projectionBindin throw new InvalidOperationException(CoreStrings.QueryFailed(projectionBindingExpression.Print(), GetType().Name)); } } + + private static Expression MatchTypes(Expression expression, Type targetType) + { + if (targetType != expression.Type + && targetType.TryGetElementType(typeof(IQueryable<>)) == null) + { + if (targetType.MakeNullable() != expression.Type) + { + throw new InvalidFilterCriteriaException(); + } + + expression = Expression.Convert(expression, targetType); + } + + return expression; + } + +#pragma warning disable IDE0052 // Remove unread private members + private static T GetParameterValue(QueryContext queryContext, string parameterName) +#pragma warning restore IDE0052 // Remove unread private members + => (T)queryContext.ParameterValues[parameterName]; } } diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs index 5f47d60c19e..2b244e2838d 100644 --- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs @@ -80,7 +80,9 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType for (var i = 0; i < concreteEntityTypes.Length; i++) { body = Condition( - valueBufferParameter.CreateValueBufferReadValueExpression(typeof(bool), i, property: null), + Equal( + valueBufferParameter.CreateValueBufferReadValueExpression(typeof(bool?), i, property: null), + Constant(true, typeof(bool?))), Constant(concreteEntityTypes[i], typeof(IEntityType)), body); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 5a64572dd11..c7c437979f7 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -198,8 +198,12 @@ protected override ShapedQueryExpression TranslateAll(ShapedQueryExpression sour } translation = _sqlExpressionFactory.Exists(selectExpression, true); - return source.Update(_sqlExpressionFactory.Select(translation), - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool))); + + return source.Update( + _sqlExpressionFactory.Select(translation), + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)), + typeof(bool))); } /// @@ -223,8 +227,12 @@ protected override ShapedQueryExpression TranslateAny(ShapedQueryExpression sour } var translation = _sqlExpressionFactory.Exists(selectExpression, false); - return source.Update(_sqlExpressionFactory.Select(translation), - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool))); + + return source.Update( + _sqlExpressionFactory.Select(translation), + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)), + typeof(bool))); } /// @@ -298,8 +306,12 @@ protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression selectExpression.ApplyProjection(); translation = _sqlExpressionFactory.In(translation, selectExpression, false); - return source.Update(_sqlExpressionFactory.Select(translation), - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool))); + + return source.Update( + _sqlExpressionFactory.Select(translation), + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)), + typeof(bool))); } /// @@ -331,7 +343,11 @@ protected override ShapedQueryExpression TranslateCount(ShapedQueryExpression so selectExpression.ClearOrdering(); selectExpression.ReplaceProjectionMapping(projectionMapping); - return source.UpdateShaperExpression(new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int))); + + return source.UpdateShaperExpression( + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), + typeof(int))); } /// @@ -683,7 +699,11 @@ protected override ShapedQueryExpression TranslateLongCount(ShapedQueryExpressio selectExpression.ClearOrdering(); selectExpression.ReplaceProjectionMapping(projectionMapping); - return source.UpdateShaperExpression(new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long))); + + return source.UpdateShaperExpression( + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long?)), + typeof(long))); } /// @@ -1553,9 +1573,9 @@ private ShapedQueryExpression AggregateResultShaper( } else { - // Sum case. Projection is always non-null. We read non-nullable value (0 if empty) - shaper = new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), projection.Type); - // Cast to nullable type if required + // Sum case. Projection is always non-null. We read nullable value. + shaper = new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), projection.Type.MakeNullable()); + if (resultType != shaper.Type) { shaper = Expression.Convert(shaper, resultType); diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 40e97ee6a14..022ce202439 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -413,11 +413,11 @@ protected override Expression VisitExtension(Expression extensionExpression) var projection = _selectExpression.Projection[projectionIndex]; return CreateGetValueExpression( - _dataReaderParameter, - projectionIndex, - IsNullableProjection(projection), - projection.Expression.TypeMapping, - projectionBindingExpression.Type); + _dataReaderParameter, + projectionIndex, + IsNullableProjection(projection), + projection.Expression.TypeMapping, + projectionBindingExpression.Type); } case ProjectionBindingExpression projectionBindingExpression @@ -429,19 +429,20 @@ protected override Expression VisitExtension(Expression extensionExpression) return accessor; } - var valueParameter = Expression.Parameter(projectionBindingExpression.Type); - _variables.Add(valueParameter); - var projectionIndex = (int)GetProjectionIndex(projectionBindingExpression); var projection = _selectExpression.Projection[projectionIndex]; + var nullable = IsNullableProjection(projection); + + var valueParameter = Expression.Parameter(projectionBindingExpression.Type); + _variables.Add(valueParameter); _expressions.Add(Expression.Assign(valueParameter, CreateGetValueExpression( _dataReaderParameter, projectionIndex, - IsNullableProjection(projection), + nullable, projection.Expression.TypeMapping, - projectionBindingExpression.Type))); + valueParameter.Type))); if (_containsCollectionMaterialization) { @@ -828,10 +829,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp : _materializationContextBindings[mappingParameter][property]; var projection = _selectExpression.Projection[projectionIndex]; + var nullable = IsNullableProjection(projection); + + Check.DebugAssert(!nullable || property != null || methodCallExpression.Type.IsNullableType(), + "For nullable reads the return type must be null unless property is specified."); + return CreateGetValueExpression( _dataReaderParameter, projectionIndex, - IsNullableProjection(projection), + nullable, projection.Expression.TypeMapping, methodCallExpression.Type, property); @@ -907,6 +913,8 @@ private Expression CreateGetValueExpression( Type clrType, IPropertyBase property = null) { + Check.DebugAssert(property != null || clrType.IsNullableType(), "Must read nullable value from database if property is not specified."); + var getMethod = typeMapping.GetDataReaderMethod(); Expression indexExpression = Expression.Constant(index); @@ -923,35 +931,31 @@ Expression valueExpression getMethod, indexExpression); - if (_readerColumns != null) + if (_readerColumns != null + && _readerColumns[index] == null) { - var columnType = valueExpression.Type; + var bufferedReaderLambdaExpression = valueExpression; + var columnType = bufferedReaderLambdaExpression.Type; if (!columnType.IsValueType || !BufferedDataReader.IsSupportedValueType(columnType)) { columnType = typeof(object); - valueExpression = Expression.Convert(valueExpression, typeof(object)); + bufferedReaderLambdaExpression = Expression.Convert(bufferedReaderLambdaExpression, columnType); } - if (_readerColumns[index] == null) - { - _readerColumns[index] = ReaderColumn.Create( - columnType, - nullable, - _indexMapParameter != null ? ((ColumnExpression)_selectExpression.Projection[index].Expression).Name : null, - Expression.Lambda( - valueExpression, - dbDataReader, - _indexMapParameter ?? Expression.Parameter(typeof(int[]))).Compile()); - } + _readerColumns[index] = ReaderColumn.Create( + columnType, + nullable, + _indexMapParameter != null ? ((ColumnExpression)_selectExpression.Projection[index].Expression).Name : null, + Expression.Lambda( + bufferedReaderLambdaExpression, + dbDataReader, + _indexMapParameter ?? Expression.Parameter(typeof(int[]))).Compile()); if (getMethod.DeclaringType != typeof(DbDataReader)) { - valueExpression - = Expression.Call( - dbDataReader, - RelationalTypeMapping.GetDataReaderMethod(columnType), - indexExpression); + valueExpression = Expression.Call( + dbDataReader, RelationalTypeMapping.GetDataReaderMethod(columnType), indexExpression); } } @@ -977,35 +981,28 @@ Expression valueExpression valueExpression = Expression.Convert(valueExpression, clrType); } - var exceptionParameter - = Expression.Parameter(typeof(Exception), name: "e"); + if (nullable) + { + valueExpression = Expression.Condition( + Expression.Call(dbDataReader, _isDbNullMethod, indexExpression), + Expression.Default(valueExpression.Type), + valueExpression); + } if (_detailedErrorsEnabled) { - var catchBlock - = Expression - .Catch( - exceptionParameter, - Expression.Call( - _throwReadValueExceptionMethod - .MakeGenericMethod(valueExpression.Type), - exceptionParameter, - Expression.Call( - dbDataReader, - _getFieldValueMethod.MakeGenericMethod(typeof(object)), - indexExpression), - Expression.Constant(property, typeof(IPropertyBase)))); + var exceptionParameter = Expression.Parameter(typeof(Exception), name: "e"); - valueExpression = Expression.TryCatch(valueExpression, catchBlock); - } + var catchBlock = Expression.Catch( + exceptionParameter, + Expression.Call( + _throwReadValueExceptionMethod.MakeGenericMethod(valueExpression.Type), + exceptionParameter, + Expression.Call(dbDataReader, _getFieldValueMethod.MakeGenericMethod(typeof(object)), indexExpression), + Expression.Constant(valueExpression.Type.MakeNullable(nullable), typeof(Type)), + Expression.Constant(property, typeof(IPropertyBase)))); - if (nullable) - { - valueExpression - = Expression.Condition( - Expression.Call(dbDataReader, _isDbNullMethod, indexExpression), - Expression.Default(valueExpression.Type), - valueExpression); + valueExpression = Expression.TryCatch(valueExpression, catchBlock); } return valueExpression; @@ -1013,9 +1010,8 @@ var catchBlock [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TValue ThrowReadValueException( - Exception exception, object value, IPropertyBase property = null) + Exception exception, object value, Type expectedType, IPropertyBase property = null) { - var expectedType = typeof(TValue); var actualType = value?.GetType(); string message; diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 3799d0c3f9f..6ccbaeb2e5f 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -552,6 +552,10 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) } if (!(subqueryTranslation.ShaperExpression is ProjectionBindingExpression + || (subqueryTranslation.ShaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert + && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type + && unaryExpression.Operand is ProjectionBindingExpression) || IsAggregateResultWithCustomShaper(methodCallExpression.Method))) { return null; @@ -573,7 +577,17 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) return subquery.Projection[0].Expression; } - return new ScalarSubqueryExpression(subquery); + SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery); + + if (subqueryTranslation.ResultCardinality == ResultCardinality.SingleOrDefault + && !subqueryTranslation.ShaperExpression.Type.IsNullableType()) + { + scalarSubqueryExpression = _sqlExpressionFactory.Coalesce( + scalarSubqueryExpression, + (SqlExpression)Visit(subqueryTranslation.ShaperExpression.Type.GetDefaultValueConstant())); + } + + return scalarSubqueryExpression; } SqlExpression sqlObject = null; diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index e8066a6e0e3..32136c75215 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1199,23 +1199,32 @@ public Expression AddSingleProjection([NotNull] ShapedQueryExpression shapedQuer if (!(innerExpression is EntityShaperExpression)) { var sentinelExpression = innerSelectExpression.Limit; + var sentinelNullableType = sentinelExpression.Type.MakeNullable(); ProjectionBindingExpression dummyProjection; if (innerSelectExpression.Projection.Any()) { var index = innerSelectExpression.AddToProjection(sentinelExpression); dummyProjection = new ProjectionBindingExpression( - innerSelectExpression, index, sentinelExpression.Type); + innerSelectExpression, index, sentinelNullableType); } else { innerSelectExpression._projectionMapping[new ProjectionMember()] = sentinelExpression; dummyProjection = new ProjectionBindingExpression( - innerSelectExpression, new ProjectionMember(), sentinelExpression.Type); + innerSelectExpression, new ProjectionMember(), sentinelNullableType); } + var defaultResult = shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault + ? (Expression)Default(shaperExpression.Type) + : Block( + Throw(New( + typeof(InvalidOperationException).GetConstructors().Single(ci => ci.GetParameters().Count() == 1), + Constant(RelationalStrings.SequenceContainsNoElements))), + Default(shaperExpression.Type)); + shaperExpression = Condition( - Equal(dummyProjection, Default(dummyProjection.Type)), - Default(shaperExpression.Type), + Equal(dummyProjection, Default(sentinelNullableType)), + defaultResult, shaperExpression); } @@ -2716,6 +2725,19 @@ private bool Equals(SelectExpression selectExpression) return false; } + if (_projection.Count != selectExpression._projection.Count) + { + return false; + } + + for (var i = 0; i < _projection.Count; i++) + { + if (!_projection[i].Equals(selectExpression._projection[i])) + { + return false; + } + } + if (_projectionMapping.Count != selectExpression._projectionMapping.Count) { return false; @@ -2889,6 +2911,11 @@ public override int GetHashCode() var hash = new HashCode(); hash.Add(base.GetHashCode()); + foreach (var projection in _projection) + { + hash.Add(projection); + } + foreach (var projectionMapping in _projectionMapping) { hash.Add(projectionMapping.Key); diff --git a/src/EFCore/Infrastructure/ExpressionExtensions.cs b/src/EFCore/Infrastructure/ExpressionExtensions.cs index 888613fe5cf..82c41f8c910 100644 --- a/src/EFCore/Infrastructure/ExpressionExtensions.cs +++ b/src/EFCore/Infrastructure/ExpressionExtensions.cs @@ -319,7 +319,7 @@ private static TValue ValueBufferTryReadValue( #pragma warning disable IDE0060 // Remove unused parameter in ValueBuffer valueBuffer, int index, IPropertyBase property) #pragma warning restore IDE0060 // Remove unused parameter - => valueBuffer[index] is TValue value ? value : default; + => (TValue)valueBuffer[index]; /// /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 8cb7aa1c5e8..f05d68fe555 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -709,7 +709,15 @@ private NavigationExpansionExpression ProcessDefaultIfEmpty(NavigationExpansionE QueryableMethods.DefaultIfEmptyWithoutArgument.MakeGenericMethod(source.SourceElementType), source.Source)); - _entityReferenceOptionalMarkingExpressionVisitor.Visit(source.PendingSelector); + var pendingSelector = source.PendingSelector; + _entityReferenceOptionalMarkingExpressionVisitor.Visit(pendingSelector); + if (!pendingSelector.Type.IsNullableType()) + { + pendingSelector = Expression.Coalesce( + Expression.Convert(pendingSelector, pendingSelector.Type.MakeNullable()), pendingSelector.Type.GetDefaultValueConstant()); + } + + source.ApplySelector(pendingSelector); return source; } diff --git a/src/Shared/ExpressionExtensions.cs b/src/Shared/ExpressionExtensions.cs index 27b6e2295c7..842ae62e5fd 100644 --- a/src/Shared/ExpressionExtensions.cs +++ b/src/Shared/ExpressionExtensions.cs @@ -10,12 +10,6 @@ namespace System.Linq.Expressions [DebuggerStepThrough] internal static class ExpressionExtensions { - /// - /// 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 static bool IsNullConstantExpression([NotNull] this Expression expression) => RemoveConvert(expression) is ConstantExpression constantExpression && constantExpression.Value == null; diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index a01d0a8ae73..533ff2afad9 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; @@ -466,12 +467,6 @@ private static void ProcessGenericType(StringBuilder builder, Type type, Type[] builder.Append('>'); } - /// - /// 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 static IEnumerable GetNamespaces([NotNull] this Type type) { if (_builtInTypeNames.ContainsKey(type)) @@ -492,5 +487,14 @@ public static IEnumerable GetNamespaces([NotNull] this Type type) } } } + + public static ConstantExpression GetDefaultValueConstant(this Type type) + => (ConstantExpression)_generateDefaultValueConstantMethod + .MakeGenericMethod(type).Invoke(null, Array.Empty()); + + private static readonly MethodInfo _generateDefaultValueConstantMethod = + typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); + + private static ConstantExpression GenerateDefaultValueConstant() => Expression.Constant(default(TDefault)); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs index f685f4ac7de..7a1371feb4b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs @@ -80,6 +80,12 @@ FROM root c WHERE ((c[""Discriminator""] = ""BuiltInDataTypes"") AND (c[""Id""] = 13))"); } + [ConditionalFact(Skip = "Issue#21678")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs index cb0b78b1f46..70ca847e7db 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs @@ -146,6 +146,18 @@ FROM root c WHERE (c[""Discriminator""] IN (""Blog"", ""RssBlog"") AND NOT((c[""IndexerVisible""] = ""Aye"")))"); } + [ConditionalFact(Skip = "Issue#27678")] + public override void Optional_owned_with_converter_reading_non_nullable_column() + { + base.Optional_owned_with_converter_reading_non_nullable_column(); + } + + [ConditionalFact(Skip = "Issue#21678")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 57fdd89e9a0..12c4ae26e4f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4151,6 +4151,18 @@ public override Task Select_distinct_Select_with_client_bindings(bool async) return base.Select_distinct_Select_with_client_bindings(async); } + [ConditionalTheory(Skip = "Issue#21678")] + public override Task Non_nullable_property_through_optional_navigation(bool async) + { + return base.Non_nullable_property_through_optional_navigation(async); + } + + [ConditionalTheory(Skip = "Issue#21678")] + public override Task Max_on_empty_sequence_throws(bool async) + { + return base.Max_on_empty_sequence_throws(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index 73f076245cf..77d93f0df61 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -693,9 +693,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { b.OwnsOne( e => e.Throned, b => b.HasData( - new { BartonId = 1, Property = "Property" })); + new { BartonId = 1, Property = "Property", Value = 42 })); b.HasData( - new Barton { Id = 1, Simple = "Simple" }); + new Barton { Id = 1, Simple = "Simple" }, + new Barton { Id = 2, Simple = "Not" }); }); modelBuilder.Entity().HasData( diff --git a/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs index 4f3405c8fb0..20968ab30e3 100644 --- a/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore @@ -14,6 +15,12 @@ public BuiltInDataTypesInMemoryTest(BuiltInDataTypesInMemoryFixture fixture) { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + public class BuiltInDataTypesInMemoryFixture : BuiltInDataTypesFixtureBase { protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; diff --git a/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs index dced737313b..549be923c08 100644 --- a/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; namespace Microsoft.EntityFrameworkCore { @@ -14,6 +15,12 @@ public ConvertToProviderTypesInMemoryTest(ConvertToProviderTypesInMemoryFixture { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + public class ConvertToProviderTypesInMemoryFixture : ConvertToProviderTypesFixtureBase { protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; diff --git a/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs index e3768b48814..ed278a74517 100644 --- a/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs @@ -14,6 +14,12 @@ public CustomConvertersInMemoryTest(CustomConvertersInMemoryFixture fixture) { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + // Disabled: In-memory database is case-sensitive public override void Can_insert_and_read_back_with_case_insensitive_string_key() { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs index eca74b677fe..06ee1121790 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs @@ -44,5 +44,11 @@ public override Task Select_subquery_single_nested_subquery2(bool async) { return base.Select_subquery_single_nested_subquery2(async); } + + [ConditionalTheory(Skip = "issue #17539")] + public override Task Union_over_entities_with_different_nullability(bool async) + { + return base.Union_over_entities_with_different_nullability(async); + } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs index 5bf97e7336f..6c99a4389db 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs @@ -42,6 +42,12 @@ public override Task Where_nav_prop_reference_optional1_via_DefaultIfEmpty(bool return base.Where_nav_prop_reference_optional1_via_DefaultIfEmpty(async); } + [ConditionalTheory(Skip = "Issue#17539")] + public override Task Where_nav_prop_reference_optional2_via_DefaultIfEmpty(bool async) + { + return base.Where_nav_prop_reference_optional2_via_DefaultIfEmpty(async); + } + [ConditionalTheory(Skip = "Issue#17539")] public override Task Optional_navigation_propagates_nullability_to_manually_created_left_join2(bool async) { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs index 6ffb0a6463e..f9ef1b06484 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -24,17 +25,32 @@ public NorthwindAggregateOperatorsQueryInMemoryTest( // InMemory can throw server side exception public override void Average_no_data_subquery() { - Assert.Throws(() => base.Average_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList()).Message); } public override void Max_no_data_subquery() { - Assert.Throws(() => base.Max_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList()).Message); } public override void Min_no_data_subquery() { - Assert.Throws(() => base.Min_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList()).Message); } public override Task Collection_Last_member_access_in_projection_translated(bool async) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs index ced0ba9b9a1..fa2cc302a5c 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; using Xunit.Abstractions; @@ -105,5 +107,17 @@ public override Task Using_string_Equals_with_StringComparison_throws_informativ [ConditionalTheory(Skip = "issue #17386")] public override Task Using_static_string_Equals_with_StringComparison_throws_informative_error(bool async) => base.Using_static_string_Equals_with_StringComparison_throws_informative_error(async); + + public override async Task Max_on_empty_sequence_throws(bool async) + { + using var context = CreateContext(); + var query = context.Set().Select(e => new { Max = e.Orders.Max(o => o.OrderID) }); + + var message = async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message; + + Assert.Equal("Sequence contains no elements", message); + } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index ceb992edfb0..f668c4edd77 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -885,8 +885,8 @@ public virtual Task Select_IndexOf(bool async) { return AssertQueryScalar( async, - ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.IndexOf("oo")), - ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.MaybeScalar(x => x.IndexOf("oo")) ?? 0), + ss => ss.Set().OrderBy(e => e.Id).Select(e => (int?)e.NullableStringA.IndexOf("oo")), + ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.MaybeScalar(x => x.IndexOf("oo"))), assertOrder: true); } diff --git a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs index ed6b31c8271..f7d284e2427 100644 --- a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs +++ b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs @@ -2078,6 +2078,22 @@ public virtual void Object_to_string_conversion() Assert.Equal(expected.Char, actual.Char); } + [ConditionalFact] + public virtual void Optional_datetime_reading_null_from_database() + { + using var context = CreateContext(); + var expected = context.Set().ToList() + .Select(e => new { DT = e.DateTimeOffset == null ? (DateTime?)null : e.DateTimeOffset.Value.DateTime.Date }).ToList(); + + var actual = context.Set() + .Select(e => new { DT = e.DateTimeOffset == null ? (DateTime?)null : e.DateTimeOffset.Value.DateTime.Date }).ToList(); + + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].DT, actual[i].DT); + } + } + public abstract class BuiltInDataTypesFixtureBase : SharedStoreFixtureBase { protected override string StoreName { get; } = "BuiltInDataTypes"; @@ -2334,6 +2350,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con AnimalId = 1, Method = IdentificationMethod.EarTag }); + + modelBuilder.Entity() + .HasData( + new DateTimeEnclosure + { + Id = 1, + DateTimeOffset = new DateTimeOffset(2020, 3, 12, 1, 1, 1, new TimeSpan(3, 0, 0)) + }, + new DateTimeEnclosure + { + Id = 2 + }); } protected static void MakeRequired(ModelBuilder modelBuilder) @@ -3133,5 +3161,11 @@ protected enum IdentificationMethod EarTag, Rfid } + + protected class DateTimeEnclosure + { + public int Id { get; set; } + public DateTimeOffset? DateTimeOffset { get; set; } + } } } diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index c14a52e05a8..a6281c93e1d 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -667,6 +667,26 @@ protected enum Roles public override void Object_to_string_conversion() {} + [ConditionalFact] + public virtual void Optional_owned_with_converter_reading_non_nullable_column() + { + using var context = CreateContext(); + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Set().Select(e => new { e.OwnedWithConverter.Value }).ToList()).Message); + } + + protected class Parent + { + public int Id { get; set; } + public OwnedWithConverter OwnedWithConverter { get; set; } + } + + protected class OwnedWithConverter + { + public int Value { get; set; } + } public abstract class CustomConvertersFixtureBase : BuiltInDataTypesFixtureBase { protected override string StoreName { get; } = "CustomConverters"; @@ -1129,6 +1149,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con Roles = new List { Roles.Seller } }); }); + + modelBuilder.Entity( + b => + { + b.OwnsOne( + e => e.OwnedWithConverter, + ob => + { + ob.Property(i => i.Value).HasConversion(); + ob.HasData(new { ParentId = 1, Value = 42 }); + }); + + b.HasData( + new Parent { Id = 1 }, + new Parent { Id = 2 }); + }); } private static class StringToDictionarySerializer diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 5263946bc5b..deb16e8e76f 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -4709,14 +4709,14 @@ public virtual Task Union_over_entities_with_different_nullability(bool async) .Concat(ss.Set().GroupJoin(ss.Set(), l2 => l2.Level1_Optional_Id, l1 => l1.Id, (l2, l1s) => new { l2, l1s }) .SelectMany(g => g.l1s.DefaultIfEmpty(), (g, l1) => new { l1, g.l2 }) .Where(e => e.l1.Equals(null))) - .Select(e => e.l1.Id), + .Select(e => (int?)e.l1.Id), ss => ss.Set() .GroupJoin(ss.Set(), l1 => l1.Id, l2 => l2.Level1_Optional_Id, (l1, l2s) => new { l1, l2s }) .SelectMany(g => g.l2s.DefaultIfEmpty(), (g, l2) => new { g.l1, l2 }) .Concat(ss.Set().GroupJoin(ss.Set(), l2 => l2.Level1_Optional_Id, l1 => l1.Id, (l2, l1s) => new { l2, l1s }) .SelectMany(g => g.l1s.DefaultIfEmpty(), (g, l1) => new { l1, g.l2 }) .Where(e => e.l1 == null)) - .Select(e => e.l1.MaybeScalar(x => x.Id) ?? 0)); + .Select(e => e.l1.MaybeScalar(x => x.Id))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index d80bb52c69b..18a1b68ac71 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -5430,26 +5430,6 @@ public virtual Task Select_subquery_boolean_empty_with_pushdown(bool async) ss => ss.Set().Select(g => (bool?)null)); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - return AssertQueryScalar( - async, - ss => ss.Set().Select(g => g.Weapons.Where(w => w.Name == "BFG").OrderBy(w => w.Id).FirstOrDefault().IsAutomatic), - ss => ss.Set().Select(g => false)); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - return AssertQueryScalar( - async, - ss => ss.Set().Select(g => g.Weapons.Where(w => w.Name == "BFG").OrderBy(w => w.Id).FirstOrDefault().Id), - ss => ss.Set().Select(g => 0)); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_subquery_distinct_singleordefault_boolean1(bool async) @@ -6801,13 +6781,13 @@ public virtual Task GroupBy_with_boolean_groupin_key_thru_navigation_access(bool return AssertQuery( async, ss => ss.Set() - .GroupBy(t => new { t.Gear.HasSoulPatch, t.Gear.Squad.Name }) + .GroupBy(t => new { HasSoulPatch = (bool?)t.Gear.HasSoulPatch, t.Gear.Squad.Name }) .Select(g => new { g.Key.HasSoulPatch, Name = g.Key.Name.ToLower() }), ss => ss.Set() .GroupBy( t => new { - HasSoulPatch = t.Gear.MaybeScalar(x => x.HasSoulPatch) ?? false, + HasSoulPatch = t.Gear.MaybeScalar(x => x.HasSoulPatch), Name = t.Gear.Squad.Name }) .Select(g => new { g.Key.HasSoulPatch, Name = g.Key.Name.Maybe(x => x.ToLower()) }), diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs index 4526697c810..cad533b3712 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs @@ -392,8 +392,11 @@ public virtual void Min_no_data_cast_to_nullable() public virtual void Min_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList()).Message); } [ConditionalFact] @@ -421,8 +424,11 @@ public virtual void Max_no_data_cast_to_nullable() public virtual void Max_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList()).Message); } [ConditionalFact] @@ -450,8 +456,11 @@ public virtual void Average_no_data_cast_to_nullable() public virtual void Average_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList()).Message); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 822f19f9710..c5b2ac18efe 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -15,6 +15,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.Utilities; using Xunit; +using Xunit.Sdk; #pragma warning disable RCS1202 // Avoid NullReferenceException. @@ -6021,7 +6022,7 @@ public virtual Task ToList_over_string(bool async) async, ss => ss.Set().OrderBy(c => c.CustomerID).Select(e => new { Property = e.City.ToList() }), assertOrder: true, - elementAsserter: (e,a) => Assert.True(e.Property.SequenceEqual(a.Property))); + elementAsserter: (e, a) => Assert.True(e.Property.SequenceEqual(a.Property))); } [ConditionalTheory] @@ -6047,5 +6048,31 @@ public virtual Task AsEnumerable_over_string(bool async) } private static int ClientMethod(int s) => s; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Non_nullable_property_through_optional_navigation(bool async) + { + Assert.Equal( + "Nullable object must have a value.", + (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Region.Length })))).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Max_on_empty_sequence_throws(bool async) + { + using var context = CreateContext(); + var query = context.Set().Select(e => new { Max = e.Orders.Max(o => o.OrderID) }); + + var message = async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message; + + Assert.Equal("Nullable object must have a value.", message); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs index 41f7ee23f2b..a957bb1ce26 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs @@ -946,10 +946,10 @@ public virtual Task Project_single_element_from_collection_with_OrderBy_Distinct async, ss => ss.Set() .Select(c => c.Orders.OrderBy(o => o.OrderID).Select(o => o.CustomerID).Distinct().FirstOrDefault()) - .Select(e => e.Length), + .Select(e => (int?)e.Length), ss => ss.Set() .Select(c => c.Orders.OrderBy(o => o.OrderID).Select(o => o.CustomerID).Distinct().FirstOrDefault()) - .Select(e => e == null ? 0 : e.Length)); + .Select(e => e.MaybeScalar(e => e.Length))); } [ConditionalTheory] @@ -1449,8 +1449,9 @@ public virtual Task Project_non_nullable_value_after_FirstOrDefault_on_empty_col return AssertQueryScalar( async, ss => ss.Set().Select( - c => ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().Length), - ss => ss.Set().Select(c => 0)); + c => (int?)ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().Length), + ss => ss.Set().Select( + c => (int?)ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().MaybeScalar(e => e.Length))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index 632133a1abc..33d21bba0df 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -810,6 +810,19 @@ public virtual Task GroupBy_with_multiple_aggregates_on_owned_navigation_propert })); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + + public virtual async Task Non_nullable_property_through_optional_navigation(bool async) + { + Assert.Equal( + "Nullable object must have a value.", + (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Throned.Value })))).Message); + } + protected virtual DbContext CreateContext() => Fixture.CreateContext(); public abstract class OwnedQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase @@ -1078,6 +1091,7 @@ public IReadOnlyDictionary GetEntityAsserters() Assert.Equal(e == null, a == null); if (a != null) { + Assert.Equal(((Throned)e).Value, ((Throned)a).Value); Assert.Equal(((Throned)e).Property, ((Throned)a).Property); } } @@ -1291,9 +1305,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { b.OwnsOne( e => e.Throned, b => b.HasData( - new { BartonId = 1, Property = "Property" })); + new { BartonId = 1, Property = "Property", Value = 42 })); b.HasData( - new Barton { Id = 1, Simple = "Simple" }); + new Barton { Id = 1, Simple = "Simple" }, + new Barton { Id = 2, Simple = "Not" }); }); modelBuilder.Entity().HasData( @@ -1509,7 +1524,12 @@ private static IReadOnlyList CreateBartons() { Id = 1, Simple = "Simple", - Throned = new Throned { Property = "Property" } + Throned = new Throned { Property = "Property", Value = 42 } + }, + new Barton + { + Id = 2, + Simple = "Not", } }; @@ -1744,6 +1764,7 @@ protected class Fink protected class Throned { + public int Value { get; set; } public string Property { get; set; } } } diff --git a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs index 5749dabd135..bc8b31e7cfd 100644 --- a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs @@ -340,19 +340,6 @@ public virtual Task Disjoint_with_cast_to_nullable(bool async) elementSorter: x => x.Id); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Disjoint_without_cast_to_nullable(bool async) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(1, 1)); - - return AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Disjoint = e.Polygon.Disjoint(point) }), - ss => ss.Set().Select(e => new { e.Id, Disjoint = (e.Polygon == null ? false : e.Polygon.Disjoint(point)) }), - elementSorter: x => x.Id); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Disjoint_with_null_check(bool async) @@ -365,28 +352,6 @@ public virtual Task Disjoint_with_null_check(bool async) elementSorter: x => x.Id); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Distance_without_null_check(bool async) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); - - return AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Distance = e.Point.Distance(point) }), - ss => ss.Set().Select(e => new { e.Id, Distance = e.Point == null ? default : e.Point.Distance(point) }), - elementSorter: e => e.Id, - elementAsserter: (e, a) => - { - Assert.Equal(e.Id, a.Id); - - if (AssertDistances) - { - Assert.Equal(e.Distance, a.Distance); - } - }); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Distance_with_null_check(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs index 4ec43cb9be2..f6a70026ec2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs @@ -2593,6 +2593,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable bigint] [Precision = 19 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] MappedDataTypes.BoolAsBit ---> [bit] diff --git a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs index 78d856b2df1..3908752dfc5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs @@ -143,6 +143,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] MaxLengthDataTypes.ByteArray5 ---> [nullable nvarchar] [MaxLength = 8] diff --git a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs index 5dfc070f089..e9c8c18cde2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs @@ -154,6 +154,8 @@ public virtual void Columns_have_expected_data_types() CollectionEnum.Roles ---> [nullable nvarchar] [MaxLength = -1] CollectionScalar.Id ---> [int] [Precision = 10 Scale = 0] CollectionScalar.Tags ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] EntityWithValueWrapper.Id ---> [int] [Precision = 10 Scale = 0] @@ -169,6 +171,8 @@ public virtual void Columns_have_expected_data_types() NonNullableDependent.PrincipalId ---> [int] [Precision = 10 Scale = 0] NullablePrincipal.Id ---> [int] [Precision = 10 Scale = 0] Order.Id ---> [nvarchar] [MaxLength = 450] +Parent.Id ---> [int] [Precision = 10 Scale = 0] +Parent.OwnedWithConverter_Value ---> [nullable nvarchar] [MaxLength = 64] Person.Id ---> [int] [Precision = 10 Scale = 0] Person.Name ---> [nullable nvarchar] [MaxLength = -1] Person.SSN ---> [nullable int] [Precision = 10 Scale = 0] diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs index 8a68e987538..b7017df1f37 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs @@ -147,6 +147,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable varbinary] [MaxLength = 4] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable varbinary] [MaxLength = 8] BuiltInNullableDataTypesShadow.TestString ---> [nullable varbinary] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable varbinary] [MaxLength = 12] +DateTimeEnclosure.Id ---> [varbinary] [MaxLength = 4] EmailTemplate.Id ---> [varbinary] [MaxLength = 16] EmailTemplate.TemplateType ---> [varbinary] [MaxLength = 4] MaxLengthDataTypes.ByteArray5 ---> [nullable varbinary] [MaxLength = 5] diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs index 487bfe6bca6..e3ad946a16b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs @@ -148,6 +148,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable nvarchar] [MaxLength = 64] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable nvarchar] [MaxLength = 64] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable nvarchar] [MaxLength = 48] +DateTimeEnclosure.Id ---> [nvarchar] [MaxLength = 64] EmailTemplate.Id ---> [nvarchar] [MaxLength = 36] EmailTemplate.TemplateType ---> [nvarchar] [MaxLength = -1] MaxLengthDataTypes.ByteArray5 ---> [nullable nvarchar] [MaxLength = 8] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs b/test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs similarity index 98% rename from test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs rename to test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs index b76b7956113..23c028ca4fc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs @@ -9,8 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore.Query +namespace Microsoft.EntityFrameworkCore { public class NavigationTest : IClassFixture { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index cb3361de315..0801f4292a4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -2176,10 +2176,10 @@ public override async Task Select_join_with_key_selector_being_a_subquery(bool a AssertSql( @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] FROM [LevelOne] AS [l] -INNER JOIN [LevelTwo] AS [l0] ON [l].[Id] = ( +INNER JOIN [LevelTwo] AS [l0] ON [l].[Id] = COALESCE(( SELECT TOP(1) [l1].[Id] FROM [LevelTwo] AS [l1] - ORDER BY [l1].[Id])"); + ORDER BY [l1].[Id]), 0)"); } public override async Task Contains_with_subquery_optional_navigation_and_constant_item(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index e2217dd8b97..432db54bd79 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -529,29 +529,23 @@ public override async Task Where_enum_has_flag_subquery(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE (([g].[Rank] & ( +WHERE ([g].[Rank] & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL", + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)", // @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ((2 & ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( +WHERE (2 & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL"); + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)"); } public override async Task Where_enum_has_flag_subquery_with_pushdown(bool async) @@ -1263,11 +1257,11 @@ public override async Task Where_subquery_boolean(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ( +WHERE COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) = CAST(1 AS bit)"); + ORDER BY [w].[Id]), CAST(0 AS bit)) = CAST(1 AS bit)"); } public override async Task Where_subquery_boolean_with_pushdown(bool async) @@ -1291,14 +1285,14 @@ public override async Task Where_subquery_distinct_firstordefault_boolean(bool a AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_firstordefault_boolean_with_pushdown(bool async) @@ -1343,13 +1337,13 @@ public override async Task Where_subquery_distinct_singleordefault_boolean1(bool AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) = CAST(1 AS bit)) + ) AS [t]), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1360,10 +1354,10 @@ public override async Task Where_subquery_distinct_singleordefault_boolean2(bool AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) = CAST(1 AS bit)) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1427,14 +1421,14 @@ public override async Task Where_subquery_distinct_orderby_firstordefault_boolea AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_orderby_firstordefault_boolean_with_pushdown(bool async) @@ -4704,10 +4698,10 @@ public override async Task Project_one_value_type_from_empty_collection(bool asy await base.Project_one_value_type_from_empty_collection(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) [g].[SquadId] FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [SquadId] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [SquadId] FROM [Squads] AS [s] WHERE [s].[Name] = N'Kilo'"); } @@ -4763,10 +4757,10 @@ public override async Task Select_subquery_projecting_single_constant_int(bool a await base.Select_subquery_projecting_single_constant_int(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) 42 FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [Gear] FROM [Squads] AS [s]"); } @@ -4787,10 +4781,10 @@ public override async Task Select_subquery_projecting_single_constant_bool(bool await base.Select_subquery_projecting_single_constant_bool(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) CAST(1 AS bit) FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), CAST(0 AS bit)) AS [Gear] FROM [Squads] AS [s]"); } @@ -4928,11 +4922,11 @@ public override async Task Include_collection_with_complex_OrderBy3(bool async) FROM [Gears] AS [g] LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname]) AND ([g].[SquadId] = [g0].[LeaderSquadId]) WHERE [g].[Discriminator] = N'Officer' -ORDER BY ( +ORDER BY COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]"); + ORDER BY [w].[Id]), CAST(0 AS bit)), [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]"); } public override async Task Correlated_collection_with_complex_OrderBy(bool async) @@ -4970,10 +4964,10 @@ FROM [Gears] AS [g0] ORDER BY ( SELECT COUNT(*) FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = ( + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = COALESCE(( SELECT TOP(1) [g1].[HasSoulPatch] FROM [Gears] AS [g1] - WHERE [g1].[Nickname] = N'Marcus'))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + WHERE [g1].[Nickname] = N'Marcus'), CAST(0 AS bit)))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Cast_to_derived_type_after_OfType_works(bool async) @@ -4991,11 +4985,11 @@ public override async Task Select_subquery_boolean(bool async) await base.Select_subquery_boolean(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g]"); } @@ -5030,11 +5024,11 @@ public override async Task Select_subquery_int_with_outside_cast_and_coalesce(bo await base.Select_subquery_int_with_outside_cast_and_coalesce(async); AssertSql( - @"SELECT COALESCE(( + @"SELECT COALESCE(COALESCE(( SELECT TOP(1) [w].[Id] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), 42) + ORDER BY [w].[Id]), 0), 42) FROM [Gears] AS [g]"); } @@ -5073,11 +5067,11 @@ public override async Task Select_subquery_boolean_empty(bool async) await base.Select_subquery_boolean_empty(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g]"); } @@ -5094,44 +5088,18 @@ FROM [Weapons] AS [w] FROM [Gears] AS [g]"); } - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[IsAutomatic] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g]"); - } - - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[Id] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g]"); - } - public override async Task Select_subquery_distinct_singleordefault_boolean1(bool async) { await base.Select_subquery_distinct_singleordefault_boolean1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5141,10 +5109,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean2(boo await base.Select_subquery_distinct_singleordefault_boolean2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5170,13 +5138,13 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5186,10 +5154,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index ebc9f621a0d..a0031f14a35 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -441,7 +441,7 @@ public override async Task Default_if_empty_top_level_projection(bool async) await base.Default_if_empty_top_level_projection(async); AssertSql( - @"SELECT [t].[EmployeeID] + @"SELECT COALESCE([t].[EmployeeID], 0) FROM ( SELECT NULL AS [empty] ) AS [empty] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs index 03fbc7054d3..5bcb32ff181 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs @@ -896,11 +896,11 @@ public override async Task Project_single_scalar_value_subquery_in_query_with_op AssertSql( @"@__p_0='3' -SELECT [t].[OrderID], ( +SELECT [t].[OrderID], COALESCE(( SELECT TOP(1) [o].[OrderID] FROM [Order Details] AS [o] WHERE [t].[OrderID] = [o].[OrderID] - ORDER BY [o].[OrderID], [o].[ProductID]) AS [OrderDetail], [c].[City] + ORDER BY [o].[OrderID], [o].[ProductID]), 0) AS [OrderDetail], [c].[City] FROM ( SELECT TOP(@__p_0) [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] FROM [Orders] AS [o0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 52c397f74db..24525477e43 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -371,13 +371,13 @@ public override void Select_nested_collection_multi_level4() base.Select_nested_collection_multi_level4(); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) ( SELECT COUNT(*) FROM [Order Details] AS [o] WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] > 10)) FROM [Orders] AS [o0] - WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -387,8 +387,8 @@ public override void Select_nested_collection_multi_level5() base.Select_nested_collection_multi_level5(); AssertSql( - @"SELECT ( - SELECT TOP(1) ( + @"SELECT COALESCE(( + SELECT TOP(1) COALESCE(( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] WHERE ([o1].[OrderID] = [o].[OrderID]) AND (([o].[OrderID] <> ( @@ -397,9 +397,9 @@ FROM [Orders] AS [o0] WHERE [c].[CustomerID] = [o0].[CustomerID])) OR ( SELECT COUNT(*) FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) IS NULL)) + WHERE [c].[CustomerID] = [o0].[CustomerID]) IS NULL)), 0) FROM [Orders] AS [o1] - WHERE ([c].[CustomerID] = [o1].[CustomerID]) AND ([o1].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o1].[CustomerID]) AND ([o1].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -409,13 +409,13 @@ public override void Select_nested_collection_multi_level6() base.Select_nested_collection_multi_level6(); AssertSql( - @"SELECT ( - SELECT TOP(1) ( + @"SELECT COALESCE(( + SELECT TOP(1) COALESCE(( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] - WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] <> CAST(LEN([c].[CustomerID]) AS int))) + WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] <> CAST(LEN([c].[CustomerID]) AS int))), 0) FROM [Orders] AS [o0] - WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -774,7 +774,7 @@ public override async Task Project_single_element_from_collection_with_OrderBy_o await base.Project_single_element_from_collection_with_OrderBy_over_navigation_Take_and_FirstOrDefault(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[OrderID] FROM ( SELECT TOP(1) [o].[OrderID], [o].[ProductID], [p].[ProductID] AS [ProductID0], [p].[ProductName] @@ -783,7 +783,7 @@ FROM [Order Details] AS [o] WHERE [o0].[OrderID] = [o].[OrderID] ORDER BY [p].[ProductName] ) AS [t] - ORDER BY [t].[ProductName]) + ORDER BY [t].[ProductName]), 0) FROM [Orders] AS [o0] WHERE [o0].[OrderID] < 10300"); } @@ -1127,11 +1127,11 @@ public override async Task FirstOrDefault_over_empty_collection_of_value_type_re await base.FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(async); AssertSql( - @"SELECT [c].[CustomerID], ( + @"SELECT [c].[CustomerID], COALESCE(( SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID]) AS [OrderId] + ORDER BY [o].[OrderID]), 0) AS [OrderId] FROM [Customers] AS [c] WHERE [c].[CustomerID] = N'FISSA'"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index b24a79bc4b1..f8c8a4de9cf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -153,14 +153,14 @@ public override async Task Navigation_rewrite_on_owned_collection_with_compositi await base.Navigation_rewrite_on_owned_collection_with_composition(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) CASE WHEN [o].[Id] <> 42 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END FROM [Order] AS [o] WHERE [o0].[Id] = [o].[ClientId] - ORDER BY [o].[Id]) + ORDER BY [o].[Id]), CAST(0 AS bit)) FROM [OwnedPerson] AS [o0] ORDER BY [o0].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index bfab233df86..0770508dbf1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6293,14 +6293,14 @@ public virtual void Let_multiple_references_with_reference_to_outer_2() @"SELECT [a0].[Id], [a0].[ActivityTypeId], [a0].[DateTime], [a0].[Points], ( SELECT TOP(1) [c].[Id] FROM [CompetitionSeasons] AS [c] - WHERE ([c].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c].[EndDate])) AS [CompetitionSeasonId], COALESCE([a0].[Points], ( + WHERE ([c].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c].[EndDate])) AS [CompetitionSeasonId], COALESCE([a0].[Points], COALESCE(( SELECT TOP(1) [a].[Points] FROM [ActivityTypePoints12456] AS [a] INNER JOIN [CompetitionSeasons] AS [c0] ON [a].[CompetitionSeasonId] = [c0].[Id] WHERE ([a1].[Id] = [a].[ActivityTypeId]) AND ([c0].[Id] = ( SELECT TOP(1) [c1].[Id] FROM [CompetitionSeasons] AS [c1] - WHERE ([c1].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c1].[EndDate]))))) AS [Points] + WHERE ([c1].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c1].[EndDate])))), 0)) AS [Points] FROM [Activities] AS [a0] INNER JOIN [ActivityType12456] AS [a1] ON [a0].[ActivityTypeId] = [a1].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs index 75e85d501a7..cf7fc87474f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs @@ -188,17 +188,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0xE6100000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) -SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] -FROM [PolygonEntity] AS [p]"); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0xE6100000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) - SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] FROM [PolygonEntity] AS [p]"); } @@ -217,17 +206,6 @@ END AS [Disjoint] FROM [PolygonEntity] AS [p]"); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0xE6100000010C000000000000F03F0000000000000000' (Size = 22) (DbType = Object) - -SELECT [p].[Id], [p].[Point].STDistance(@__point_0) AS [Distance] -FROM [PointEntity] AS [p]"); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs index ff96b769104..61716294eab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs @@ -275,17 +275,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0x00000000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) -SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] -FROM [PolygonEntity] AS [p]"); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0x00000000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) - SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] FROM [PolygonEntity] AS [p]"); } @@ -304,17 +293,6 @@ END AS [Disjoint] FROM [PolygonEntity] AS [p]"); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0x00000000010C0000000000000000000000000000F03F' (Size = 22) (DbType = Object) - -SELECT [p].[Id], [p].[Point].STDistance(@__point_0) AS [Distance] -FROM [PointEntity] AS [p]"); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index e026b220ee5..93ec44fcd12 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -719,19 +719,15 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE (([g].[Rank] & ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( +WHERE ([g].[Rank] & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL", + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)", // @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN CAST(1 AS bit) @@ -739,19 +735,15 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ((2 & ( +WHERE (2 & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL"); + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)"); } public override async Task Where_enum_has_flag_subquery_with_pushdown(bool async) @@ -1670,11 +1662,11 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ( +WHERE COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) = CAST(1 AS bit)"); + ORDER BY [w].[Id]), CAST(0 AS bit)) = CAST(1 AS bit)"); } public override async Task Where_subquery_boolean_with_pushdown(bool async) @@ -1706,14 +1698,14 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_firstordefault_boolean_with_pushdown(bool async) @@ -1770,13 +1762,13 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) = CAST(1 AS bit)) + ) AS [t]), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1791,10 +1783,10 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) = CAST(1 AS bit)) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1874,14 +1866,14 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_orderby_firstordefault_boolean_with_pushdown(bool async) @@ -6087,11 +6079,11 @@ public override async Task Project_one_value_type_from_empty_collection(bool asy await base.Project_one_value_type_from_empty_collection(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) [g].[SquadId] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [SquadId] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [SquadId] FROM [Squads] AS [s] WHERE [s].[Name] = N'Kilo'"); } @@ -6149,11 +6141,11 @@ public override async Task Select_subquery_projecting_single_constant_int(bool a await base.Select_subquery_projecting_single_constant_int(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) 42 FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [Gear] FROM [Squads] AS [s]"); } @@ -6175,11 +6167,11 @@ public override async Task Select_subquery_projecting_single_constant_bool(bool await base.Select_subquery_projecting_single_constant_bool(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) CAST(1 AS bit) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), CAST(0 AS bit)) AS [Gear] FROM [Squads] AS [s]"); } @@ -6353,11 +6345,11 @@ FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) ) AS [t] ON ([g].[Nickname] = [t].[LeaderNickname]) AND ([g].[SquadId] = [t].[LeaderSquadId]) WHERE [o].[Nickname] IS NOT NULL -ORDER BY ( +ORDER BY COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + ORDER BY [w].[Id]), CAST(0 AS bit)), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Correlated_collection_with_complex_OrderBy(bool async) @@ -6405,11 +6397,11 @@ WHERE [o].[Nickname] IS NOT NULL ORDER BY ( SELECT COUNT(*) FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = ( + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = COALESCE(( SELECT TOP(1) [g1].[HasSoulPatch] FROM [Gears] AS [g1] LEFT JOIN [Officers] AS [o1] ON ([g1].[Nickname] = [o1].[Nickname]) AND ([g1].[SquadId] = [o1].[SquadId]) - WHERE [g1].[Nickname] = N'Marcus'))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + WHERE [g1].[Nickname] = N'Marcus'), CAST(0 AS bit)))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Cast_to_derived_type_after_OfType_works(bool async) @@ -6428,11 +6420,11 @@ public override async Task Select_subquery_boolean(bool async) await base.Select_subquery_boolean(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6470,11 +6462,11 @@ public override async Task Select_subquery_int_with_outside_cast_and_coalesce(bo await base.Select_subquery_int_with_outside_cast_and_coalesce(async); AssertSql( - @"SELECT COALESCE(( + @"SELECT COALESCE(COALESCE(( SELECT TOP(1) [w].[Id] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), 42) + ORDER BY [w].[Id]), 0), 42) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6516,11 +6508,11 @@ public override async Task Select_subquery_boolean_empty(bool async) await base.Select_subquery_boolean_empty(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6539,46 +6531,18 @@ FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[IsAutomatic] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g] -LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); - } - - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[Id] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g] -LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); - } - public override async Task Select_subquery_distinct_singleordefault_boolean1(bool async) { await base.Select_subquery_distinct_singleordefault_boolean1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6589,10 +6553,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean2(boo await base.Select_subquery_distinct_singleordefault_boolean2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6620,13 +6584,13 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6637,10 +6601,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs index 769e2d04487..88400ee7310 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs @@ -256,19 +256,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0x000100000000000000000000F03F000000000000F03F000000000000F03F0000...' (Size = 60) (DbType = String) -SELECT ""p"".""Id"", CASE - WHEN ""p"".""Polygon"" IS NOT NULL THEN Disjoint(""p"".""Polygon"", @__point_0) -END AS ""Disjoint"" -FROM ""PolygonEntity"" AS ""p"""); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0x000100000000000000000000F03F000000000000F03F000000000000F03F0000...' (Size = 60) (DbType = String) - SELECT ""p"".""Id"", CASE WHEN ""p"".""Polygon"" IS NOT NULL THEN Disjoint(""p"".""Polygon"", @__point_0) END AS ""Disjoint"" @@ -289,17 +276,6 @@ END AS ""Disjoint"" FROM ""PolygonEntity"" AS ""p"""); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0x0001000000000000000000000000000000000000F03F00000000000000000000...' (Size = 60) (DbType = String) - -SELECT ""p"".""Id"", Distance(""p"".""Point"", @__point_0) AS ""Distance"" -FROM ""PointEntity"" AS ""p"""); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); From d2b9f32e634848966cb1e13020716d8d74acf1b6 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 18 Jul 2020 14:09:54 -0700 Subject: [PATCH 4/4] Add API to allow configuration of the association entity type. (#21672) * Add API to allow configuration of the association entity type. Co-authored-by: lajones --- .../Builders/CollectionCollectionBuilder.cs | 19 +++ .../Builders/CollectionCollectionBuilder`.cs | 19 +++ .../Migrations/ModelSnapshotSqlServerTest.cs | 132 ++++++++++++++++++ 3 files changed, 170 insertions(+) diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs index bb23ed53441..18ab761bc0b 100644 --- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs +++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs @@ -83,6 +83,25 @@ public CollectionCollectionBuilder( [EntityFrameworkInternal] protected virtual InternalModelBuilder ModelBuilder => LeftEntityType.AsEntityType().Model.Builder; + /// + /// Configures the association entity type implementing the many-to-many relationship. + /// + /// The configuration of the association type. + /// The builder for the originating entity type so that multiple configuration calls can be chained. + public virtual EntityTypeBuilder UsingEntity( + [NotNull] Action configureAssociation) + { + Check.DebugAssert(LeftNavigation.AssociationEntityType != null, "LeftNavigation.AssociationEntityType is null"); + Check.DebugAssert(RightNavigation.AssociationEntityType != null, "RightNavigation.AssociationEntityType is null"); + Check.DebugAssert(LeftNavigation.AssociationEntityType == RightNavigation.AssociationEntityType, + "LeftNavigation.AssociationEntityType != RightNavigation.AssociationEntityType"); + + var associationEntityTypeBuilder = new EntityTypeBuilder(LeftNavigation.AssociationEntityType); + configureAssociation(associationEntityTypeBuilder); + + return new EntityTypeBuilder(RightEntityType); + } + /// /// Configures the relationships to the entity types participating in the many-to-many relationship. /// diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs index ed657f0c53b..f4c71151464 100644 --- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs +++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs @@ -41,6 +41,25 @@ public CollectionCollectionBuilder( { } + /// + /// Configures the association entity type implementing the many-to-many relationship. + /// + /// The configuration of the association type. + /// The builder for the originating entity type so that multiple configuration calls can be chained. + public new virtual EntityTypeBuilder UsingEntity( + [NotNull] Action configureAssociation) + { + Check.DebugAssert(LeftNavigation.AssociationEntityType != null, "LeftNavigation.AssociationEntityType is null"); + Check.DebugAssert(RightNavigation.AssociationEntityType != null, "RightNavigation.AssociationEntityType is null"); + Check.DebugAssert(LeftNavigation.AssociationEntityType == RightNavigation.AssociationEntityType, + "LeftNavigation.AssociationEntityType != RightNavigation.AssociationEntityType"); + + var associationEntityTypeBuilder = new EntityTypeBuilder(LeftNavigation.AssociationEntityType); + configureAssociation(associationEntityTypeBuilder); + + return new EntityTypeBuilder(RightEntityType); + } + /// /// Configures the relationships to the entity types participating in the many-to-many relationship. /// diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index bc7c55ce24a..9595aef8b4c 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -1238,6 +1238,138 @@ public virtual void Many_to_many_join_table_stored_in_snapshot() }); } + + [ConditionalFact] + public virtual void Can_override_table_name_for_many_to_many_join_table_stored_in_snapshot() + { + Test( + builder => + { + var manyToMany = builder + .Entity() + .HasMany(l => l.Rights) + .WithMany(r => r.Lefts) + .UsingEntity(a => a.ToTable("MyJoinTable")); + }, + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""ManyToManyLeftManyToManyRight"", b => + { + b.Property(""ManyToManyLeft_Id"") + .HasColumnType(""int""); + + b.Property(""ManyToManyRight_Id"") + .HasColumnType(""int""); + + b.HasKey(""ManyToManyLeft_Id"", ""ManyToManyRight_Id""); + + b.HasIndex(""ManyToManyRight_Id""); + + b.ToTable(""MyJoinTable""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""Name"") + .HasColumnType(""nvarchar(max)""); + + b.HasKey(""Id""); + + b.ToTable(""ManyToManyLeft""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""Description"") + .HasColumnType(""nvarchar(max)""); + + b.HasKey(""Id""); + + b.ToTable(""ManyToManyRight""); + }); + + modelBuilder.Entity(""ManyToManyLeftManyToManyRight"", b => + { + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft"", null) + .WithMany() + .HasForeignKey(""ManyToManyLeft_Id"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight"", null) + .WithMany() + .HasForeignKey(""ManyToManyRight_Id"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + });", usingSystem: true), + model => + { + var associationEntity = model.FindEntityType("ManyToManyLeftManyToManyRight"); + Assert.NotNull(associationEntity); + Assert.Equal("MyJoinTable", associationEntity.GetTableName()); + Assert.Collection(associationEntity.GetDeclaredProperties(), + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + Assert.True(p.IsShadowProperty()); + }, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + Assert.True(p.IsShadowProperty()); + }); + Assert.Collection(associationEntity.FindDeclaredPrimaryKey().Properties, + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + }, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + }); + Assert.Collection(associationEntity.GetDeclaredForeignKeys(), + fk => + { + Assert.Equal("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft", fk.PrincipalEntityType.Name); + Assert.Collection(fk.PrincipalKey.Properties, + p => + { + Assert.Equal("Id", p.Name); + }); + Assert.Collection(fk.Properties, + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + }); + }, + fk => + { + Assert.Equal("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight", fk.PrincipalEntityType.Name); + Assert.Collection(fk.PrincipalKey.Properties, + p => + { + Assert.Equal("Id", p.Name); + }); + Assert.Collection(fk.Properties, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + }); + }); + }); + } + [ConditionalFact] public virtual void TableName_preserved_when_generic() {