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; }