diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs index 110ae9e1692..4982eec5ddc 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs @@ -1,8 +1,12 @@ // 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.Expressions; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; @@ -160,5 +164,96 @@ public static bool ForCosmosCanSetProperty( return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.PropertyName, name, fromDataAnnotation); } + + /// + /// Configures the property that is used to store the partition key. + /// + /// The builder for the entity type being configured. + /// The name of the partition key property. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ForCosmosHasPartitionKey( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name) + { + entityTypeBuilder.Metadata.SetCosmosPartitionKeyPropertyName(name); + + return entityTypeBuilder; + } + + /// + /// Configures the property that is used to store the partition key. + /// + /// The builder for the entity type being configured. + /// The name of the partition key property. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ForCosmosHasPartitionKey( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name) + where TEntity : class + { + entityTypeBuilder.Metadata.SetCosmosPartitionKeyPropertyName(name); + + return entityTypeBuilder; + } + + /// + /// Configures the property that is used to store the partition key. + /// + /// The builder for the entity type being configured. + /// The partition key property. + /// The same builder instance so that multiple calls can be chained. + public static EntityTypeBuilder ForCosmosHasPartitionKey( + [NotNull] this EntityTypeBuilder entityTypeBuilder, + [NotNull] Expression> propertyExpression) + where TEntity : class + { + Check.NotNull(propertyExpression, nameof(propertyExpression)); + + entityTypeBuilder.Metadata.SetCosmosPartitionKeyPropertyName(propertyExpression.GetPropertyAccess().GetSimpleMemberName()); + + return entityTypeBuilder; + } + + /// + /// Configures the property that is used to store the partition key. + /// + /// The builder for the entity type being configured. + /// The name of the partition key property. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// null otherwise. + /// + public static IConventionEntityTypeBuilder ForCosmosHasPartitionKey( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, + [CanBeNull] string name, + bool fromDataAnnotation = false) + { + if (!entityTypeBuilder.ForCosmosCanSetPartitionKey(name, fromDataAnnotation)) + { + return null; + } + + entityTypeBuilder.Metadata.SetCosmosPartitionKeyPropertyName(name, fromDataAnnotation); + + return entityTypeBuilder; + } + + /// + /// Returns a value indicating whether the property that is used to store the partition key can be set + /// from the current configuration source + /// + /// The builder for the entity type being configured. + /// The name of the partition key property. + /// Indicates whether the configuration was specified using a data annotation. + /// true if the configuration can be applied. + public static bool ForCosmosCanSetPartitionKey( + [NotNull] this IConventionEntityTypeBuilder entityTypeBuilder, [CanBeNull] string name, bool fromDataAnnotation = false) + { + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); + Check.NullButNotEmpty(name, nameof(name)); + + return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.PartitionKeyName, name, fromDataAnnotation); + } } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs index 0d4aeadf2f6..d2ed57f9bc8 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; @@ -104,5 +105,61 @@ public static void SetCosmosContainingPropertyName( public static ConfigurationSource? GetCosmosContainingPropertyNameConfigurationSource([NotNull] this IConventionEntityType entityType) => entityType.FindAnnotation(CosmosAnnotationNames.PropertyName) ?.GetConfigurationSource(); + + /// + /// Returns the name of the property that is used to store the partition key. + /// + /// The entity type to get the partition key property name for. + /// The name of the partition key property. + public static string GetCosmosPartitionKeyPropertyName([NotNull] this IEntityType entityType) => + entityType[CosmosAnnotationNames.PartitionKeyName] as string; + + /// + /// Sets the name of the property that is used to store the partition key key. + /// + /// The entity type to set the partition key property name for. + /// The name to set. + public static void SetCosmosPartitionKeyPropertyName([NotNull] this IMutableEntityType entityType, [CanBeNull] string name) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.PartitionKeyName, + Check.NullButNotEmpty(name, nameof(name))); + + /// + /// Sets the name of the property that is used to store the partition key. + /// + /// The entity type to set the partition key property name for. + /// The name to set. + /// Indicates whether the configuration was specified using a data annotation. + public static void SetCosmosPartitionKeyPropertyName( + [NotNull] this IConventionEntityType entityType, [CanBeNull] string name, bool fromDataAnnotation = false) + => entityType.SetOrRemoveAnnotation( + CosmosAnnotationNames.PartitionKeyName, + Check.NullButNotEmpty(name, nameof(name)), + fromDataAnnotation); + + /// + /// Gets the for the property that is used to store the partition key. + /// + /// The entity type to find configuration source for. + /// The for the partition key property. + public static ConfigurationSource? GetCosmosPartitionKeyPropertyNameConfigurationSource([NotNull] this IConventionEntityType entityType) + => entityType.FindAnnotation(CosmosAnnotationNames.PartitionKeyName) + ?.GetConfigurationSource(); + + /// + /// Returns the store name of the property that is used to store the partition key. + /// + /// The entity type to get the partition key property name for. + /// The name of the partition key property. + public static string GetCosmosPartitionKeyStoreName([NotNull] this IEntityType entityType) + { + var name = entityType.GetCosmosPartitionKeyPropertyName(); + if (name != null) + { + return entityType.FindProperty(name).GetCosmosPropertyName(); + } + + return CosmosClientWrapper.DefaultPartitionKey; + } } } diff --git a/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs index 5f761350608..32da2dc918c 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosModelBuilderExtensions.cs @@ -49,7 +49,7 @@ public static IConventionModelBuilder ForCosmosHasDefaultContainerName( [CanBeNull] string name, bool fromDataAnnotation = false) { - if (modelBuilder.ForCosmosCanSetDefaultContainerName(name, fromDataAnnotation)) + if (!modelBuilder.ForCosmosCanSetDefaultContainerName(name, fromDataAnnotation)) { return null; } diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index 5419e9724a3..a134921f17d 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static IServiceCollection AddEntityFrameworkCosmos([NotNull] this IServic .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index baf264c8b88..69ea6f1e656 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs b/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs index 89d80143214..c0f2e4b04dd 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosModelCustomizer.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/CosmosModelValidator.cs new file mode 100644 index 00000000000..85044c9ecb3 --- /dev/null +++ b/src/EFCore.Cosmos/Infrastructure/CosmosModelValidator.cs @@ -0,0 +1,173 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure +{ + public class CosmosModelValidator : ModelValidator + { + public CosmosModelValidator([NotNull] ModelValidatorDependencies dependencies) + : base(dependencies) + { + } + + /// + /// Validates a model, throwing an exception if any errors are found. + /// + /// The model to validate. + /// The logger to use. + public override void Validate(IModel model, IDiagnosticsLogger logger) + { + base.Validate(model, logger); + + ValidateSharedContainerCompatibility(model, logger); + } + + /// + /// Validates the mapping/configuration of shared containers in the model. + /// + /// The model to validate. + /// The logger to use. + protected virtual void ValidateSharedContainerCompatibility( + [NotNull] IModel model, + [NotNull] IDiagnosticsLogger logger) + { + var containers = new Dictionary>(); + foreach (var entityType in model.GetEntityTypes().Where(et => et.FindPrimaryKey() != null)) + { + var containerName = entityType.GetCosmosContainerName(); + + if (!containers.TryGetValue(containerName, out var mappedTypes)) + { + mappedTypes = new List(); + containers[containerName] = mappedTypes; + } + + mappedTypes.Add(entityType); + } + + foreach (var containerMapping in containers) + { + var mappedTypes = containerMapping.Value; + var containerName = containerMapping.Key; + ValidateSharedContainerCompatibility(mappedTypes, containerName, logger); + } + } + + /// + /// Validates the compatibility of entity types sharing a given container. + /// + /// The mapped entity types. + /// The container name. + /// The logger to use. + protected virtual void ValidateSharedContainerCompatibility( + [NotNull] IReadOnlyList mappedTypes, + [NotNull] string containerName, + [NotNull] IDiagnosticsLogger logger) + { + if (mappedTypes.Count == 1) + { + var entityType = mappedTypes[0]; + var partitionKeyPropertyName = entityType.GetCosmosPartitionKeyPropertyName(); + if (partitionKeyPropertyName != null) + { + var nextPartitionKeyProperty = entityType.FindProperty(partitionKeyPropertyName); + if (nextPartitionKeyProperty == null) + { + throw new InvalidOperationException( + CosmosStrings.PartitionKeyMissingProperty(entityType.DisplayName(), partitionKeyPropertyName)); + } + } + return; + } + + var discriminatorValues = new Dictionary(); + IProperty partitionKey = null; + IEntityType firstEntityType = null; + foreach (var entityType in mappedTypes) + { + var partitionKeyPropertyName = entityType.GetCosmosPartitionKeyPropertyName(); + if (partitionKeyPropertyName != null) + { + var nextPartitionKeyProperty = entityType.FindProperty(partitionKeyPropertyName); + if (nextPartitionKeyProperty == null) + { + throw new InvalidOperationException( + CosmosStrings.PartitionKeyMissingProperty(entityType.DisplayName(), partitionKeyPropertyName)); + } + + if (partitionKey == null) + { + if (firstEntityType != null) + { + throw new InvalidOperationException(CosmosStrings.NoPartitionKey(firstEntityType.DisplayName(), containerName)); + } + partitionKey = nextPartitionKeyProperty; + } + else if (partitionKey.GetCosmosPropertyName() != nextPartitionKeyProperty.GetCosmosPropertyName()) + { + throw new InvalidOperationException( + CosmosStrings.PartitionKeyStoreNameMismatch( + partitionKey.Name, firstEntityType.DisplayName(), partitionKey.GetCosmosPropertyName(), + nextPartitionKeyProperty.Name, entityType.DisplayName(), nextPartitionKeyProperty.GetCosmosPropertyName())); + } + else if ((partitionKey.FindMapping().Converter?.ProviderClrType ?? partitionKey.ClrType) + != (nextPartitionKeyProperty.FindMapping().Converter?.ProviderClrType ?? nextPartitionKeyProperty.ClrType)) + { + throw new InvalidOperationException( + CosmosStrings.PartitionKeyStoreTypeMismatch( + partitionKey.Name, + firstEntityType.DisplayName(), + (partitionKey.FindMapping().Converter?.ProviderClrType ?? partitionKey.ClrType).ShortDisplayName(), + nextPartitionKeyProperty.Name, + entityType.DisplayName(), + (nextPartitionKeyProperty.FindMapping().Converter?.ProviderClrType ?? nextPartitionKeyProperty.ClrType) + .ShortDisplayName())); + } + } + else if (partitionKey != null) + { + throw new InvalidOperationException(CosmosStrings.NoPartitionKey(entityType.DisplayName(), containerName)); + } + + if (firstEntityType == null) + { + firstEntityType = entityType; + } + + if (entityType.ClrType?.IsInstantiable() == true) + { + if (entityType.GetDiscriminatorProperty() == null) + { + throw new InvalidOperationException( + CosmosStrings.NoDiscriminatorProperty(entityType.DisplayName(), containerName)); + } + + var discriminatorValue = entityType.GetDiscriminatorValue(); + if (discriminatorValue == null) + { + throw new InvalidOperationException( + CosmosStrings.NoDiscriminatorValue(entityType.DisplayName(), containerName)); + } + + if (discriminatorValues.TryGetValue(discriminatorValue, out var duplicateEntityType)) + { + throw new InvalidOperationException( + CosmosStrings.DuplicateDiscriminatorValue( + entityType.DisplayName(), discriminatorValue, duplicateEntityType.DisplayName(), containerName)); + } + + discriminatorValues[discriminatorValue] = entityType; + } + } + } + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs index b4bc86ffd9f..2a06957fdca 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs @@ -14,5 +14,6 @@ public static class CosmosAnnotationNames public const string Prefix = "Cosmos:"; public const string ContainerName = Prefix + "ContainerName"; public const string PropertyName = Prefix + "PropertyName"; + public const string PartitionKeyName = Prefix + "PartitionKeyName"; } } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index e9b575b13c2..27e68247745 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -24,6 +24,38 @@ private static readonly ResourceManager _resourceManager public static string CosmosNotInUse => GetString("CosmosNotInUse"); + /// + /// The discriminator value for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type mapped to the container '{container}' needs to have a unique discriminator value. + /// + public static string DuplicateDiscriminatorValue([CanBeNull] object entityType1, [CanBeNull] object discriminatorValue, [CanBeNull] object entityType2, [CanBeNull] object container) + => string.Format( + GetString("DuplicateDiscriminatorValue", nameof(entityType1), nameof(discriminatorValue), nameof(entityType2), nameof(container)), + entityType1, discriminatorValue, entityType2, container); + + /// + /// The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator property configured. + /// + public static string NoDiscriminatorProperty([CanBeNull] object entityType, [CanBeNull] object container) + => string.Format( + GetString("NoDiscriminatorProperty", nameof(entityType), nameof(container)), + entityType, container); + + /// + /// The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator value configured. + /// + public static string NoDiscriminatorValue([CanBeNull] object entityType, [CanBeNull] object container) + => string.Format( + GetString("NoDiscriminatorValue", nameof(entityType), nameof(container)), + entityType, container); + + /// + /// The entity type '{entityType}' does not have a partition key set, but it is mapped to the collection '{collection}' shared by entity types with partition keys. + /// + public static string NoPartitionKey([CanBeNull] object entityType, [CanBeNull] object collection) + => string.Format( + GetString("NoPartitionKey", nameof(entityType), nameof(collection)), + entityType, collection); + /// /// 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. /// @@ -40,6 +72,30 @@ public static string OrphanedNestedDocumentSensitive([CanBeNull] object entityTy GetString("OrphanedNestedDocumentSensitive", nameof(entityType), nameof(missingEntityType), nameof(keyValue)), entityType, missingEntityType, keyValue); + /// + /// The partition key for entity type '{entityType}' is set to '{property}', but there is no property with that name. + /// + public static string PartitionKeyMissingProperty([CanBeNull] object entityType, [CanBeNull] object property) + => string.Format( + GetString("PartitionKeyMissingProperty", nameof(entityType), nameof(property)), + entityType, property); + + /// + /// The partition key property '{property1}' on '{entityType1}' is mapped as '{storeName1}', but the partition key property '{property2}' on '{entityType2}' is mapped as '{storeName2}'. All partition key properties need to be mapped to the same store property. + /// + public static string PartitionKeyStoreNameMismatch([CanBeNull] object property1, [CanBeNull] object entityType1, [CanBeNull] object storeName1, [CanBeNull] object property2, [CanBeNull] object entityType2, [CanBeNull] object storeName2) + => string.Format( + GetString("PartitionKeyStoreNameMismatch", nameof(property1), nameof(entityType1), nameof(storeName1), nameof(property2), nameof(entityType2), nameof(storeName2)), + property1, entityType1, storeName1, property2, entityType2, storeName2); + + /// + /// The type of the partition key property '{property1}' on '{entityType1}' is '{propertyType1}', but the type of the partition key property '{property2}' on '{entityType2}' is '{propertyType2}'. All partition key properties need to have matching types. + /// + public static string PartitionKeyStoreTypeMismatch([CanBeNull] object property1, [CanBeNull] object entityType1, [CanBeNull] object propertyType1, [CanBeNull] object property2, [CanBeNull] object entityType2, [CanBeNull] object propertyType2) + => string.Format( + GetString("PartitionKeyStoreTypeMismatch", nameof(property1), nameof(entityType1), nameof(propertyType1), nameof(property2), nameof(entityType2), nameof(propertyType2)), + property1, entityType1, propertyType1, property2, entityType2, propertyType2); + /// /// No matching discriminator values where found for this instance of '{entityType}'. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index 8d2c6d30923..d61eb59f30b 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -120,12 +120,33 @@ Cosmos-specific methods can only be used when the context is using the Cosmos provider. + + The discriminator value for '{entityType1}' is '{discriminatorValue}' which is the same for '{entityType2}'. Every concrete entity type mapped to the container '{container}' needs to have a unique discriminator value. + + + The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator property configured. + + + The entity type '{entityType}' is sharing the container '{container}' with other types, but does not have a discriminator value configured. + + + The entity type '{entityType}' does not have a partition key set, but it is mapped to the collection '{collection}' shared by entity types with partition keys. + 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 partition key for entity type '{entityType}' is set to '{property}', but there is no property with that name. + + + The partition key property '{property1}' on '{entityType1}' is mapped as '{storeName1}', but the partition key property '{property2}' on '{entityType2}' is mapped as '{storeName2}'. All partition key properties need to be mapped to the same store property. + + + The type of the partition key property '{property1}' on '{entityType1}' is '{propertyType1}', but the type of the partition key property '{property2}' on '{entityType2}' is '{propertyType2}'. All partition key properties need to have matching types. + No matching discriminator values where found for this instance of '{entityType}'. diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 228aa07a544..4b63570e406 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -39,6 +39,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal public class CosmosClientWrapper { public static readonly JsonSerializer Serializer = new JsonSerializer(); + public static readonly string DefaultPartitionKey = "__partitionKey"; private readonly SingletonCosmosClientWrapper _singletonWrapper; private readonly string _databaseId; @@ -145,25 +146,27 @@ private async Task CreateContainerIfNotExistsOnceAsync( public bool CreateItem( string containerId, - JToken document) + JToken document, + object partitionKey) => _executionStrategyFactory.Create().Execute( - (containerId, document), CreateItemOnce, null); + (containerId, document, partitionKey), CreateItemOnce, null); private bool CreateItemOnce( DbContext context, - (string ContainerId, JToken Document) parameters) + (string ContainerId, JToken Document, object PartitionKey) parameters) => CreateItemOnceAsync(context, parameters).GetAwaiter().GetResult(); public Task CreateItemAsync( string containerId, JToken document, + object partitionKey, CancellationToken cancellationToken = default) => _executionStrategyFactory.Create().ExecuteAsync( - (containerId, document), CreateItemOnceAsync, null, cancellationToken); + (containerId, document, partitionKey), CreateItemOnceAsync, null, cancellationToken); private async Task CreateItemOnceAsync( DbContext _, - (string ContainerId, JToken Document) parameters, + (string ContainerId, JToken Document, object PartitionKey) parameters, CancellationToken cancellationToken = default) { using (var stream = new MemoryStream()) @@ -174,7 +177,8 @@ private async Task CreateItemOnceAsync( await jsonWriter.FlushAsync(cancellationToken); var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - using (var response = await container.CreateItemStreamAsync(PartitionKey.NonePartitionKeyValue, stream, null, cancellationToken)) + var partitionKey = CreatePartitionKey(parameters.PartitionKey); + using (var response = await container.CreateItemStreamAsync(partitionKey, stream, null, cancellationToken)) { return response.StatusCode == HttpStatusCode.Created; } @@ -184,38 +188,41 @@ private async Task CreateItemOnceAsync( public bool ReplaceItem( string collectionId, string documentId, - JObject document) + JObject document, + object partitionKey) => _executionStrategyFactory.Create().Execute( - (collectionId, documentId, document), ReplaceItemOnce, null); + (collectionId, documentId, document, partitionKey), ReplaceItemOnce, null); private bool ReplaceItemOnce( DbContext context, - (string, string, JObject) parameters) + (string ContainerId, string ItemId, JObject Document, object PartitionKey) parameters) => ReplaceItemOnceAsync(context, parameters).GetAwaiter().GetResult(); public Task ReplaceItemAsync( string collectionId, string documentId, JObject document, + object partitionKey, CancellationToken cancellationToken = default) => _executionStrategyFactory.Create().ExecuteAsync( - (collectionId, documentId, document), ReplaceItemOnceAsync, null, cancellationToken); + (collectionId, documentId, document, partitionKey), ReplaceItemOnceAsync, null, cancellationToken); private async Task ReplaceItemOnceAsync( DbContext _, - (string ContainerId, string ItemId, JObject Document) parameters, + (string ContainerId, string ItemId, JObject Document, object PartitionKey) parameters, CancellationToken cancellationToken = default) { - using (var stream = new MemoryStream()) - using (var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: false)) + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: false); using (var jsonWriter = new JsonTextWriter(writer)) { JsonSerializer.Create().Serialize(jsonWriter, parameters.Document); await jsonWriter.FlushAsync(cancellationToken); var container = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); + var partitionKey = CreatePartitionKey(parameters.PartitionKey); using (var response = await container.ReplaceItemStreamAsync( - PartitionKey.NonePartitionKeyValue, parameters.ItemId, stream, null, cancellationToken)) + partitionKey, parameters.ItemId, stream, null, cancellationToken)) { return response.StatusCode == HttpStatusCode.OK; } @@ -224,34 +231,42 @@ private async Task ReplaceItemOnceAsync( public bool DeleteItem( string containerId, - string documentId) + string documentId, + object partitionKey) => _executionStrategyFactory.Create().Execute( - (containerId, documentId), DeleteItemOnce, null); + (containerId, documentId, partitionKey), DeleteItemOnce, null); public bool DeleteItemOnce( DbContext context, - (string ContainerId, string DocumentId) parameters) + (string ContainerId, string DocumentId, object PartitionKey) parameters) => DeleteItemOnceAsync(context, parameters).GetAwaiter().GetResult(); public Task DeleteItemAsync( string containerId, string documentId, + object partitionKey, CancellationToken cancellationToken = default) => _executionStrategyFactory.Create().ExecuteAsync( - (containerId, documentId), DeleteItemOnceAsync, null, cancellationToken); + (containerId, documentId, partitionKey), DeleteItemOnceAsync, null, cancellationToken); public async Task DeleteItemOnceAsync( DbContext _, - (string ContainerId, string DocumentId) parameters, + (string ContainerId, string DocumentId, object PartitionKey) parameters, CancellationToken cancellationToken = default) { var items = Client.GetDatabase(_databaseId).GetContainer(parameters.ContainerId); - using (var response = await items.DeleteItemStreamAsync(PartitionKey.NonePartitionKeyValue, parameters.DocumentId, null, cancellationToken)) + var partitionKey = CreatePartitionKey(parameters.PartitionKey); + using (var response = await items.DeleteItemStreamAsync(partitionKey, parameters.DocumentId, null, cancellationToken)) { return response.StatusCode == HttpStatusCode.NoContent; } } + private PartitionKey CreatePartitionKey(object partitionKey) + => partitionKey == null + ? PartitionKey.NonePartitionKeyValue + : new PartitionKey(partitionKey); + public IEnumerable ExecuteSqlQuery( string containerId, [NotNull] CosmosSqlQuery query) @@ -281,7 +296,7 @@ private FeedIterator CreateQuery( queryDefinition.UseParameter(parameter.Name, parameter.Value); } - return container.CreateItemQueryStream(queryDefinition, maxConcurrency: 1, PartitionKey.NonePartitionKeyValue); + return container.CreateItemQueryStream(queryDefinition, maxConcurrency: 1); } private class DocumentEnumerable : IEnumerable diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index fdc654aadef..b9d5cdb59df 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -34,7 +34,9 @@ public bool EnsureCreated() var created = _cosmosClient.CreateDatabaseIfNotExists(); foreach (var entityType in _model.GetEntityTypes()) { - created |= _cosmosClient.CreateContainerIfNotExists(entityType.GetCosmosContainerName(), "__partitionKey"); + created |= _cosmosClient.CreateContainerIfNotExists( + entityType.GetCosmosContainerName(), + entityType.GetCosmosPartitionKeyStoreName()); } if (created) @@ -60,7 +62,10 @@ public async Task EnsureCreatedAsync(CancellationToken cancellationToken = var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(cancellationToken); foreach (var entityType in _model.GetEntityTypes()) { - created |= await _cosmosClient.CreateContainerIfNotExistsAsync(entityType.GetCosmosContainerName(), "__partitionKey", cancellationToken); + created |= await _cosmosClient.CreateContainerIfNotExistsAsync( + entityType.GetCosmosContainerName(), + entityType.GetCosmosPartitionKeyStoreName(), + cancellationToken); } if (created) diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs index ac9852a48d3..505b65bc8a7 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs @@ -172,8 +172,8 @@ private bool Save(IUpdateEntry entry) { case EntityState.Added: var newDocument = documentSource.CreateDocument(entry); - newDocument["__partitionKey"] = "0"; - return _cosmosClient.CreateItem(collectionId, newDocument); + + return _cosmosClient.CreateItem(collectionId, newDocument, GetPartitionKey(entry)); case EntityState.Modified: var jObjectProperty = entityType.FindProperty(StoreKeyConvention.JObjectPropertyName); var document = jObjectProperty != null @@ -189,16 +189,15 @@ private bool Save(IUpdateEntry entry) else { document = documentSource.CreateDocument(entry); - document["__partitionKey"] = "0"; document[entityType.GetDiscriminatorProperty().GetCosmosPropertyName()] = JToken.FromObject(entityType.GetDiscriminatorValue(), CosmosClientWrapper.Serializer); } return _cosmosClient.ReplaceItem( - collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document); + collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document, GetPartitionKey(entry)); case EntityState.Deleted: - return _cosmosClient.DeleteItem(collectionId, documentSource.GetId(entry)); + return _cosmosClient.DeleteItem(collectionId, documentSource.GetId(entry), GetPartitionKey(entry)); default: return false; } @@ -228,8 +227,7 @@ private Task SaveAsync(IUpdateEntry entry, CancellationToken cancellationT { case EntityState.Added: var newDocument = documentSource.CreateDocument(entry); - newDocument["__partitionKey"] = "0"; - return _cosmosClient.CreateItemAsync(collectionId, newDocument, cancellationToken); + return _cosmosClient.CreateItemAsync(collectionId, newDocument, GetPartitionKey(entry), cancellationToken); case EntityState.Modified: var jObjectProperty = entityType.FindProperty(StoreKeyConvention.JObjectPropertyName); var document = jObjectProperty != null @@ -245,16 +243,15 @@ private Task SaveAsync(IUpdateEntry entry, CancellationToken cancellationT else { document = documentSource.CreateDocument(entry); - document["__partitionKey"] = "0"; document[entityType.GetDiscriminatorProperty().GetCosmosPropertyName()] = JToken.FromObject(entityType.GetDiscriminatorValue(), CosmosClientWrapper.Serializer); } return _cosmosClient.ReplaceItemAsync( - collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document, cancellationToken); + collectionId, documentSource.GetId(entry.SharedIdentityEntry ?? entry), document, GetPartitionKey(entry), cancellationToken); case EntityState.Deleted: - return _cosmosClient.DeleteItemAsync(collectionId, documentSource.GetId(entry), cancellationToken); + return _cosmosClient.DeleteItemAsync(collectionId, documentSource.GetId(entry), GetPartitionKey(entry), cancellationToken); default: return Task.FromResult(false); } @@ -295,5 +292,24 @@ private IUpdateEntry GetRootDocument(InternalEntityEntry entry) return principal.EntityType.IsDocumentRoot() ? principal : GetRootDocument(principal); } + + private static object GetPartitionKey(IUpdateEntry entry) + { + object partitionKey = null; + var partitionKeyPropertyName = entry.EntityType.GetCosmosPartitionKeyPropertyName(); + if (partitionKeyPropertyName != null) + { + var partitionKeyProperty = entry.EntityType.FindProperty(partitionKeyPropertyName); + partitionKey = entry.GetCurrentValue(partitionKeyProperty); + + var converter = partitionKeyProperty.FindMapping().Converter; + if (converter != null) + { + partitionKey = converter.ConvertToProvider(partitionKey); + } + } + + return partitionKey; + } } } diff --git a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs index c8e2ea98067..0ad41ae1ec1 100644 --- a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs +++ b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs @@ -6,6 +6,7 @@ using System.Text; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.ValueGeneration; namespace Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal @@ -17,19 +18,21 @@ public class IdValueGenerator : ValueGenerator protected override object NextValue([NotNull] EntityEntry entry) { var builder = new StringBuilder(); + var entityType = entry.Metadata; - var pk = entry.Metadata.FindPrimaryKey(); - var discriminator = entry.Metadata.GetDiscriminatorValue(); + var pk = entityType.FindPrimaryKey(); + var discriminator = entityType.GetDiscriminatorValue(); if (discriminator != null - && !pk.Properties.Contains(entry.Metadata.GetDiscriminatorProperty())) + && !pk.Properties.Contains(entityType.GetDiscriminatorProperty())) { - AppendString(builder,discriminator); + AppendString(builder, discriminator); builder.Append("|"); } + var partitionKey = entityType.GetCosmosPartitionKeyPropertyName() ?? CosmosClientWrapper.DefaultPartitionKey; foreach (var property in pk.Properties) { - if (property.Name == "__partitionKey") + if (property.Name == partitionKey) { continue; } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 407432cc42d..33b313d8add 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -297,11 +297,9 @@ protected virtual void ValidateSharedTableCompatibility( } private static bool IsIdentifyingPrincipal(IEntityType dependentEntityType, IEntityType principalEntityType) - { - return dependentEntityType.FindForeignKeys(dependentEntityType.FindPrimaryKey().Properties) + => dependentEntityType.FindForeignKeys(dependentEntityType.FindPrimaryKey().Properties) .Any(fk => fk.PrincipalKey.IsPrimaryKey() && fk.PrincipalEntityType == principalEntityType); - } /// /// Validates the compatibility of properties sharing columns in a given table. diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index e64366bcf37..0fe6591cc90 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -916,7 +916,11 @@ void IMutableModel.AddIgnored(string name) => AddIgnored(name, ConfigurationSource.Explicit); /// - IConventionModelBuilder IConventionModel.Builder => Builder; + IConventionModelBuilder IConventionModel.Builder + { + [DebuggerStepThrough] + get => Builder; + } /// IConventionEntityType IConventionModel.FindEntityType(string name) => FindEntityType(name); diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index 133f79b5f0f..8a2833d0895 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -125,6 +125,7 @@ private class Customer { public int Id { get; set; } public string Name { get; set; } + public int PartitionKey { get; set; } } private class CustomerContext : DbContext @@ -209,6 +210,70 @@ public async Task Can_add_update_delete_detached_entity_end_to_end_async() } } + [ConditionalFact] + public void Can_add_update_delete_end_to_end_with_partition_key() + { + using (var testDatabase = CosmosTestStore.CreateInitialized(DatabaseName)) + { + var options = Fixture.CreateOptions(testDatabase); + + var customer = new Customer { Id = 42, Name = "Theon", PartitionKey = 1 }; + + using (var context = new PartitionKeyContext(options)) + { + context.Database.EnsureCreated(); + + context.Add(customer); + + context.SaveChanges(); + } + + using (var context = new PartitionKeyContext(options)) + { + var customerFromStore = context.Set().Single(); + + Assert.Equal(42, customerFromStore.Id); + Assert.Equal("Theon", customerFromStore.Name); + Assert.Equal(1, customerFromStore.PartitionKey); + + customerFromStore.Name = "Theon Greyjoy"; + + context.SaveChanges(); + } + + using (var context = new PartitionKeyContext(options)) + { + var customerFromStore = context.Set().Single(); + + Assert.Equal(42, customerFromStore.Id); + Assert.Equal("Theon Greyjoy", customerFromStore.Name); + Assert.Equal(1, customerFromStore.PartitionKey); + + context.Remove(customerFromStore); + + context.SaveChanges(); + } + + using (var context = new PartitionKeyContext(options)) + { + Assert.Equal(0, context.Set().Count()); + } + } + } + + private class PartitionKeyContext : DbContext + { + public PartitionKeyContext(DbContextOptions dbContextOptions) + : base(dbContextOptions) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ForCosmosHasPartitionKey(c => c.PartitionKey); + } + } + [ConditionalFact] public void Can_update_unmapped_properties() { diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs index ef4ae3998eb..1b20306bd74 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.cs @@ -435,6 +435,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Order"")"); } + [ConditionalTheory(Skip = "Issue #12086")] public override async Task Where_subquery_anon(bool isAsync) { await base.Where_subquery_anon(isAsync); diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 16a21f46792..bfdcb99bf22 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -20,7 +20,8 @@ public class CosmosTestStore : TestStore private readonly string _dataFilePath; private readonly Action _configureCosmos; - public static CosmosTestStore Create(string name, Action extensionConfiguration = null) => new CosmosTestStore(name, shared: false, extensionConfiguration: extensionConfiguration); + public static CosmosTestStore Create(string name, Action extensionConfiguration = null) + => new CosmosTestStore(name, shared: false, extensionConfiguration: extensionConfiguration); public static CosmosTestStore CreateInitialized(string name, Action extensionConfiguration = null) => (CosmosTestStore)Create(name, extensionConfiguration).Initialize(null, (Func)null, null, null); @@ -112,9 +113,11 @@ private async Task CreateFromFile(DbContext context) if (reader.TokenType == JsonToken.StartObject) { var document = serializer.Deserialize(reader); + + document["id"] = $"{entityName}|{document["id"]}"; document["Discriminator"] = entityName; - await cosmosClient.CreateItemAsync(entityName, document); + await cosmosClient.CreateItemAsync("NorthwindContext", document, null); } else if (reader.TokenType == JsonToken.EndObject) { diff --git a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs new file mode 100644 index 00000000000..019a8409b96 --- /dev/null +++ b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs @@ -0,0 +1,144 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.TestUtilities; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Infrastructure +{ + public class CosmosModelValidatorTest : ModelValidatorTestBase + { + [ConditionalFact] + public virtual void Passes_on_valid_model() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity(); + + var model = modelBuilder.Model; + Validate(model); + } + + [ConditionalFact] + public virtual void Passes_on_valid_partition_keys() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + + var model = modelBuilder.Model; + Validate(model); + } + + [ConditionalFact] + public virtual void Detects_missing_partition_key_property() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosHasPartitionKey("PartitionKey"); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.PartitionKeyMissingProperty(typeof(Order).Name, "PartitionKey"), model); + } + + [ConditionalFact] + public virtual void Detects_missing_partition_key_on_first_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders"); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.NoPartitionKey(typeof(Customer).Name, "Orders"), model); + } + + [ConditionalFact] + public virtual void Detects_missing_partition_keys_one_last_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + modelBuilder.Entity().ForCosmosToContainer("Orders"); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.NoPartitionKey(typeof(Order).Name, "Orders"), model); + } + + [ConditionalFact] + public virtual void Detects_partition_keys_mapped_to_different_properties() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId) + .Property(c => c.PartitionId).ForCosmosToProperty("pk"); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.PartitionKeyStoreNameMismatch( + nameof(Customer.PartitionId), typeof(Customer).Name, "pk", nameof(Order.PartitionId), typeof(Order).Name, nameof(Order.PartitionId)), model); + } + + [ConditionalFact] + public virtual void Detects_partition_key_of_different_type() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId); + modelBuilder.Entity().ForCosmosToContainer("Orders").ForCosmosHasPartitionKey(c => c.PartitionId) + .Property(c => c.PartitionId).HasConversion(); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.PartitionKeyStoreTypeMismatch( + nameof(Customer.PartitionId), typeof(Customer).Name, "int", nameof(Order.PartitionId), typeof(Order).Name, "string"), model); + } + + [ConditionalFact] + public virtual void Detects_missing_discriminator() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").HasNoDiscriminator(); + modelBuilder.Entity().ForCosmosToContainer("Orders"); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.NoDiscriminatorProperty(typeof(Customer).Name, "Orders"), model); + } + + [ConditionalFact] + public virtual void Detects_missing_discriminator_value() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").HasDiscriminator().HasValue(null); + modelBuilder.Entity().ForCosmosToContainer("Orders"); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.NoDiscriminatorValue(typeof(Customer).Name, "Orders"), model); + } + + [ConditionalFact] + public virtual void Detects_duplicate_discriminator_values() + { + var modelBuilder = CreateConventionalModelBuilder(); + modelBuilder.Entity().ForCosmosToContainer("Orders").HasDiscriminator().HasValue("type"); + modelBuilder.Entity().ForCosmosToContainer("Orders").HasDiscriminator().HasValue("type"); + + var model = modelBuilder.Model; + VerifyError(CosmosStrings.DuplicateDiscriminatorValue(typeof(Order).Name, "type", typeof(Customer).Name, "Orders"), model); + } + + protected override TestHelpers TestHelpers => CosmosTestHelpers.Instance; + + private class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public int PartitionId { get; set; } + public ICollection Orders { get; set; } + } + + private class Order + { + public int Id { get; set; } + public int PartitionId { get; set; } + public Customer Customer { get; set; } + } + } +} diff --git a/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs index 544263b7639..a4e55f663d1 100644 --- a/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs +++ b/test/EFCore.Cosmos.Tests/Metadata/CosmosBuilderExtensionsTest.cs @@ -9,6 +9,51 @@ namespace Microsoft.EntityFrameworkCore.Metadata { public class CosmosBuilderExtensionsTest { + [ConditionalFact] + public void Can_get_and_set_collection_name() + { + var modelBuilder = CreateConventionModelBuilder(); + + var entityType = modelBuilder + .Entity(); + + Assert.Equal(nameof(Customer), entityType.Metadata.GetCosmosContainerName()); + + entityType.ForCosmosToContainer("Customizer"); + Assert.Equal("Customizer", entityType.Metadata.GetCosmosContainerName()); + + entityType.ForCosmosToContainer(null); + Assert.Equal(nameof(Customer), entityType.Metadata.GetCosmosContainerName()); + + modelBuilder.ForCosmosHasDefaultContainerName("Unicorn"); + Assert.Equal("Unicorn", entityType.Metadata.GetCosmosContainerName()); + } + + [ConditionalFact] + public void Can_get_and_set_partition_key_name() + { + var modelBuilder = CreateConventionModelBuilder(); + + var entityTypeBuilder = modelBuilder.Entity(); + var entityType = entityTypeBuilder.Metadata; + + ((IConventionEntityType)entityType).Builder.ForCosmosHasPartitionKey("pk"); + Assert.Equal("pk", entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Equal(ConfigurationSource.Convention, + ((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); + + entityTypeBuilder.ForCosmosHasPartitionKey("pk"); + Assert.Equal("pk", entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Equal(ConfigurationSource.Explicit, + ((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); + + Assert.False(((IConventionEntityType)entityType).Builder.ForCosmosCanSetPartitionKey("partition")); + + entityTypeBuilder.ForCosmosHasPartitionKey(null); + Assert.Null(entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Null(((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); + } + [ConditionalFact] public void Default_container_name_is_used_if_not_set() { diff --git a/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs b/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs index 4d0ed4a6a80..03972e367a3 100644 --- a/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs +++ b/test/EFCore.Cosmos.Tests/Metadata/CosmosMetadataExtensionsTest.cs @@ -13,23 +13,54 @@ public class CosmosMetadataExtensionsTest [ConditionalFact] public void Can_get_and_set_collection_name() { - var modelBuilder = new ModelBuilder(new ConventionSet()); + var modelBuilder = CreateModelBuilder(); var entityType = modelBuilder - .Entity(); + .Entity().Metadata; - Assert.Equal(nameof(Customer), entityType.Metadata.GetCosmosContainerName()); + Assert.Equal(nameof(Customer), entityType.GetCosmosContainerName()); - entityType.ForCosmosToContainer("Customizer"); - Assert.Equal("Customizer", entityType.Metadata.GetCosmosContainerName()); + ((IConventionEntityType)entityType).SetCosmosContainerName("Customizer"); + Assert.Equal("Customizer", entityType.GetCosmosContainerName()); + Assert.Equal(ConfigurationSource.Convention, ((IConventionEntityType)entityType).GetCosmosContainerNameConfigurationSource()); - entityType.ForCosmosToContainer(null); - Assert.Equal(nameof(Customer), entityType.Metadata.GetCosmosContainerName()); + entityType.SetCosmosContainerName("Customizer"); + Assert.Equal("Customizer", entityType.GetCosmosContainerName()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionEntityType)entityType).GetCosmosContainerNameConfigurationSource()); - modelBuilder.ForCosmosHasDefaultContainerName("Unicorn"); - Assert.Equal("Unicorn", entityType.Metadata.GetCosmosContainerName()); + entityType.SetCosmosContainerName(null); + Assert.Equal(nameof(Customer), entityType.GetCosmosContainerName()); + Assert.Null(((IConventionEntityType)entityType).GetCosmosContainerNameConfigurationSource()); + + ((IConventionModel)modelBuilder.Model).Builder.ForCosmosHasDefaultContainerName("Unicorn"); + Assert.Equal("Unicorn", entityType.GetCosmosContainerName()); + } + + [ConditionalFact] + public void Can_get_and_set_partition_key_name() + { + var modelBuilder = CreateModelBuilder(); + + var entityType = modelBuilder + .Entity().Metadata; + + Assert.Null(entityType.GetCosmosPartitionKeyPropertyName()); + + ((IConventionEntityType)entityType).SetCosmosPartitionKeyPropertyName("pk"); + Assert.Equal("pk", entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Equal(ConfigurationSource.Convention, ((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); + + entityType.SetCosmosPartitionKeyPropertyName("pk"); + Assert.Equal("pk", entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Equal(ConfigurationSource.Explicit, ((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); + + entityType.SetCosmosPartitionKeyPropertyName(null); + Assert.Null(entityType.GetCosmosPartitionKeyPropertyName()); + Assert.Null(((IConventionEntityType)entityType).GetCosmosPartitionKeyPropertyNameConfigurationSource()); } + private static ModelBuilder CreateModelBuilder() => new ModelBuilder(new ConventionSet()); + private class Customer { public int Id { get; set; }