From b8ad5cc289bcd501d079f64223849672fc071222 Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Wed, 10 Oct 2018 18:14:21 -0700 Subject: [PATCH] Cosmos: Add API to configure the container that a hierarchy is mapped to, as well as the default container name Enable materializing derived types polymorphically Add exceptions for some unsupported cases --- .../EFCore.Abstractions.csproj | 1 - ...mosCollectionOwnershipBuilderExtensions.cs | 53 +++ .../CosmosEntityTypeBuilderExtensions.cs | 52 +++ .../CosmosModelBuilderExtensions.cs | 34 ++ ...smosReferenceOwnershipBuilderExtensions.cs | 53 +++ src/EFCore.Cosmos/EFCore.Cosmos.csproj | 26 ++ .../Extensions/CosmosMetadataExtensions.cs | 16 + .../CosmosServiceCollectionExtensions.cs | 3 +- .../Infrastructure/CosmosModelCustomizer.cs | 47 ++ ...xtension.cs => CosmosDbOptionExtension.cs} | 0 .../Internal/CosmosConventionSetBuilder.cs | 4 +- .../Internal/StoreKeyConvention.cs | 51 ++- .../Metadata/CosmosEntityTypeAnnotations.cs | 10 +- .../Metadata/CosmosModelAnnotations.cs | 37 ++ .../Metadata/ICosmosModelAnnotations.cs | 10 + .../Internal/CosmosAnnotationsBuilder.cs | 65 +++ .../CosmosEntityTypeBuilderAnnotations.cs | 65 +++ ...CosmosInternalMetadataBuilderExtensions.cs | 33 ++ .../Internal/CosmosModelBuilderAnnotations.cs | 45 ++ .../Metadata/Internal/EntityTypeExtensions.cs | 5 +- .../Properties/CosmosStrings.Designer.cs | 62 +++ .../Properties/CosmosStrings.Designer.tt | 5 + .../Properties/CosmosStrings.resx | 132 ++++++ .../CosmosEagerLoadingExpressionVisitor.cs | 28 -- ...mosEagerLoadingExpressionVisitorFactory.cs | 24 - .../CosmosEntityQueryableExpressionVisitor.cs | 7 + ...EntityQueryableExpressionVisitorFactory.cs | 6 +- .../Internal/RootReferenceExpression.cs | 5 +- .../Query/Internal/CosmosQueryModelVisitor.cs | 2 - .../Query/Internal/EntityShaper.cs | 145 +++++- .../Internal/ValueBufferFactoryFactory.cs | 5 +- .../Storage/Internal/CosmosClient.cs | 4 +- .../Storage/Internal/CosmosDatabase.cs | 89 +++- .../Update/Internal/DocumentSource.cs | 24 +- .../TableSplittingTestBase.cs | 2 +- .../Internal/CompositeShaper.cs | 9 +- .../Internal/MaterializerFactory.cs | 5 +- .../TransportationModel/FuelTank.cs | 3 +- .../TransportationContext.cs | 17 +- src/EFCore/Infrastructure/ModelValidator.cs | 11 +- src/EFCore/Metadata/Internal/EntityType.cs | 27 +- .../Metadata/Internal/EntityTypeExtensions.cs | 14 + .../Internal/InternalEntityTypeBuilder.cs | 21 +- .../Metadata/Internal/InternalModelBuilder.cs | 8 +- .../Internal/InternalRelationshipBuilder.cs | 3 +- src/EFCore/Query/EntityQueryModelVisitor.cs | 3 +- .../Query/Internal/ExpressionPrinter.cs | 1 - ...ionQueryModelRewritingExpressionVisitor.cs | 22 +- src/EFCore/Storage/MaterializationContext.cs | 5 +- .../CosmosEndToEndTest.cs | 1 + .../NestedDocumentsTest.cs | 423 ++++++++++++++++++ .../SimpleQueryCosmosSqlTest.Functions.cs | 6 +- .../TestUtilities/CosmosTestStore.cs | 3 +- .../Metadata/CosmosBuilderExtensionsTest.cs | 45 ++ .../Metadata/CosmosMetadataExtensionsTest.cs | 16 +- 55 files changed, 1609 insertions(+), 184 deletions(-) create mode 100644 src/EFCore.Cosmos/CosmosCollectionOwnershipBuilderExtensions.cs create mode 100644 src/EFCore.Cosmos/CosmosEntityTypeBuilderExtensions.cs create mode 100644 src/EFCore.Cosmos/CosmosModelBuilderExtensions.cs create mode 100644 src/EFCore.Cosmos/CosmosReferenceOwnershipBuilderExtensions.cs create mode 100644 src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs rename src/EFCore.Cosmos/Infrastructure/Internal/{CosmosSqlDbOptionExtension.cs => CosmosDbOptionExtension.cs} (100%) create mode 100644 src/EFCore.Cosmos/Metadata/CosmosModelAnnotations.cs create mode 100644 src/EFCore.Cosmos/Metadata/ICosmosModelAnnotations.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationsBuilder.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeBuilderAnnotations.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/CosmosInternalMetadataBuilderExtensions.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/CosmosModelBuilderAnnotations.cs create mode 100644 src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs create mode 100644 src/EFCore.Cosmos/Properties/CosmosStrings.Designer.tt create mode 100644 src/EFCore.Cosmos/Properties/CosmosStrings.resx delete mode 100644 src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitor.cs delete mode 100644 src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitorFactory.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/NestedDocumentsTest.cs create mode 100644 test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs diff --git a/src/EFCore.Abstractions/EFCore.Abstractions.csproj b/src/EFCore.Abstractions/EFCore.Abstractions.csproj index 3b4b28fad6b..991c9218cc2 100644 --- a/src/EFCore.Abstractions/EFCore.Abstractions.csproj +++ b/src/EFCore.Abstractions/EFCore.Abstractions.csproj @@ -23,7 +23,6 @@ TextTemplatingFileGenerator - Microsoft.EntityFrameworkCore.Internal AbstractionsStrings.Designer.cs diff --git a/src/EFCore.Cosmos/CosmosCollectionOwnershipBuilderExtensions.cs b/src/EFCore.Cosmos/CosmosCollectionOwnershipBuilderExtensions.cs new file mode 100644 index 00000000000..cde0c3844b0 --- /dev/null +++ b/src/EFCore.Cosmos/CosmosCollectionOwnershipBuilderExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + /// + /// Cosmos specific extension methods for . + /// + public static class CosmosCollectionOwnershipBuilderExtensions + { + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static CollectionOwnershipBuilder ToContainer( + [NotNull] this CollectionOwnershipBuilder referenceOwnershipBuilder, + [CanBeNull] string name) + { + Check.NotNull(referenceOwnershipBuilder, nameof(referenceOwnershipBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + referenceOwnershipBuilder.GetInfrastructure() + .Cosmos(ConfigurationSource.Explicit) + .ToContainer(name); + + return referenceOwnershipBuilder; + } + + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The entity type being configured. + /// The entity type that this relationship targets. + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static CollectionOwnershipBuilder ToContainer( + [NotNull] this CollectionOwnershipBuilder referenceOwnershipBuilder, + [CanBeNull] string name) + where TEntity : class + where TDependentEntity : class + => (CollectionOwnershipBuilder)ToContainer((CollectionOwnershipBuilder)referenceOwnershipBuilder, name); + } +} diff --git a/src/EFCore.Cosmos/CosmosEntityTypeBuilderExtensions.cs b/src/EFCore.Cosmos/CosmosEntityTypeBuilderExtensions.cs new file mode 100644 index 00000000000..9ab7607750d --- /dev/null +++ b/src/EFCore.Cosmos/CosmosEntityTypeBuilderExtensions.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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Cosmos specific extension methods for . + /// + public static class CosmosEntityTypeBuilderExtensions + { + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToContainer( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + entityTypeBuilder.GetInfrastructure() + .Cosmos(ConfigurationSource.Explicit) + .ToContainer(name); + + return entityTypeBuilder; + } + + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The entity type being configured. + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ToContainer( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name) + where TEntity : class + => (EntityTypeBuilder)ToContainer((EntityTypeBuilder)entityTypeBuilder, name); + } +} diff --git a/src/EFCore.Cosmos/CosmosModelBuilderExtensions.cs b/src/EFCore.Cosmos/CosmosModelBuilderExtensions.cs new file mode 100644 index 00000000000..cda22460840 --- /dev/null +++ b/src/EFCore.Cosmos/CosmosModelBuilderExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + public static class CosmosModelBuilderExtensions + { + /// + /// Configures the default container name that will be used if no name + /// is explicitly configured for an entity type. + /// + /// The model builder. + /// The default container name. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder HasDefaultContainerName( + [NotNull] this ModelBuilder modelBuilder, + [CanBeNull] string name) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + modelBuilder.GetInfrastructure().Cosmos(ConfigurationSource.Explicit).HasDefaultContainerName(name); + + return modelBuilder; + } + } +} diff --git a/src/EFCore.Cosmos/CosmosReferenceOwnershipBuilderExtensions.cs b/src/EFCore.Cosmos/CosmosReferenceOwnershipBuilderExtensions.cs new file mode 100644 index 00000000000..ed3064123cb --- /dev/null +++ b/src/EFCore.Cosmos/CosmosReferenceOwnershipBuilderExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + /// + /// Cosmos specific extension methods for . + /// + public static class CosmosReferenceOwnershipBuilderExtensions + { + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static ReferenceOwnershipBuilder ToContainer( + [NotNull] this ReferenceOwnershipBuilder referenceOwnershipBuilder, + [CanBeNull] string name) + { + Check.NotNull(referenceOwnershipBuilder, nameof(referenceOwnershipBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + referenceOwnershipBuilder.GetInfrastructure() + .Cosmos(ConfigurationSource.Explicit) + .ToContainer(name); + + return referenceOwnershipBuilder; + } + + /// + /// Configures the container that the entity maps to when targeting Azure Cosmos. + /// + /// The entity type being configured. + /// The entity type that this relationship targets. + /// The builder for the entity type being configured. + /// The name of the container. + /// The same builder instance so that multiple calls can be chained. + public static ReferenceOwnershipBuilder ToContainer( + [NotNull] this ReferenceOwnershipBuilder referenceOwnershipBuilder, + [CanBeNull] string name) + where TEntity : class + where TRelatedEntity : class + => (ReferenceOwnershipBuilder)ToContainer((ReferenceOwnershipBuilder)referenceOwnershipBuilder, name); + } +} diff --git a/src/EFCore.Cosmos/EFCore.Cosmos.csproj b/src/EFCore.Cosmos/EFCore.Cosmos.csproj index 9ebd289733d..e8f1ad7381f 100644 --- a/src/EFCore.Cosmos/EFCore.Cosmos.csproj +++ b/src/EFCore.Cosmos/EFCore.Cosmos.csproj @@ -26,4 +26,30 @@ + + + CosmosStrings.Designer.tt + True + True + + + + + + + TextTemplatingFileGenerator + CosmosStrings.Designer.cs + + + + + + + + + + Microsoft.EntityFrameworkCore.Cosmos.Internal + + + diff --git a/src/EFCore.Cosmos/Extensions/CosmosMetadataExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosMetadataExtensions.cs index 5a0671fd251..dd03336d44f 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosMetadataExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosMetadataExtensions.cs @@ -11,6 +11,22 @@ namespace Microsoft.EntityFrameworkCore /// public static class CosmosMetadataExtensions { + /// + /// Gets the Cosmos-specific metadata for a model. + /// + /// The model to get metadata for. + /// The Cosmos-specific metadata for the model. + public static ICosmosModelAnnotations Cosmos(this IModel model) + => new CosmosModelAnnotations(model); + + /// + /// Gets the Cosmos-specific metadata for a model. + /// + /// The model to get metadata for. + /// The Cosmos-specific metadata for the model. + public static CosmosModelAnnotations Cosmos(this IMutableModel model) + => (CosmosModelAnnotations)Cosmos((IModel)model); + /// /// Gets the Cosmos-specific metadata for an entity type. /// diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index 6b61de339f4..47c666901da 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.ExpressionVisitors.Internal; @@ -28,6 +29,7 @@ public static IServiceCollection AddEntityFrameworkCosmos([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() @@ -35,7 +37,6 @@ public static IServiceCollection AddEntityFrameworkCosmos([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() - .TryAdd() .TryAddProviderSpecificServices( b => b .TryAddScoped() diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs b/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs new file mode 100644 index 00000000000..c3178020367 --- /dev/null +++ b/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs @@ -0,0 +1,47 @@ +// 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.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure +{ + /// + /// + /// Builds the model for a given context. This default implementation builds the model by calling + /// on the context. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public class CosmosModelCustomizer : ModelCustomizer + { + public CosmosModelCustomizer(ModelCustomizerDependencies dependencies) + : base(dependencies) + { + } + + /// + /// + /// Performs additional configuration of the model in addition to what is discovered by convention. This implementation + /// builds the model for a given context by calling + /// on the context. + /// + /// + /// + /// The builder being used to construct the model. + /// + /// + /// The context instance that the model is being created for. + /// + public override void Customize(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.GetInfrastructure().Cosmos(ConfigurationSource.Convention).HasDefaultContainerName(context.GetType().Name); + + base.Customize(modelBuilder, context); + } + } +} diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSqlDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs similarity index 100% rename from src/EFCore.Cosmos/Infrastructure/Internal/CosmosSqlDbOptionExtension.cs rename to src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs index 58730ebf527..ebc7b5a1680 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs @@ -11,15 +11,17 @@ public class CosmosConventionSetBuilder : IConventionSetBuilder public ConventionSet AddConventions(ConventionSet conventionSet) { var discriminatorConvention = new DiscriminatorConvention(); - var storeKeyConvention = new StoreKeyConvention(); conventionSet.EntityTypeAddedConventions.Add(storeKeyConvention); conventionSet.EntityTypeAddedConventions.Add(discriminatorConvention); + conventionSet.BaseEntityTypeChangedConventions.Add(storeKeyConvention); conventionSet.BaseEntityTypeChangedConventions.Add(discriminatorConvention); conventionSet.ForeignKeyOwnershipChangedConventions.Add(storeKeyConvention); + conventionSet.EntityTypeAnnotationChangedConventions.Add(storeKeyConvention); + return conventionSet; } } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/StoreKeyConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/StoreKeyConvention.cs index a7b09f91ee3..e6b16a23720 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/Internal/StoreKeyConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/Internal/StoreKeyConvention.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.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; @@ -9,7 +10,11 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal { - public class StoreKeyConvention : IEntityTypeAddedConvention, IForeignKeyOwnershipChangedConvention + public class StoreKeyConvention : + IEntityTypeAddedConvention, + IForeignKeyOwnershipChangedConvention, + IEntityTypeAnnotationChangedConvention, + IBaseTypeChangedConvention { public static readonly string IdPropertyName = "id"; public static readonly string JObjectPropertyName = "__jObject"; @@ -25,33 +30,51 @@ public InternalEntityTypeBuilder Apply(InternalEntityTypeBuilder entityTypeBuild var jObjectProperty = entityTypeBuilder.Property(JObjectPropertyName, typeof(JObject), ConfigurationSource.Convention); } - - return entityTypeBuilder; - } - - public InternalRelationshipBuilder Apply(InternalRelationshipBuilder relationshipBuilder) - { - if (relationshipBuilder.Metadata.IsOwnership) + else { - var ownedType = relationshipBuilder.Metadata.DeclaringEntityType; - var idProperty = ownedType.FindProperty(IdPropertyName); + var entityType = entityTypeBuilder.Metadata; + var idProperty = entityType.FindDeclaredProperty(IdPropertyName); if (idProperty != null) { - var key = ownedType.FindKey(idProperty); + var key = entityType.FindKey(idProperty); if (key != null) { - ownedType.Builder.RemoveKey(key, ConfigurationSource.Convention); + entityType.Builder.RemoveKey(key, ConfigurationSource.Convention); } } - var jObjectProperty = ownedType.FindProperty(JObjectPropertyName); + var jObjectProperty = entityType.FindDeclaredProperty(JObjectPropertyName); if (jObjectProperty != null) { - ownedType.Builder.RemoveShadowPropertiesIfUnused(new[] { jObjectProperty }); + entityType.Builder.RemoveShadowPropertiesIfUnused(new[] { jObjectProperty }); } } + return entityTypeBuilder; + } + + public InternalRelationshipBuilder Apply(InternalRelationshipBuilder relationshipBuilder) + { + Apply(relationshipBuilder.Metadata.DeclaringEntityType.Builder); + return relationshipBuilder; } + + public Annotation Apply(InternalEntityTypeBuilder entityTypeBuilder, string name, Annotation annotation, Annotation oldAnnotation) + { + if(name == CosmosAnnotationNames.ContainerName) + { + Apply(entityTypeBuilder); + } + + return annotation; + } + + public bool Apply(InternalEntityTypeBuilder entityTypeBuilder, EntityType oldBaseType) + { + Apply(entityTypeBuilder); + + return true; + } } } diff --git a/src/EFCore.Cosmos/Metadata/CosmosEntityTypeAnnotations.cs b/src/EFCore.Cosmos/Metadata/CosmosEntityTypeAnnotations.cs index 0f5395b53ab..a65581911ad 100644 --- a/src/EFCore.Cosmos/Metadata/CosmosEntityTypeAnnotations.cs +++ b/src/EFCore.Cosmos/Metadata/CosmosEntityTypeAnnotations.cs @@ -24,6 +24,9 @@ public CosmosEntityTypeAnnotations(IEntityType entityType) protected virtual IEntityType EntityType => (IEntityType)Annotations.Metadata; + protected virtual CosmosModelAnnotations GetAnnotations(IModel model) + => new CosmosModelAnnotations(model); + protected virtual CosmosEntityTypeAnnotations GetAnnotations([NotNull] IEntityType entityType) => new CosmosEntityTypeAnnotations(entityType); @@ -38,14 +41,13 @@ public virtual string ContainerName set => SetContainerName(value); } - private static string GetDefaultContainerName() => "Unicorn"; + private string GetDefaultContainerName() => GetAnnotations(EntityType.Model).DefaultContainerName + ?? EntityType.ShortName(); protected virtual bool SetContainerName([CanBeNull] string value) - { - return Annotations.SetAnnotation( + => Annotations.SetAnnotation( CosmosAnnotationNames.ContainerName, Check.NullButNotEmpty(value, nameof(value))); - } public virtual IProperty DiscriminatorProperty { diff --git a/src/EFCore.Cosmos/Metadata/CosmosModelAnnotations.cs b/src/EFCore.Cosmos/Metadata/CosmosModelAnnotations.cs new file mode 100644 index 00000000000..27bd1736413 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/CosmosModelAnnotations.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata +{ + public class CosmosModelAnnotations : ICosmosModelAnnotations + { + public CosmosModelAnnotations(IModel model) + : this(new CosmosAnnotations(model)) + { + } + + protected CosmosModelAnnotations(CosmosAnnotations annotations) => Annotations = annotations; + + protected virtual CosmosAnnotations Annotations { get; } + + protected virtual IModel Model => (IModel)Annotations.Metadata; + + public virtual string DefaultContainerName + { + get => (string)Annotations.Metadata[CosmosAnnotationNames.ContainerName]; + + [param: CanBeNull] + set => SetDefaultContainerName(value); + } + + protected virtual bool SetDefaultContainerName([CanBeNull] string value) + => Annotations.SetAnnotation( + CosmosAnnotationNames.ContainerName, + Check.NullButNotEmpty(value, nameof(value))); + } +} diff --git a/src/EFCore.Cosmos/Metadata/ICosmosModelAnnotations.cs b/src/EFCore.Cosmos/Metadata/ICosmosModelAnnotations.cs new file mode 100644 index 00000000000..47b79283024 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/ICosmosModelAnnotations.cs @@ -0,0 +1,10 @@ +// 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.Cosmos.Metadata +{ + public interface ICosmosModelAnnotations + { + string DefaultContainerName { get; } + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationsBuilder.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationsBuilder.cs new file mode 100644 index 00000000000..0451d927a29 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationsBuilder.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class CosmosAnnotationsBuilder : CosmosAnnotations + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CosmosAnnotationsBuilder( + [NotNull] InternalMetadataBuilder internalBuilder, + ConfigurationSource configurationSource) + : base(internalBuilder.Metadata) + { + MetadataBuilder = internalBuilder; + ConfigurationSource = configurationSource; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual ConfigurationSource ConfigurationSource { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual InternalMetadataBuilder MetadataBuilder { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool SetAnnotation( + string relationalAnnotationName, + object value) + => MetadataBuilder.HasAnnotation(relationalAnnotationName, value, ConfigurationSource); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool CanSetAnnotation( + string relationalAnnotationName, + object value) + => MetadataBuilder.CanSetAnnotation(relationalAnnotationName, value, ConfigurationSource); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public override bool RemoveAnnotation(string annotationName) + => MetadataBuilder.RemoveAnnotation(annotationName, ConfigurationSource); + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeBuilderAnnotations.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeBuilderAnnotations.cs new file mode 100644 index 00000000000..57a15dfeb3b --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeBuilderAnnotations.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal +{ + public class CosmosEntityTypeBuilderAnnotations : CosmosEntityTypeAnnotations + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CosmosEntityTypeBuilderAnnotations( + [NotNull] InternalEntityTypeBuilder internalBuilder, + ConfigurationSource configurationSource) + : base(new CosmosAnnotationsBuilder(internalBuilder, configurationSource)) + { + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected new virtual CosmosAnnotationsBuilder Annotations => (CosmosAnnotationsBuilder)base.Annotations; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual InternalEntityTypeBuilder EntityTypeBuilder => (InternalEntityTypeBuilder)Annotations.MetadataBuilder; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override CosmosModelAnnotations GetAnnotations(IModel model) + => new CosmosModelBuilderAnnotations( + ((Model)model).Builder, + Annotations.ConfigurationSource); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override CosmosEntityTypeAnnotations GetAnnotations(IEntityType entityType) + => new CosmosEntityTypeBuilderAnnotations( + ((EntityType)entityType).Builder, + Annotations.ConfigurationSource); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool ToContainer([CanBeNull] string name) + { + Check.NullButNotEmpty(name, nameof(name)); + + return SetContainerName(name); + } + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosInternalMetadataBuilderExtensions.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosInternalMetadataBuilderExtensions.cs new file mode 100644 index 00000000000..d0c80818340 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosInternalMetadataBuilderExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static class CosmosInternalMetadataBuilderExtensions + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static CosmosModelBuilderAnnotations Cosmos( + [NotNull] this InternalModelBuilder builder, + ConfigurationSource configurationSource) + => new CosmosModelBuilderAnnotations(builder, configurationSource); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static CosmosEntityTypeBuilderAnnotations Cosmos( + [NotNull] this InternalEntityTypeBuilder builder, + ConfigurationSource configurationSource) + => new CosmosEntityTypeBuilderAnnotations(builder, configurationSource); + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosModelBuilderAnnotations.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosModelBuilderAnnotations.cs new file mode 100644 index 00000000000..5c573f132ae --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosModelBuilderAnnotations.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class CosmosModelBuilderAnnotations : CosmosModelAnnotations + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public CosmosModelBuilderAnnotations( + [NotNull] InternalModelBuilder internalBuilder, + ConfigurationSource configurationSource) + : base(new CosmosAnnotationsBuilder(internalBuilder, configurationSource)) + { + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected new virtual CosmosAnnotationsBuilder Annotations => (CosmosAnnotationsBuilder)base.Annotations; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected virtual InternalModelBuilder ModelBuilder => (InternalModelBuilder)Annotations.MetadataBuilder; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool HasDefaultContainerName([CanBeNull] string name) => SetDefaultContainerName(name); + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore.Cosmos/Metadata/Internal/EntityTypeExtensions.cs index 0a3985c6962..940adbf316c 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/EntityTypeExtensions.cs @@ -8,6 +8,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal public static class EntityTypeExtensions { public static bool IsDocumentRoot(this IEntityType entityType) - => !entityType.IsOwned(); + => entityType.BaseType == null + ? !entityType.IsOwned() + || entityType[CosmosAnnotationNames.ContainerName] != null + : entityType.BaseType.IsDocumentRoot(); } } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs new file mode 100644 index 00000000000..e9756e83e52 --- /dev/null +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -0,0 +1,62 @@ +// + +using System; +using System.Reflection; +using System.Resources; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static class CosmosStrings + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.EntityFrameworkCore.Cosmos.Properties.CosmosStrings", typeof(CosmosStrings).GetTypeInfo().Assembly); + + /// + /// The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. + /// + public static string OrphanedNestedDocument([CanBeNull] object entityType, [CanBeNull] object missingEntityType) + => string.Format( + GetString("OrphanedNestedDocument", nameof(entityType), nameof(missingEntityType)), + entityType, missingEntityType); + + /// + /// The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the key value '{keyValue}'. + /// + public static string OrphanedNestedDocumentSensitive([CanBeNull] object entityType, [CanBeNull] object missingEntityType, [CanBeNull] object keyValue) + => string.Format( + GetString("OrphanedNestedDocumentSensitive", nameof(entityType), nameof(missingEntityType), nameof(keyValue)), + entityType, missingEntityType, keyValue); + + /// + /// The entity of type '{entityType}' cannot be queried directly because it is mapped as a part of the document mapped to '{principalEntityType}'. Rewrite the query to start with '{principalEntityType}'. + /// + public static string QueryRootNestedEntityType([CanBeNull] object entityType, [CanBeNull] object principalEntityType) + => string.Format( + GetString("QueryRootNestedEntityType", nameof(entityType), nameof(principalEntityType)), + entityType, principalEntityType); + + /// + /// No matching discriminator values where found for this instance of '{entityType}'. + /// + public static string UnableToDiscriminate([CanBeNull] object entityType) + => string.Format( + GetString("UnableToDiscriminate", nameof(entityType)), + entityType); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.tt b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.tt new file mode 100644 index 00000000000..702ca5eade1 --- /dev/null +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.tt @@ -0,0 +1,5 @@ +<# + Session["ResourceFile"] = "CosmosStrings.resx"; + Session["NoDiagnostics"] = true; +#> +<#@ include file="..\..\..\tools\Resources.tt" #> \ No newline at end of file diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx new file mode 100644 index 00000000000..1b5c51ef014 --- /dev/null +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the corresponding key value. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values. + + + The entity of type '{entityType}' is mapped as a part of the document mapped to '{missingEntityType}', but there is no tracked entity of this type with the key value '{keyValue}'. + + + The entity of type '{entityType}' cannot be queried directly because it is mapped as a part of the document mapped to '{principalEntityType}'. Rewrite the query to start with '{principalEntityType}'. + + + No matching discriminator values where found for this instance of '{entityType}'. + + \ No newline at end of file diff --git a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitor.cs deleted file mode 100644 index 4315b802564..00000000000 --- a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitor.cs +++ /dev/null @@ -1,28 +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 Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.ExpressionVisitors.Internal -{ - /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public class CosmosEagerLoadingExpressionVisitor : EagerLoadingExpressionVisitor - { - public CosmosEagerLoadingExpressionVisitor( - QueryCompilationContext queryCompilationContext, - IQuerySourceTracingExpressionVisitorFactory querySourceTracingExpressionVisitorFactory) - : base(queryCompilationContext, querySourceTracingExpressionVisitorFactory) - { - } - - public override bool ShouldInclude(INavigation navigation) - => base.ShouldInclude(navigation) - && navigation.GetTargetType().IsDocumentRoot(); - } -} diff --git a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitorFactory.cs b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitorFactory.cs deleted file mode 100644 index c2b71d21c8c..00000000000 --- a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEagerLoadingExpressionVisitorFactory.cs +++ /dev/null @@ -1,24 +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 Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.ExpressionVisitors.Internal -{ - /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public class CosmosEagerLoadingExpressionVisitorFactory : IEagerLoadingExpressionVisitorFactory - { - /// - /// This API supports the Entity Framework Core infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public EagerLoadingExpressionVisitor Create( - QueryCompilationContext queryCompilationContext, - IQuerySourceTracingExpressionVisitorFactory querySourceTracingExpressionVisitorFactory) - => new CosmosEagerLoadingExpressionVisitor(queryCompilationContext, querySourceTracingExpressionVisitorFactory); - } -} diff --git a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitor.cs b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitor.cs index 0604d29457a..a6630742087 100644 --- a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitor.cs @@ -4,6 +4,8 @@ using System; using System.Linq.Expressions; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Expressions.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -36,6 +38,11 @@ public class CosmosEntityQueryableExpressionVisitor : EntityQueryableExpressionV protected override Expression VisitEntityQueryable([NotNull] Type elementType) { var entityType = _model.FindEntityType(elementType); + if (!entityType.IsDocumentRoot()) + { + throw new InvalidOperationException( + CosmosStrings.QueryRootNestedEntityType(entityType.DisplayName(), entityType.FindOwnership().PrincipalEntityType.DisplayName())); + } return new QueryShaperExpression( QueryModelVisitor.QueryCompilationContext.IsAsyncQuery, diff --git a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitorFactory.cs b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitorFactory.cs index 84b9d74a435..527f3c7615e 100644 --- a/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitorFactory.cs +++ b/src/EFCore.Cosmos/Query/ExpressionVisitors/Internal/CosmosEntityQueryableExpressionVisitorFactory.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query; @@ -26,12 +26,10 @@ public class CosmosEntityQueryableExpressionVisitorFactory : IEntityQueryableExp } public ExpressionVisitor Create(EntityQueryModelVisitor entityQueryModelVisitor, IQuerySource querySource) - { - return new CosmosEntityQueryableExpressionVisitor( + => new CosmosEntityQueryableExpressionVisitor( _model, _entityMaterializerSource, (CosmosQueryModelVisitor)entityQueryModelVisitor, querySource); - } } } diff --git a/src/EFCore.Cosmos/Query/Expressions/Internal/RootReferenceExpression.cs b/src/EFCore.Cosmos/Query/Expressions/Internal/RootReferenceExpression.cs index 522f65a38b4..4f3f14bedbb 100644 --- a/src/EFCore.Cosmos/Query/Expressions/Internal/RootReferenceExpression.cs +++ b/src/EFCore.Cosmos/Query/Expressions/Internal/RootReferenceExpression.cs @@ -21,9 +21,6 @@ public RootReferenceExpression(IEntityType entityType, string alias) _alias = alias; } - public override string ToString() - { - return _alias; - } + public override string ToString() => _alias; } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryModelVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryModelVisitor.cs index 3a3d00f1268..86bfcda141c 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryModelVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryModelVisitor.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; using System.Linq.Expressions; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Query.Expressions.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.ExpressionVisitors.Internal; using Microsoft.EntityFrameworkCore.Query; diff --git a/src/EFCore.Cosmos/Query/Internal/EntityShaper.cs b/src/EFCore.Cosmos/Query/Internal/EntityShaper.cs index e3dc2aa057a..50e12b8495e 100644 --- a/src/EFCore.Cosmos/Query/Internal/EntityShaper.cs +++ b/src/EFCore.Cosmos/Query/Internal/EntityShaper.cs @@ -7,6 +7,7 @@ using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -58,16 +59,13 @@ public virtual LambdaExpression CreateShaperLambda() private NewExpression CreateEntityInfoExpression(IEntityType entityType, INavigation navigation) { - var valueBufferFactory = ValueBufferFactoryFactory.Create(entityType); + var usedProperties = new List(); + var materializer = CreateMaterializerExpression(entityType, usedProperties, out var indexMap); - var materializationContextParameter - = Expression.Parameter(typeof(MaterializationContext), "materializationContext"); - var materializer = Expression.Lambda(_entityMaterializerSource - .CreateMaterializeExpression( - entityType, materializationContextParameter), materializationContextParameter); + var valueBufferFactory = ValueBufferFactoryFactory.Create(usedProperties); var nestedEntities = new List(); - foreach (var ownedNavigation in entityType.GetNavigations()) + foreach (var ownedNavigation in entityType.GetNavigations().Concat(entityType.GetDerivedNavigations())) { var fk = ownedNavigation.ForeignKey; if (!fk.IsOwnership @@ -88,12 +86,129 @@ var materializationContextParameter return Expression.New( EntityInfo.ConstructorInfo, Expression.Constant(navigation, typeof(INavigation)), - Expression.Constant(entityType.FindPrimaryKey()), + Expression.Constant(entityType.FindPrimaryKey(), typeof(IKey)), valueBufferFactory, materializer, + Expression.Constant(indexMap, typeof(Dictionary)), nestedEntitiesExpression); } + private LambdaExpression CreateMaterializerExpression( + IEntityType entityType, + List usedProperties, + out Dictionary typeIndexMap) + { + typeIndexMap = null; + + var materializationContextParameter + = Expression.Parameter(typeof(MaterializationContext), "materializationContext"); + + var concreteEntityTypes = entityType.GetConcreteTypesInHierarchy().ToList(); + var firstEntityType = concreteEntityTypes[0]; + var indexMap = new int[firstEntityType.PropertyCount()]; + + foreach (var property in firstEntityType.GetProperties()) + { + usedProperties.Add(property); + indexMap[property.GetIndex()] = usedProperties.Count - 1; + } + + var materializer + = _entityMaterializerSource + .CreateMaterializeExpression( + firstEntityType, materializationContextParameter); + + if (concreteEntityTypes.Count == 1) + { + return Expression.Lambda(materializer, materializationContextParameter); + } + + var discriminatorProperty = firstEntityType.Cosmos().DiscriminatorProperty; + + var firstDiscriminatorValue + = Expression.Constant( + firstEntityType.Cosmos().DiscriminatorValue, + discriminatorProperty.ClrType); + + var discriminatorValueVariable + = Expression.Variable(discriminatorProperty.ClrType); + + var returnLabelTarget = Expression.Label(entityType.ClrType); + + var blockExpressions + = new Expression[] + { + Expression.Assign( + discriminatorValueVariable, + _entityMaterializerSource + .CreateReadValueExpression( + Expression.Call(materializationContextParameter, MaterializationContext.GetValueBufferMethod), + discriminatorProperty.ClrType, + indexMap[discriminatorProperty.GetIndex()], + discriminatorProperty)), + Expression.IfThenElse( + Expression.Equal(discriminatorValueVariable, firstDiscriminatorValue), + Expression.Return(returnLabelTarget, materializer), + Expression.Throw( + Expression.Call( + _createUnableToDiscriminateException, + Expression.Constant(firstEntityType)))), + Expression.Label( + returnLabelTarget, + Expression.Default(returnLabelTarget.Type)) + }; + + foreach (var concreteEntityType in concreteEntityTypes.Skip(1)) + { + indexMap = new int[concreteEntityType.PropertyCount()]; + + var shadowPropertyExists = false; + + foreach (var property in concreteEntityType.GetProperties()) + { + var propertyIndex = usedProperties.IndexOf(property); + if (propertyIndex == -1) + { + usedProperties.Add(property); + propertyIndex = usedProperties.Count - 1; + } + indexMap[property.GetIndex()] = propertyIndex; + + shadowPropertyExists = shadowPropertyExists || property.IsShadowProperty; + } + + if (shadowPropertyExists) + { + if (typeIndexMap == null) + { + typeIndexMap = new Dictionary(); + } + + typeIndexMap[concreteEntityType.ClrType] = indexMap; + } + + var discriminatorValue + = Expression.Constant( + concreteEntityType.Cosmos().DiscriminatorValue, + discriminatorProperty.ClrType); + + materializer + = _entityMaterializerSource + .CreateMaterializeExpression( + concreteEntityType, materializationContextParameter); + + blockExpressions[1] + = Expression.IfThenElse( + Expression.Equal(discriminatorValueVariable, discriminatorValue), + Expression.Return(returnLabelTarget, materializer), + blockExpressions[1]); + } + + return Expression.Lambda( + Expression.Block(new[] { discriminatorValueVariable }, blockExpressions), + materializationContextParameter); + } + private static readonly MethodInfo _listAddMethodInfo = typeof(List).GetTypeInfo().GetDeclaredMethod(nameof(List.Add)); @@ -143,7 +258,8 @@ var materializationContextParameter entityInfo.Key, new EntityLoadInfo( new MaterializationContext(valueBuffer, queryContext.Context), - entityInfo.Materializer), + entityInfo.Materializer, + entityInfo.TypeIndexMap), queryStateManager: trackingQuery, throwOnNullKey: true); @@ -214,6 +330,14 @@ var materializationContextParameter return parentEntity; } + private static readonly MethodInfo _createUnableToDiscriminateException + = typeof(EntityShaper).GetTypeInfo() + .GetDeclaredMethod(nameof(CreateUnableToDiscriminateException)); + + [UsedImplicitly] + private static Exception CreateUnableToDiscriminateException(IEntityType entityType) + => new InvalidOperationException(CosmosStrings.UnableToDiscriminate(entityType.DisplayName())); + private class EntityInfo { public static readonly ConstructorInfo ConstructorInfo @@ -224,12 +348,14 @@ private class EntityInfo IKey key, Func valueBufferFactory, Func materializer, + Dictionary typeIndexMap, IList nestedEntities) { Navigation = navigation; Key = key; ValueBufferFactory = valueBufferFactory; Materializer = materializer; + TypeIndexMap = typeIndexMap; NestedEntities = nestedEntities; } @@ -237,6 +363,7 @@ private class EntityInfo public IKey Key { get; } public Func ValueBufferFactory { get; } public Func Materializer { get; } + public Dictionary TypeIndexMap { get; } public IList NestedEntities { get; } } } diff --git a/src/EFCore.Cosmos/Query/Internal/ValueBufferFactoryFactory.cs b/src/EFCore.Cosmos/Query/Internal/ValueBufferFactoryFactory.cs index b08483678cd..9a2f27d4908 100644 --- a/src/EFCore.Cosmos/Query/Internal/ValueBufferFactoryFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ValueBufferFactoryFactory.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 System.Linq.Expressions; using System.Reflection; @@ -20,11 +21,11 @@ public static class ValueBufferFactoryFactory .Single(pi => pi.Name == "Item" && pi.GetIndexParameters()[0].ParameterType == typeof(string)) .GetMethod; - public static Expression> Create(IEntityType entityType) + public static Expression> Create(List usedProperties) => Expression.Lambda>( Expression.NewArrayInit( typeof(object), - entityType.GetProperties() + usedProperties .Select(p => CreateGetValueExpression( _jObjectParameter, diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClient.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClient.cs index 79cb0adc5c9..5759cb34422 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClient.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClient.cs @@ -258,7 +258,7 @@ public bool DeleteDatabase() var response = (HttpWebResponse)request.GetResponse(); - return response.StatusCode == HttpStatusCode.OK; + return response.StatusCode == HttpStatusCode.Created; } public Task CreateDocumentAsync( @@ -287,7 +287,7 @@ public bool DeleteDatabase() throw new HttpException(response); } - return response.StatusCode == HttpStatusCode.OK; + return response.StatusCode == HttpStatusCode.Created; } public bool ReplaceDocument( diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabase.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabase.cs index 165b901ed48..89eca11f998 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabase.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabase.cs @@ -1,18 +1,22 @@ // 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.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Update.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update; +using Microsoft.EntityFrameworkCore.Update.Internal; using Newtonsoft.Json.Linq; namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal @@ -22,13 +26,20 @@ public class CosmosDatabase : Database private readonly Dictionary _documentCollections = new Dictionary(); private readonly CosmosClient _cosmosClient; + private readonly bool _sensitiveLoggingEnabled; public CosmosDatabase( DatabaseDependencies dependencies, - CosmosClient cosmosClient) + CosmosClient cosmosClient, + ILoggingOptions loggingOptions) : base(dependencies) { _cosmosClient = cosmosClient; + + if (loggingOptions.IsSensitiveDataLoggingEnabled) + { + _sensitiveLoggingEnabled = true; + } } public override int SaveChanges(IReadOnlyList entries) @@ -48,9 +59,11 @@ public override int SaveChanges(IReadOnlyList entries) if (!entityType.IsDocumentRoot()) { var root = GetRootDocument((InternalEntityEntry)entry); - if (!entriesSaved.Contains(root)) + if (!entriesSaved.Contains(root) + && rootEntriesToSave.Add(root) + && root.EntityState == EntityState.Unchanged) { - rootEntriesToSave.Add(root); + ((InternalEntityEntry)root).SetEntityState(EntityState.Modified); } continue; } @@ -79,14 +92,30 @@ private bool Save(IUpdateEntry entry) var entityType = entry.EntityType; var documentSource = GetDocumentSource(entityType); var collectionId = documentSource.GetCollectionId(); + var state = entry.EntityState; - switch (entry.EntityState) + if (entry.SharedIdentityEntry != null) + { + if (entry.EntityState == EntityState.Deleted) + { + return false; + } + + if (state == EntityState.Added) + { + state = EntityState.Modified; + } + } + + switch (state) { case EntityState.Added: return _cosmosClient.CreateDocument(collectionId, documentSource.CreateDocument(entry)); case EntityState.Modified: var jObjectProperty = entityType.FindProperty(StoreKeyConvention.JObjectPropertyName); - var document = jObjectProperty != null ? (JObject)entry.GetCurrentValue(jObjectProperty) : null; + var document = jObjectProperty != null + ? (JObject)(entry.SharedIdentityEntry ?? entry).GetCurrentValue(jObjectProperty) + : null; if (document != null) { documentSource.UpdateDocument(document, entry); @@ -94,13 +123,10 @@ private bool Save(IUpdateEntry entry) else { document = documentSource.CreateDocument(entry); - - // Set Discriminator Property for updates - document[entityType.Cosmos().DiscriminatorProperty.Name] = - JToken.FromObject(entityType.Cosmos().DiscriminatorValue); } - return _cosmosClient.ReplaceDocument(collectionId, documentSource.GetId(entry), document); + return _cosmosClient.ReplaceDocument( + collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document); case EntityState.Deleted: return _cosmosClient.DeleteDocument(collectionId, documentSource.GetId(entry)); default: @@ -156,14 +182,30 @@ private Task SaveAsync(IUpdateEntry entry, CancellationToken cancellationT var entityType = entry.EntityType; var documentSource = GetDocumentSource(entityType); var collectionId = documentSource.GetCollectionId(); + var state = entry.EntityState; + + if (entry.SharedIdentityEntry != null) + { + if (entry.EntityState == EntityState.Deleted) + { + return Task.FromResult(false); + } + + if (state == EntityState.Added) + { + state = EntityState.Modified; + } + } - switch (entry.EntityState) + switch (state) { case EntityState.Added: return _cosmosClient.CreateDocumentAsync(collectionId, documentSource.CreateDocument(entry), cancellationToken); case EntityState.Modified: var jObjectProperty = entityType.FindProperty(StoreKeyConvention.JObjectPropertyName); - var document = jObjectProperty != null ? (JObject)entry.GetCurrentValue(jObjectProperty) : null; + var document = jObjectProperty != null + ? (JObject)(entry.SharedIdentityEntry ?? entry).GetCurrentValue(jObjectProperty) + : null; if (document != null) { documentSource.UpdateDocument(document, entry); @@ -177,7 +219,8 @@ private Task SaveAsync(IUpdateEntry entry, CancellationToken cancellationT JToken.FromObject(entityType.Cosmos().DiscriminatorValue); } - return _cosmosClient.ReplaceDocumentAsync(collectionId, documentSource.GetId(entry), document, cancellationToken); + return _cosmosClient.ReplaceDocumentAsync( + collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document, cancellationToken); case EntityState.Deleted: return _cosmosClient.DeleteDocumentAsync(collectionId, documentSource.GetId(entry), cancellationToken); default: @@ -199,7 +242,25 @@ public DocumentSource GetDocumentSource(IEntityType entityType) private IUpdateEntry GetRootDocument(InternalEntityEntry entry) { var stateManager = entry.StateManager; - var principal = stateManager.GetPrincipal(entry, entry.EntityType.FindOwnership()); + var ownership = entry.EntityType.FindOwnership(); + var principal = stateManager.GetPrincipal(entry, ownership); + if (principal == null) + { + if (_sensitiveLoggingEnabled) + { + throw new InvalidOperationException( + CosmosStrings.OrphanedNestedDocumentSensitive( + entry.EntityType.DisplayName(), + ownership.PrincipalEntityType.DisplayName(), + entry.BuildCurrentValuesString(entry.EntityType.FindPrimaryKey().Properties))); + } + + throw new InvalidOperationException( + CosmosStrings.OrphanedNestedDocument( + entry.EntityType.DisplayName(), + ownership.PrincipalEntityType.DisplayName())); + } + return principal.EntityType.IsDocumentRoot() ? principal : GetRootDocument(principal); } } diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs index 2c70317721a..2852a0f8108 100644 --- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs +++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs @@ -15,26 +15,26 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal public class DocumentSource { private readonly string _collectionId; - private readonly IEntityType _entityType; private readonly CosmosDatabase _database; + private readonly IProperty _idProperty; public DocumentSource(IEntityType entityType, CosmosDatabase database) { _collectionId = entityType.Cosmos().ContainerName; - _entityType = entityType; _database = database; + _idProperty = entityType.FindProperty(StoreKeyConvention.IdPropertyName); } public string GetCollectionId() => _collectionId; public string GetId(IUpdateEntry entry) - => entry.GetCurrentValue(_entityType.FindProperty(StoreKeyConvention.IdPropertyName)); + => entry.GetCurrentValue(_idProperty); public JObject CreateDocument(IUpdateEntry entry) { var document = new JObject(); - foreach (var property in _entityType.GetProperties()) + foreach (var property in entry.EntityType.GetProperties()) { if (property.Name != StoreKeyConvention.JObjectPropertyName) { @@ -43,7 +43,7 @@ public JObject CreateDocument(IUpdateEntry entry) } } - foreach (var ownedNavigation in _entityType.GetNavigations()) + foreach (var ownedNavigation in entry.EntityType.GetNavigations()) { var fk = ownedNavigation.ForeignKey; if (!fk.IsOwnership @@ -58,8 +58,7 @@ public JObject CreateDocument(IUpdateEntry entry) { document[ownedNavigation.Name] = null; } - - if (fk.IsUnique) + else if (fk.IsUnique) { var dependentEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(nestedValue, fk.DeclaringEntityType); document[ownedNavigation.Name] = _database.GetDocumentSource(dependentEntry.EntityType).CreateDocument(dependentEntry); @@ -82,17 +81,19 @@ public JObject CreateDocument(IUpdateEntry entry) public JObject UpdateDocument(JObject document, IUpdateEntry entry) { - foreach (var property in _entityType.GetProperties()) + foreach (var property in entry.EntityType.GetProperties()) { if (property.Name != StoreKeyConvention.JObjectPropertyName - && entry.IsModified(property)) + && property.Name != StoreKeyConvention.IdPropertyName + && (entry.EntityState == EntityState.Added + || entry.IsModified(property))) { var value = entry.GetCurrentValue(property); document[property.Name] = value != null ? JToken.FromObject(value) : null; } } - foreach (var ownedNavigation in _entityType.GetNavigations()) + foreach (var ownedNavigation in entry.EntityType.GetNavigations()) { var fk = ownedNavigation.ForeignKey; if (!fk.IsOwnership @@ -108,8 +109,7 @@ public JObject UpdateDocument(JObject document, IUpdateEntry entry) { document[ownedNavigation.Name] = null; } - - if (fk.IsUnique) + else if (fk.IsUnique) { var nestedEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(nestedValue, fk.DeclaringEntityType); var nestedDocument = (JObject)document[ownedNavigation.Name]; diff --git a/src/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs b/src/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs index c037b95a59d..2d46506b002 100644 --- a/src/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs +++ b/src/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs @@ -19,7 +19,7 @@ public abstract class TableSplittingTestBase { protected TableSplittingTestBase(ITestOutputHelper testOutputHelper) { - TestSqlLoggerFactory = (TestSqlLoggerFactory)TestStoreFactory.CreateListLoggerFactory(l => true); + TestSqlLoggerFactory = (TestSqlLoggerFactory)TestStoreFactory.CreateListLoggerFactory(_ => true); //TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/Internal/CompositeShaper.cs b/src/EFCore.Relational/Query/ExpressionVisitors/Internal/CompositeShaper.cs index 415f3c0eff7..aab2fafc24e 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/Internal/CompositeShaper.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/Internal/CompositeShaper.cs @@ -33,8 +33,7 @@ public static class CompositeShaper Check.NotNull(innerShaper, nameof(innerShaper)); Check.NotNull(materializer, nameof(materializer)); - var compositeShaper - = (Shaper)_createCompositeShaperMethodInfo + return (Shaper)_createCompositeShaperMethodInfo .MakeGenericMethod( outerShaper.GetType(), outerShaper.Type, @@ -51,8 +50,6 @@ var compositeShaper materializer.Compile(), storeMaterializerExpression ? materializer : null }); - - return compositeShaper; } /// @@ -175,11 +172,9 @@ public override Shaper AddOffset(int offset) } public override Shaper Unwrap(IQuerySource querySource) - { - return _outerShaper.Unwrap(querySource) + => _outerShaper.Unwrap(querySource) ?? _innerShaper.Unwrap(querySource) ?? base.Unwrap(querySource); - } } } } diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/Internal/MaterializerFactory.cs b/src/EFCore.Relational/Query/ExpressionVisitors/Internal/MaterializerFactory.cs index d2df430410f..d52815004d3 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/Internal/MaterializerFactory.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/Internal/MaterializerFactory.cs @@ -24,9 +24,6 @@ public class MaterializerFactory : IMaterializerFactory { private readonly IEntityMaterializerSource _entityMaterializerSource; - private static readonly MethodInfo _getValueBufferMethod - = typeof(MaterializationContext).GetProperty(nameof(MaterializationContext.ValueBuffer)).GetMethod; - /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -96,7 +93,7 @@ var blockExpressions discriminatorValueVariable, _entityMaterializerSource .CreateReadValueExpression( - Expression.Call(materializationContextParameter, _getValueBufferMethod), + Expression.Call(materializationContextParameter, MaterializationContext.GetValueBufferMethod), discriminatorProperty.ClrType, indexMap[discriminatorProperty.GetIndex()], discriminatorProperty)), diff --git a/src/EFCore.Specification.Tests/TestModels/TransportationModel/FuelTank.cs b/src/EFCore.Specification.Tests/TestModels/TransportationModel/FuelTank.cs index d53e9fc8a78..883da084be8 100644 --- a/src/EFCore.Specification.Tests/TestModels/TransportationModel/FuelTank.cs +++ b/src/EFCore.Specification.Tests/TestModels/TransportationModel/FuelTank.cs @@ -10,8 +10,7 @@ public class FuelTank public string Capacity { get; set; } - // #9005 - //public PoweredVehicle Vehicle { get; set; } + public PoweredVehicle Vehicle { get; set; } public CombustionEngine Engine { get; set; } public override bool Equals(object obj) diff --git a/src/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs b/src/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs index cd36b07cafa..a286d23257b 100644 --- a/src/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs +++ b/src/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs @@ -58,6 +58,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) //eb.HasOne(e => e.Vehicle) // .WithOne() // .HasForeignKey(e => e.VehicleName); + eb.Ignore(e => e.Vehicle); }); modelBuilder.Entity( @@ -76,13 +77,15 @@ public void Seed() } public void AssertSeeded() - => Assert.Equal( - CreateVehicles().OrderBy(v => v.Name).ToList(), - Vehicles - .Include(v => v.Operator) - .Include(v => ((PoweredVehicle)v).Engine) - .ThenInclude(e => (e as CombustionEngine).FuelTank) - .OrderBy(v => v.Name).ToList()); + { + var expected = CreateVehicles().OrderBy(v => v.Name).ToList(); + var actual = Vehicles + .Include(v => v.Operator) + .Include(v => ((PoweredVehicle)v).Engine) + .ThenInclude(e => (e as CombustionEngine).FuelTank) + .OrderBy(v => v.Name).ToList(); + Assert.Equal(expected, actual); + } protected IEnumerable CreateVehicles() => new List diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index edbcf400207..ed350572724 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -303,7 +303,8 @@ protected virtual void ValidateOwnership([NotNull] IModel model) throw new InvalidOperationException(CoreStrings.OwnedDerivedType(entityType.DisplayName())); } - foreach (var referencingFk in entityType.GetReferencingForeignKeys().Where(fk => !fk.IsOwnership)) + foreach (var referencingFk in entityType.GetReferencingForeignKeys().Where(fk => !fk.IsOwnership + && !Contains(fk.DeclaringEntityType.FindOwnership(), fk))) { throw new InvalidOperationException( CoreStrings.PrincipalOwnedType( @@ -318,7 +319,8 @@ protected virtual void ValidateOwnership([NotNull] IModel model) entityType.DisplayName())); } - foreach (var fk in entityType.GetDeclaredForeignKeys().Where(fk => !fk.IsOwnership && fk.PrincipalToDependent != null)) + foreach (var fk in entityType.GetDeclaredForeignKeys().Where(fk => !fk.IsOwnership && fk.PrincipalToDependent != null + && !Contains(fk.DeclaringEntityType.FindOwnership(), fk))) { throw new InvalidOperationException( CoreStrings.InverseToOwnedType( @@ -335,6 +337,11 @@ protected virtual void ValidateOwnership([NotNull] IModel model) } } + private bool Contains(IForeignKey inheritedFk, IForeignKey derivedFk) + => inheritedFk != null + && inheritedFk.PrincipalEntityType.IsAssignableFrom(derivedFk.PrincipalEntityType) + && PropertyListComparer.Instance.Equals(inheritedFk.Properties, derivedFk.Properties); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 08ebc857b68..eeaa2795698 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -2124,10 +2124,29 @@ public virtual void AddData([NotNull] IEnumerable data) #region Explicit interface implementations - IModel ITypeBase.Model => Model; - IMutableModel IMutableTypeBase.Model => Model; - IMutableModel IMutableEntityType.Model => Model; - IEntityType IEntityType.BaseType => _baseType; + IModel ITypeBase.Model + { + [DebuggerStepThrough] + get => Model; + } + + IMutableModel IMutableTypeBase.Model + { + [DebuggerStepThrough] + get => Model; + } + + IMutableModel IMutableEntityType.Model + { + [DebuggerStepThrough] + get => Model; + } + + IEntityType IEntityType.BaseType + { + [DebuggerStepThrough] + get => _baseType; + } IMutableEntityType IMutableEntityType.BaseType { diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index 861909352b1..702b59c80e9 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -530,6 +530,20 @@ public static IEnumerable GetDeclaredReferencingForeignKeys([NotNul => entityType.GetDerivedTypes().SelectMany( et => et.GetDeclaredNavigations().Where(navigation => navigationName == navigation.Name)); + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static IEnumerable GetDerivedNavigations([NotNull] this IEntityType entityType) + => entityType.AsEntityType().GetDerivedNavigations(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static IEnumerable GetDerivedNavigationsInclusive([NotNull] this IEntityType entityType) + => entityType.AsEntityType().GetDerivedNavigationsInclusive(); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index e2e291d7e71..9680e9cb364 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -2289,19 +2289,23 @@ private static InternalIndexBuilder DetachIndex(Index indexToDetach) /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public virtual bool RemoveNonOwnershipRelationships(ConfigurationSource configurationSource) + public virtual bool RemoveNonOwnershipRelationships(ForeignKey ownership, ConfigurationSource configurationSource) { - var referencingRelationships = Metadata.GetDerivedForeignKeysInclusive() - .Where(fk => !fk.IsOwnership && fk.PrincipalToDependent != null) + var incompatibleRelationships = Metadata.GetDerivedForeignKeysInclusive() + .Where(fk => !fk.IsOwnership && fk.PrincipalToDependent != null + && !Contains(ownership, fk)) .Concat(Metadata.GetDerivedReferencingForeignKeysInclusive() - .Where(fk => !fk.IsOwnership)).ToList(); + .Where(fk => !fk.IsOwnership + && !Contains(fk.DeclaringEntityType.FindOwnership(), fk))) + .Distinct() + .ToList(); - if (referencingRelationships.Any(fk => !configurationSource.Overrides(fk.GetConfigurationSource()))) + if (incompatibleRelationships.Any(fk => !configurationSource.Overrides(fk.GetConfigurationSource()))) { return false; } - foreach (var foreignKey in referencingRelationships) + foreach (var foreignKey in incompatibleRelationships) { foreignKey.DeclaringEntityType.Builder.RemoveForeignKey(foreignKey, configurationSource); } @@ -2309,6 +2313,11 @@ public virtual bool RemoveNonOwnershipRelationships(ConfigurationSource configur return true; } + private bool Contains(IForeignKey inheritedFk, IForeignKey derivedFk) + => inheritedFk != null + && inheritedFk.PrincipalEntityType.IsAssignableFrom(derivedFk.PrincipalEntityType) + && PropertyListComparer.Instance.Equals(inheritedFk.Properties, derivedFk.Properties); + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs index 52ff1020b1a..aeeb4b48800 100644 --- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs @@ -307,20 +307,20 @@ private bool Owned(in TypeIdentity type, ConfigurationSource configurationSource throw new InvalidOperationException(CoreStrings.ClashingNonOwnedEntityType(entityType.DisplayName())); } - var ownership = entityType.GetForeignKeys().FirstOrDefault( + var ownershipCandidate = entityType.GetForeignKeys().FirstOrDefault( fk => fk.PrincipalToDependent != null && !fk.PrincipalEntityType.IsInOwnershipPath(entityType) && !fk.PrincipalEntityType.IsInDefinitionPath(clrType)); - if (ownership != null) + if (ownershipCandidate != null) { - if (ownership.Builder.IsOwnership(true, configurationSource) == null) + if (ownershipCandidate.Builder.IsOwnership(true, configurationSource) == null) { return false; } } else { - if (!entityType.Builder.RemoveNonOwnershipRelationships(configurationSource)) + if (!entityType.Builder.RemoveNonOwnershipRelationships(null, configurationSource)) { return false; } diff --git a/src/EFCore/Metadata/Internal/InternalRelationshipBuilder.cs b/src/EFCore/Metadata/Internal/InternalRelationshipBuilder.cs index 84f202047e1..7b6efff544b 100644 --- a/src/EFCore/Metadata/Internal/InternalRelationshipBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalRelationshipBuilder.cs @@ -933,7 +933,8 @@ public virtual InternalRelationshipBuilder IsOwnership(bool ownership, Configura Metadata.SetIsOwnership(true, configurationSource); newRelationshipBuilder.Metadata.DeclaringEntityType.Builder.HasBaseType((Type)null, configurationSource); - if (!newRelationshipBuilder.Metadata.DeclaringEntityType.Builder.RemoveNonOwnershipRelationships(configurationSource)) + if (!newRelationshipBuilder.Metadata.DeclaringEntityType.Builder + .RemoveNonOwnershipRelationships(newRelationshipBuilder.Metadata, configurationSource)) { return null; } diff --git a/src/EFCore/Query/EntityQueryModelVisitor.cs b/src/EFCore/Query/EntityQueryModelVisitor.cs index cca3c9f2186..2f4a514b27b 100644 --- a/src/EFCore/Query/EntityQueryModelVisitor.cs +++ b/src/EFCore/Query/EntityQueryModelVisitor.cs @@ -606,8 +606,7 @@ var groupResultOperator else { var subqueryExpression - = (queryModel.SelectClause.Selector - .TryGetReferencedQuerySource() as MainFromClause)?.FromExpression as SubQueryExpression; + = (outputExpression.TryGetReferencedQuerySource() as MainFromClause)?.FromExpression as SubQueryExpression; var nestedGroupResultOperator = subqueryExpression?.QueryModel?.ResultOperators diff --git a/src/EFCore/Query/Internal/ExpressionPrinter.cs b/src/EFCore/Query/Internal/ExpressionPrinter.cs index fb7e3722e35..b5ab35f0ef7 100644 --- a/src/EFCore/Query/Internal/ExpressionPrinter.cs +++ b/src/EFCore/Query/Internal/ExpressionPrinter.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Runtime.CompilerServices; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Extensions.Internal; using Microsoft.EntityFrameworkCore.Internal; diff --git a/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs b/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs index 40a491b1f31..6d69a4d0909 100644 --- a/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/IncludeCompiler.CollectionQueryModelRewritingExpressionVisitor.cs @@ -29,6 +29,9 @@ private sealed class CollectionQueryModelRewritingExpressionVisitor : RelinqExpr private readonly QueryModel _parentQueryModel; private readonly IncludeCompiler _includeCompiler; + private static readonly MethodInfo _emptyMethodInfo + = typeof(Enumerable).GetTypeInfo().GetDeclaredMethod(nameof(Enumerable.Empty)); + public CollectionQueryModelRewritingExpressionVisitor( QueryCompilationContext queryCompilationContext, QueryModel parentQueryModel, @@ -92,17 +95,22 @@ var querySourceReferenceFindingExpressionTreeVisitor .OfType() .SingleOrDefault(); - if (whereClause != null) + if (whereClause == null) { - whereClause.TransformExpressions(querySourceReferenceFindingExpressionTreeVisitor.Visit); + // Assuming this is a client query - collectionQueryModel.BodyClauses.Remove(whereClause); - } - else - { - collectionQueryModel.MainFromClause.TransformExpressions(querySourceReferenceFindingExpressionTreeVisitor.Visit); + collectionQueryModel.MainFromClause.FromExpression = + Expression.Coalesce( + collectionQueryModel.MainFromClause.FromExpression, + Expression.Call(null, _emptyMethodInfo.MakeGenericMethod(navigation.GetTargetType().ClrType))); + + return; } + whereClause.TransformExpressions(querySourceReferenceFindingExpressionTreeVisitor.Visit); + + collectionQueryModel.BodyClauses.Remove(whereClause); + var parentQuerySourceReferenceExpression = querySourceReferenceFindingExpressionTreeVisitor.QuerySourceReferenceExpression; diff --git a/src/EFCore/Storage/MaterializationContext.cs b/src/EFCore/Storage/MaterializationContext.cs index 8e4c11ed249..705536eea6a 100644 --- a/src/EFCore/Storage/MaterializationContext.cs +++ b/src/EFCore/Storage/MaterializationContext.cs @@ -20,7 +20,10 @@ namespace Microsoft.EntityFrameworkCore.Storage /// public readonly struct MaterializationContext { - internal static readonly MethodInfo GetValueBufferMethod + /// + /// The for the get method. + /// + public static readonly MethodInfo GetValueBufferMethod = typeof(MaterializationContext).GetProperty(nameof(ValueBuffer)).GetMethod; internal static readonly PropertyInfo ContextProperty diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosEndToEndTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosEndToEndTest.cs index b33dc3904e0..dfe55fb784f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosEndToEndTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosEndToEndTest.cs @@ -264,6 +264,7 @@ public ExtraCustomerContext(DbContextOptions dbContextOptions) protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.HasDefaultContainerName(nameof(CustomerContext)); modelBuilder.Entity().Property("EMail"); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/NestedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/NestedDocumentsTest.cs new file mode 100644 index 00000000000..3f155592e05 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/NestedDocumentsTest.cs @@ -0,0 +1,423 @@ +// 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 Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.TestUtilities; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestModels.TransportationModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + public class NestedDocumentsTest + { + public NestedDocumentsTest(ITestOutputHelper testOutputHelper) + { + TestSqlLoggerFactory = (TestSqlLoggerFactory)TestStoreFactory.CreateListLoggerFactory(_ => true); + //TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + // #13579 + // [Theory] + [InlineData(true)] + [InlineData(false)] + public virtual void Can_update_dependents(bool useNesting) + { + using (CreateTestStore(modelBuilder => + { + OnModelCreating(modelBuilder); + + if (!useNesting) + { + RemoveNesting(modelBuilder); + } + })) + { + Operator firstOperator; + Engine firstEngine; + using (var context = CreateContext()) + { + firstOperator = context.Set().Select(v => v.Operator).OrderBy(o => o.VehicleName).First(); + firstOperator.Name += "1"; + firstEngine = context.Set().Select(v => v.Engine).OrderBy(o => o.VehicleName).First(); + firstEngine.Description += "1"; + + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + Assert.Equal(firstOperator.Name, + context.Set().Select(v => v.Operator).OrderBy(o => o.VehicleName).First().Name); + Assert.Equal(firstEngine.Description, + context.Set().Select(v => v.Engine).OrderBy(o => o.VehicleName).First().Description); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public virtual void Can_update_owner_with_dependents(bool useNesting) + { + using (CreateTestStore(modelBuilder => + { + OnModelCreating(modelBuilder); + + if (!useNesting) + { + RemoveNesting(modelBuilder); + } + })) + { + Operator firstOperator; + Engine firstEngine; + using (var context = CreateContext()) + { + firstOperator = context.Set().OrderBy(o => o.Operator.VehicleName).First().Operator; + firstOperator.Name += "1"; + firstEngine = context.Set().OrderBy(o => o.Engine.VehicleName).First().Engine; + firstEngine.Description += "1"; + + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + Assert.Equal(firstOperator.Name, + context.Set().OrderBy(o => o.Operator.VehicleName).First().Operator.Name); + Assert.Equal(firstEngine.Description, + context.Set().OrderBy(o => o.Engine.VehicleName).First().Engine.Description); + } + } + } + + // #13559 + //[Theory] + [InlineData(true)] + [InlineData(false)] + public virtual void Can_update_just_dependents(bool useNesting) + { + using (CreateTestStore(modelBuilder => + { + OnModelCreating(modelBuilder); + + if (!useNesting) + { + RemoveNesting(modelBuilder); + } + })) + { + Operator firstOperator; + Engine firstEngine; + using (var context = CreateContext()) + { + firstOperator = context.Set().OrderBy(o => o.VehicleName).First(); + firstOperator.Name += "1"; + firstEngine = context.Set().OrderBy(o => o.VehicleName).First(); + firstEngine.Description += "1"; + + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + Assert.Equal(firstOperator.Name, context.Set().OrderBy(o => o.VehicleName).First().Name); + Assert.Equal(firstEngine.Description, context.Set().OrderBy(o => o.VehicleName).First().Description); + } + } + } + + [Fact] + public virtual void Quering_nested_entity_directly_throws() + { + using (CreateTestStore(OnModelCreating)) + { + using (var context = CreateContext()) + { + Assert.Equal(CosmosStrings.QueryRootNestedEntityType(nameof(Operator), nameof(Vehicle)), + Assert.Throws(() => context.Set().ToList()).Message); + } + } + } + + // #13559 + //[Fact] + public virtual void Can_query_nested_derived_hierarchy() + { + using (CreateTestStore(OnModelCreating)) + { + using (var context = CreateContext()) + { + Assert.Equal(2, context.Set().ToList().Count); + } + } + } + + // #13559 + //[Fact] + public virtual void Can_query_nested_derived_nonhierarchy() + { + using (CreateTestStore( + modelBuilder => + { + OnModelCreating(modelBuilder); + modelBuilder.Ignore(); + })) + { + using (var context = CreateContext()) + { + Assert.Equal(2, context.Set().ToList().Count); + } + } + } + + [Fact] + public virtual void Can_roundtrip() + { + Test_roundtrip( + modelBuilder => + { + OnModelCreating(modelBuilder); + modelBuilder.Entity(eb => eb.Ignore(e => e.Vehicle)); + }); + } + + [Fact] + public virtual void Can_roundtrip_with_redundant_relationships() + { + Test_roundtrip(OnModelCreating); + } + + [Fact] + public virtual void Can_roundtrip_with_fanned_relationships() + { + Test_roundtrip( + modelBuilder => + { + OnModelCreating(modelBuilder); + modelBuilder.Entity(eb => eb.Ignore(e => e.Rocket)); + modelBuilder.Entity(eb => eb.Ignore(e => e.SolidFuelTank)); + }); + } + + protected void Test_roundtrip(Action onModelCreating) + { + using (CreateTestStore(onModelCreating)) + { + using (var context = CreateContext()) + { + context.AssertSeeded(); + } + } + } + + [Fact] + public virtual void Inserting_dependent_without_principal_throws() + { + using (CreateTestStore(OnModelCreating)) + { + using (var context = CreateContext()) + { + context.Add( + new PoweredVehicle + { + Name = "Fuel transport", + SeatingCapacity = 1, + Operator = new LicensedOperator + { + Name = "Jack Jackson", + LicenseType = "Class A CDC" + } + }); + context.Add( + new FuelTank + { + Capacity = "10000 l", + FuelType = "Gas", + VehicleName = "Fuel transport" + }); + + Assert.Equal( + CosmosStrings.OrphanedNestedDocumentSensitive( + nameof(FuelTank), nameof(CombustionEngine), "{VehicleName: Fuel transport}"), + Assert.Throws(() => context.SaveChanges()).Message); + } + } + } + + [Fact] + public virtual void Can_change_nested_instance_non_derived() + { + using (CreateTestStore( + modelBuilder => + { + OnModelCreating(modelBuilder); + modelBuilder.Entity().ToContainer("TransportationContext"); + modelBuilder.Entity( + eb => + { + eb.ToContainer("TransportationContext"); + eb.HasOne(e => e.Engine) + .WithOne(e => e.FuelTank) + .HasForeignKey(e => e.VehicleName) + .OnDelete(DeleteBehavior.Restrict); + }); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + })) + { + using (var context = CreateContext()) + { + var bike = context.Vehicles.Single(v => v.Name == "Trek Pro Fit Madone 6 Series"); + + bike.Operator = new Operator + { + Name = "Chris Horner" + }; + + context.ChangeTracker.DetectChanges(); + + bike.Operator = new LicensedOperator + { + Name = "repairman", + LicenseType = "Repair" + }; + + TestSqlLoggerFactory.Clear(); + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + var bike = context.Vehicles.Single(v => v.Name == "Trek Pro Fit Madone 6 Series"); + Assert.Equal("repairman", bike.Operator.Name); + + Assert.Equal("Repair", ((LicensedOperator)bike.Operator).LicenseType); + } + } + } + + [Fact] + public virtual void Can_change_principal_instance_non_derived() + { + using (CreateTestStore( + modelBuilder => + { + OnModelCreating(modelBuilder); + modelBuilder.Entity().ToContainer("TransportationContext"); + modelBuilder.Entity( + eb => + { + eb.ToContainer("TransportationContext"); + eb.HasOne(e => e.Engine) + .WithOne(e => e.FuelTank) + .HasForeignKey(e => e.VehicleName) + .OnDelete(DeleteBehavior.Restrict); + }); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + })) + { + using (var context = CreateContext()) + { + var bike = context.Vehicles.Single(v => v.Name == "Trek Pro Fit Madone 6 Series"); + + var newBike = new Vehicle + { + Name = "Trek Pro Fit Madone 6 Series", + Operator = bike.Operator, + SeatingCapacity = 2 + }; + + var oldEntry = context.Remove(bike); + var newEntry = context.Add(newBike); + + TestSqlLoggerFactory.Clear(); + context.SaveChanges(); + } + + using (var context = CreateContext()) + { + var bike = context.Vehicles.Single(v => v.Name == "Trek Pro Fit Madone 6 Series"); + + Assert.Equal(2, bike.SeatingCapacity); + Assert.NotNull(bike.Operator); + } + } + } + + protected readonly string DatabaseName = "NestedDocumentsTest"; + protected TestStore TestStore { get; set; } + protected ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance; + protected IServiceProvider ServiceProvider { get; set; } + protected TestSqlLoggerFactory TestSqlLoggerFactory { get; } + + protected void AssertSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected); + + protected void AssertContainsSql(params string[] expected) + => TestSqlLoggerFactory.AssertBaseline(expected, assertOrder: false); + + protected virtual void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.Operator)); + + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.FuelTank)); + + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.Engine)); + } + + private static void RemoveNesting(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.Operator).ToContainer(nameof(TransportationContext))); + + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.FuelTank).ToContainer(nameof(TransportationContext))); + + modelBuilder.Entity( + eb => eb.OwnsOne(v => v.Engine).ToContainer(nameof(TransportationContext))); + } + + protected TestStore CreateTestStore(Action onModelCreating) + { + TestStore = TestStoreFactory.Create(DatabaseName); + + ServiceProvider = TestStoreFactory.AddProviderServices(new ServiceCollection()) + .AddSingleton(TestModelSource.GetFactory(onModelCreating)) + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider(validateScopes: true); + + TestStore.Initialize(ServiceProvider, CreateContext, c => ((TransportationContext)c).Seed()); + + TestSqlLoggerFactory.Clear(); + + return TestStore; + } + + protected virtual DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder + .EnableSensitiveDataLogging() + .ConfigureWarnings( + b => b.Default(WarningBehavior.Throw) + .Log(CoreEventId.SensitiveDataLoggingEnabledWarning) + .Log(CoreEventId.PossibleUnintendedReferenceComparisonWarning)); + + protected virtual TransportationContext CreateContext() + { + var options = AddOptions(TestStore.AddProviderOptions(new DbContextOptionsBuilder())) + .UseInternalServiceProvider(ServiceProvider).Options; + return new TransportationContext(options); + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosSqlTest.Functions.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosSqlTest.Functions.cs index 89a2cc7763e..dd3a6743656 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosSqlTest.Functions.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosSqlTest.Functions.cs @@ -393,7 +393,7 @@ public override async Task Where_math_log10(bool isAsync) AssertSql( @"SELECT c FROM root c -WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0)))"); +WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0.0)))"); } public override async Task Where_math_log(bool isAsync) @@ -403,7 +403,7 @@ public override async Task Where_math_log(bool isAsync) AssertSql( @"SELECT c FROM root c -WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0)))"); +WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0.0)))"); } public override async Task Where_math_log_new_base(bool isAsync) @@ -413,7 +413,7 @@ public override async Task Where_math_log_new_base(bool isAsync) AssertSql( @"SELECT c FROM root c -WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0)))"); +WHERE ((c[""Discriminator""] = ""OrderDetail"") AND ((c[""OrderID""] = 11077) AND (c[""Discount""] > 0.0)))"); } public override async Task Where_math_sqrt(bool isAsync) diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index cd6c0b0465d..7fb4f77862c 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -82,9 +82,8 @@ private async Task CreateFromFile(DbContext context) { document["id"] = $"{entityName}|{document["id"]}"; document["Discriminator"] = entityName; - // TODO: Update the collection name here once there is model builder config // TODO: Stream the document - await cosmosClient.CreateDocumentAsync("Unicorn", document); + await cosmosClient.CreateDocumentAsync("NorthwindContext", document); } } } diff --git a/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs new file mode 100644 index 00000000000..0cda5fd8287 --- /dev/null +++ b/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs @@ -0,0 +1,45 @@ +// 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.Cosmos.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata +{ + public class CosmosBuilderExtensionsTest + { + [Fact] + public void Default_container_name_is_used_if_not_set() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder + .Entity(); + + var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + + Assert.Equal("Customer", entityType.Cosmos().ContainerName); + Assert.Null(modelBuilder.Model.Cosmos().DefaultContainerName); + + modelBuilder.HasDefaultContainerName("db0"); + + Assert.Equal("db0", entityType.Cosmos().ContainerName); + Assert.Equal("db0", modelBuilder.Model.Cosmos().DefaultContainerName); + + modelBuilder + .Entity() + .ToContainer("db1"); + + Assert.Equal("db1", entityType.Cosmos().ContainerName); + } + + protected virtual ModelBuilder CreateConventionModelBuilder() => CosmosTestHelpers.Instance.CreateConventionBuilder(); + + private class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public short SomeShort { get; set; } + } + } +} diff --git a/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs index 8db00a57911..b08f8ba418b 100644 --- a/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs +++ b/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs @@ -15,16 +15,18 @@ public void Can_get_and_set_collection_name() var modelBuilder = new ModelBuilder(new ConventionSet()); var entityType = modelBuilder - .Entity() - .Metadata; + .Entity(); - Assert.Equal("Unicorn", entityType.Cosmos().ContainerName); + Assert.Equal(nameof(Customer), entityType.Metadata.Cosmos().ContainerName); - entityType.Cosmos().ContainerName = "Customizer"; - Assert.Equal("Customizer", entityType.Cosmos().ContainerName); + entityType.ToContainer("Customizer"); + Assert.Equal("Customizer", entityType.Metadata.Cosmos().ContainerName); - entityType.Cosmos().ContainerName = null; - Assert.Equal("Unicorn", entityType.Cosmos().ContainerName); + entityType.ToContainer(null); + Assert.Equal(nameof(Customer), entityType.Metadata.Cosmos().ContainerName); + + modelBuilder.HasDefaultContainerName("Unicorn"); + Assert.Equal("Unicorn", entityType.Metadata.Cosmos().ContainerName); } private class Customer