diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs b/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs
index c1580503337..6de27fd0a63 100644
--- a/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs
+++ b/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs
@@ -34,7 +34,11 @@ private enum Id
ExecutedReadItem,
ExecutedCreateItem,
ExecutedReplaceItem,
- ExecutedDeleteItem
+ ExecutedDeleteItem,
+
+ // Model validation events
+ NoPartitionKeyDefined = CoreEventId.ProviderBaseId + 600,
+
}
private static readonly string DatabasePrefix = DbLoggerCategory.Database.Name + ".";
@@ -149,4 +153,21 @@ public static readonly EventId ExecutedReplaceItem
///
public static readonly EventId ExecutedDeleteItem
= new((int)Id.ExecutedDeleteItem, CommandPrefix + Id.ExecutedDeleteItem);
+
+ private static EventId MakeValidationId(Id id)
+ => new((int)id, DbLoggerCategory.Model.Validation.Name + "." + id);
+
+ ///
+ /// No partition key has been configured for an entity type. It is highly recommended that an appropriate partition key be defined.
+ /// See https://aka.ms/efdocs-cosmos-partition-keys for more information.
+ ///
+ ///
+ ///
+ /// This event is in the category.
+ ///
+ ///
+ /// This event uses the payload when used with a .
+ ///
+ ///
+ public static readonly EventId NoPartitionKeyDefined = MakeValidationId(Id.NoPartitionKeyDefined);
}
diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosItemCommandExecutedEventData.cs b/src/EFCore.Cosmos/Diagnostics/CosmosItemCommandExecutedEventData.cs
index f342eac04f0..f6865b7ea4d 100644
--- a/src/EFCore.Cosmos/Diagnostics/CosmosItemCommandExecutedEventData.cs
+++ b/src/EFCore.Cosmos/Diagnostics/CosmosItemCommandExecutedEventData.cs
@@ -21,7 +21,7 @@ public class CosmosItemCommandExecutedEventData : EventData
/// The activity ID.
/// The ID of the resource being read.
/// The ID of the Cosmos container being queried.
- /// The key of the Cosmos partition that the query is using.
+ /// The key of the Cosmos partition that the query is using.
/// Indicates whether the application allows logging of sensitive data.
public CosmosItemCommandExecutedEventData(
EventDefinitionBase eventDefinition,
@@ -31,7 +31,7 @@ public CosmosItemCommandExecutedEventData(
string activityId,
string containerId,
string resourceId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
bool logSensitiveData)
: base(eventDefinition, messageGenerator)
{
@@ -40,7 +40,7 @@ public CosmosItemCommandExecutedEventData(
ActivityId = activityId;
ContainerId = containerId;
ResourceId = resourceId;
- PartitionKey = partitionKey;
+ PartitionKeyValue = partitionKeyValue;
LogSensitiveData = logSensitiveData;
}
@@ -72,7 +72,7 @@ public CosmosItemCommandExecutedEventData(
///
/// The key of the Cosmos partition that the query is using.
///
- public virtual string? PartitionKey { get; }
+ public virtual PartitionKey PartitionKeyValue { get; }
///
/// Indicates whether the application allows logging of sensitive data.
diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosQueryEventData.cs b/src/EFCore.Cosmos/Diagnostics/CosmosQueryEventData.cs
index 2d6a56fbaad..fca1bea3489 100644
--- a/src/EFCore.Cosmos/Diagnostics/CosmosQueryEventData.cs
+++ b/src/EFCore.Cosmos/Diagnostics/CosmosQueryEventData.cs
@@ -17,7 +17,7 @@ public class CosmosQueryEventData : EventData
/// The event definition.
/// A delegate that generates a log message for this event.
/// The ID of the Cosmos container being queried.
- /// The key of the Cosmos partition that the query is using.
+ /// The key value of the Cosmos partition that the query is using.
/// Name/values for each parameter in the Cosmos Query.
/// The SQL representing the query.
/// Indicates whether the application allows logging of sensitive data.
@@ -25,14 +25,14 @@ public CosmosQueryEventData(
EventDefinitionBase eventDefinition,
Func messageGenerator,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
IReadOnlyList<(string Name, object? Value)> parameters,
string querySql,
bool logSensitiveData)
: base(eventDefinition, messageGenerator)
{
ContainerId = containerId;
- PartitionKey = partitionKey;
+ PartitionKeyValue = partitionKeyValue;
Parameters = parameters;
QuerySql = querySql;
LogSensitiveData = logSensitiveData;
@@ -46,7 +46,7 @@ public CosmosQueryEventData(
///
/// The key of the Cosmos partition that the query is using.
///
- public virtual string? PartitionKey { get; }
+ public virtual PartitionKey PartitionKeyValue { get; }
///
/// Name/values for each parameter in the Cosmos Query.
diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosQueryExecutedEventData.cs b/src/EFCore.Cosmos/Diagnostics/CosmosQueryExecutedEventData.cs
index 621ce34aae3..2934ac9e71f 100644
--- a/src/EFCore.Cosmos/Diagnostics/CosmosQueryExecutedEventData.cs
+++ b/src/EFCore.Cosmos/Diagnostics/CosmosQueryExecutedEventData.cs
@@ -20,7 +20,7 @@ public class CosmosQueryExecutedEventData : EventData
/// The request charge in RU.
/// The activity ID.
/// The ID of the Cosmos container being queried.
- /// The key of the Cosmos partition that the query is using.
+ /// The key value of the Cosmos partition that the query is using.
/// Name/values for each parameter in the Cosmos Query.
/// The SQL representing the query.
/// Indicates whether the application allows logging of sensitive data.
@@ -31,7 +31,7 @@ public CosmosQueryExecutedEventData(
double requestCharge,
string activityId,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
IReadOnlyList<(string Name, object? Value)> parameters,
string querySql,
bool logSensitiveData)
@@ -41,7 +41,7 @@ public CosmosQueryExecutedEventData(
RequestCharge = requestCharge;
ActivityId = activityId;
ContainerId = containerId;
- PartitionKey = partitionKey;
+ PartitionKeyValue = partitionKeyValue;
Parameters = parameters;
QuerySql = querySql;
LogSensitiveData = logSensitiveData;
@@ -70,7 +70,7 @@ public CosmosQueryExecutedEventData(
///
/// The key of the Cosmos partition that the query is using.
///
- public virtual string? PartitionKey { get; }
+ public virtual PartitionKey PartitionKeyValue { get; }
///
/// Name/values for each parameter in the Cosmos Query.
diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosReadItemEventData.cs b/src/EFCore.Cosmos/Diagnostics/CosmosReadItemEventData.cs
index b6c3b4edc24..a782e0c9b28 100644
--- a/src/EFCore.Cosmos/Diagnostics/CosmosReadItemEventData.cs
+++ b/src/EFCore.Cosmos/Diagnostics/CosmosReadItemEventData.cs
@@ -18,20 +18,20 @@ public class CosmosReadItemEventData : EventData
/// A delegate that generates a log message for this event.
/// The ID of the resource being read.
/// The ID of the Cosmos container being queried.
- /// The key of the Cosmos partition that the query is using.
+ /// The key value of the Cosmos partition that the query is using.
/// Indicates whether the application allows logging of sensitive data.
public CosmosReadItemEventData(
EventDefinitionBase eventDefinition,
Func messageGenerator,
string resourceId,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
bool logSensitiveData)
: base(eventDefinition, messageGenerator)
{
ResourceId = resourceId;
ContainerId = containerId;
- PartitionKey = partitionKey;
+ PartitionKeyValue = partitionKeyValue;
LogSensitiveData = logSensitiveData;
}
@@ -48,7 +48,7 @@ public CosmosReadItemEventData(
///
/// The key of the Cosmos partition that the query is using.
///
- public virtual string? PartitionKey { get; }
+ public virtual PartitionKey PartitionKeyValue { get; }
///
/// Indicates whether the application allows logging of sensitive data.
diff --git a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs
index 75834a6465c..32e4f91c1f5 100644
--- a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs
+++ b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs
@@ -54,7 +54,7 @@ public static void SyncNotSupported(
public static void ExecutingSqlQuery(
this IDiagnosticsLogger diagnostics,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery cosmosSqlQuery)
{
var definition = CosmosResources.LogExecutingSqlQuery(diagnostics);
@@ -66,7 +66,7 @@ public static void ExecutingSqlQuery(
definition.Log(
diagnostics,
containerId,
- logSensitiveData ? partitionKey : "?",
+ logSensitiveData ? partitionKeyValue.ToString() : "?",
FormatParameters(cosmosSqlQuery.Parameters, logSensitiveData && cosmosSqlQuery.Parameters.Count > 0),
Environment.NewLine,
cosmosSqlQuery.Query);
@@ -78,7 +78,7 @@ public static void ExecutingSqlQuery(
definition,
ExecutingSqlQuery,
containerId,
- partitionKey,
+ partitionKeyValue,
cosmosSqlQuery.Parameters.Select(p => (p.Name, p.Value)).ToList(),
cosmosSqlQuery.Query,
diagnostics.ShouldLogSensitiveData());
@@ -93,7 +93,7 @@ private static string ExecutingSqlQuery(EventDefinitionBase definition, EventDat
var p = (CosmosQueryEventData)payload;
return d.GenerateMessage(
p.ContainerId,
- p.LogSensitiveData ? p.PartitionKey : "?",
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?",
FormatParameters(p.Parameters, p is { LogSensitiveData: true, Parameters.Count: > 0 }),
Environment.NewLine,
p.QuerySql);
@@ -108,7 +108,7 @@ private static string ExecutingSqlQuery(EventDefinitionBase definition, EventDat
public static void ExecutingReadItem(
this IDiagnosticsLogger diagnostics,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
string resourceId)
{
var definition = CosmosResources.LogExecutingReadItem(diagnostics);
@@ -116,7 +116,11 @@ public static void ExecutingReadItem(
if (diagnostics.ShouldLog(definition))
{
var logSensitiveData = diagnostics.ShouldLogSensitiveData();
- definition.Log(diagnostics, logSensitiveData ? resourceId : "?", containerId, logSensitiveData ? partitionKey : "?");
+ definition.Log(
+ diagnostics,
+ logSensitiveData ? resourceId : "?",
+ containerId,
+ logSensitiveData ? partitionKeyValue.ToString() : "?");
}
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
@@ -126,7 +130,7 @@ public static void ExecutingReadItem(
ExecutingReadItem,
resourceId,
containerId,
- partitionKey,
+ partitionKeyValue,
diagnostics.ShouldLogSensitiveData());
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
@@ -137,7 +141,9 @@ private static string ExecutingReadItem(EventDefinitionBase definition, EventDat
{
var d = (EventDefinition)definition;
var p = (CosmosReadItemEventData)payload;
- return d.GenerateMessage(p.LogSensitiveData ? p.ResourceId : "?", p.ContainerId, p.LogSensitiveData ? p.PartitionKey : "?");
+ return d.GenerateMessage(
+ p.LogSensitiveData ? p.ResourceId : "?",
+ p.ContainerId, p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?");
}
///
@@ -152,7 +158,7 @@ public static void ExecutedReadNext(
double requestCharge,
string activityId,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery cosmosSqlQuery)
{
var definition = CosmosResources.LogExecutedReadNext(diagnostics);
@@ -171,7 +177,7 @@ public static void ExecutedReadNext(
requestCharge,
activityId,
containerId,
- logSensitiveData ? partitionKey : "?",
+ logSensitiveData ? partitionKeyValue.ToString() : "?",
FormatParameters(cosmosSqlQuery.Parameters, logSensitiveData && cosmosSqlQuery.Parameters.Count > 0),
Environment.NewLine,
cosmosSqlQuery.Query));
@@ -186,7 +192,7 @@ public static void ExecutedReadNext(
requestCharge,
activityId,
containerId,
- partitionKey,
+ partitionKeyValue,
cosmosSqlQuery.Parameters.Select(p => (p.Name, p.Value)).ToList(),
cosmosSqlQuery.Query,
diagnostics.ShouldLogSensitiveData());
@@ -208,7 +214,7 @@ private static string ExecutedReadNext(EventDefinitionBase definition, EventData
p.RequestCharge,
p.ActivityId,
p.ContainerId,
- p.LogSensitiveData ? p.PartitionKey : "?",
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?",
FormatParameters(p.Parameters, p is { LogSensitiveData: true, Parameters.Count: > 0 }),
Environment.NewLine,
p.QuerySql));
@@ -227,7 +233,7 @@ public static void ExecutedReadItem(
string activityId,
string resourceId,
string containerId,
- string? partitionKey)
+ PartitionKey partitionKeyValue)
{
var definition = CosmosResources.LogExecutedReadItem(diagnostics);
@@ -241,7 +247,7 @@ public static void ExecutedReadItem(
activityId,
containerId,
logSensitiveData ? resourceId : "?",
- logSensitiveData ? partitionKey : "?");
+ logSensitiveData ? partitionKeyValue.ToString() : "?");
}
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
@@ -254,7 +260,7 @@ public static void ExecutedReadItem(
activityId,
containerId,
resourceId,
- partitionKey,
+ partitionKeyValue,
diagnostics.ShouldLogSensitiveData());
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
@@ -271,7 +277,7 @@ private static string ExecutedReadItem(EventDefinitionBase definition, EventData
p.ActivityId,
p.ContainerId,
p.LogSensitiveData ? p.ResourceId : "?",
- p.LogSensitiveData ? p.PartitionKey : "?");
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?");
}
///
@@ -287,7 +293,7 @@ public static void ExecutedCreateItem(
string activityId,
string resourceId,
string containerId,
- string? partitionKey)
+ PartitionKey partitionKeyValue)
{
var definition = CosmosResources.LogExecutedCreateItem(diagnostics);
@@ -301,7 +307,7 @@ public static void ExecutedCreateItem(
activityId,
containerId,
logSensitiveData ? resourceId : "?",
- logSensitiveData ? partitionKey : "?");
+ logSensitiveData ? partitionKeyValue.ToString() : "?");
}
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
@@ -314,7 +320,7 @@ public static void ExecutedCreateItem(
activityId,
containerId,
resourceId,
- partitionKey,
+ partitionKeyValue,
diagnostics.ShouldLogSensitiveData());
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
@@ -331,7 +337,7 @@ private static string ExecutedCreateItem(EventDefinitionBase definition, EventDa
p.ActivityId,
p.ContainerId,
p.LogSensitiveData ? p.ResourceId : "?",
- p.LogSensitiveData ? p.PartitionKey : "?");
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?");
}
///
@@ -347,7 +353,7 @@ public static void ExecutedDeleteItem(
string activityId,
string resourceId,
string containerId,
- string? partitionKey)
+ PartitionKey partitionKeyValue)
{
var definition = CosmosResources.LogExecutedDeleteItem(diagnostics);
@@ -361,7 +367,7 @@ public static void ExecutedDeleteItem(
activityId,
containerId,
logSensitiveData ? resourceId : "?",
- logSensitiveData ? partitionKey : "?");
+ logSensitiveData ? partitionKeyValue.ToString() : "?");
}
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
@@ -374,7 +380,7 @@ public static void ExecutedDeleteItem(
activityId,
containerId,
resourceId,
- partitionKey,
+ partitionKeyValue,
diagnostics.ShouldLogSensitiveData());
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
@@ -391,7 +397,7 @@ private static string ExecutedDeleteItem(EventDefinitionBase definition, EventDa
p.ActivityId,
p.ContainerId,
p.LogSensitiveData ? p.ResourceId : "?",
- p.LogSensitiveData ? p.PartitionKey : "?");
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?");
}
///
@@ -407,7 +413,7 @@ public static void ExecutedReplaceItem(
string activityId,
string resourceId,
string containerId,
- string? partitionKey)
+ PartitionKey partitionKeyValue)
{
var definition = CosmosResources.LogExecutedReplaceItem(diagnostics);
@@ -421,7 +427,7 @@ public static void ExecutedReplaceItem(
activityId,
containerId,
logSensitiveData ? resourceId : "?",
- logSensitiveData ? partitionKey : "?");
+ logSensitiveData ? partitionKeyValue.ToString() : "?");
}
if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
@@ -434,7 +440,7 @@ public static void ExecutedReplaceItem(
activityId,
containerId,
resourceId,
- partitionKey,
+ partitionKeyValue,
diagnostics.ShouldLogSensitiveData());
diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
@@ -451,7 +457,34 @@ private static string ExecutedReplaceItem(EventDefinitionBase definition, EventD
p.ActivityId,
p.ContainerId,
p.LogSensitiveData ? p.ResourceId : "?",
- p.LogSensitiveData ? p.PartitionKey : "?");
+ p.LogSensitiveData ? p.PartitionKeyValue.ToString() : "?");
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public static void NoPartitionKeyDefined(
+ this IDiagnosticsLogger diagnostics,
+ IEntityType entityType)
+ {
+ var definition = CosmosResources.LogNoPartitionKeyDefined(diagnostics);
+
+ if (diagnostics.ShouldLog(definition))
+ {
+ definition.Log(diagnostics, entityType.DisplayName());
+ }
+
+ if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled))
+ {
+ var eventData = new EntityTypeEventData(
+ definition,
+ (d, p) => ((EventDefinition)d).GenerateMessage(((EntityTypeEventData)p).EntityType.DisplayName()),
+ entityType);
+ diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled);
+ }
}
private static string FormatParameters(IReadOnlyList<(string Name, object? Value)> parameters, bool shouldLogParameterValues)
diff --git a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs
index 79cda1d0bea..a4d504358d8 100644
--- a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs
+++ b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs
@@ -74,4 +74,12 @@ public class CosmosLoggingDefinitions : LoggingDefinitions
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public EventDefinitionBase? LogSyncNotSupported;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public EventDefinitionBase? LogNoPartitionKeyDefined;
}
diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs
index 22490499039..3292e43101a 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeBuilderExtensions.cs
@@ -196,49 +196,68 @@ public static bool CanSetJsonProperty(
}
///
- /// Configures the property that is used to store the partition key.
+ /// Configures the properties that are used to store the parts of a simple or
+ /// hierarchical partition key.
///
///
/// See Modeling entity types and relationships, and
/// Accessing Azure Cosmos DB with EF Core for more information and examples.
///
/// The builder for the entity type being configured.
- /// The name of the partition key property.
+ /// The name of the first or only partition key property.
+ /// The names of additional properties that will form a hierarchical partition key.
/// The same builder instance so that multiple calls can be chained.
public static EntityTypeBuilder HasPartitionKey(
this EntityTypeBuilder entityTypeBuilder,
- string? name)
+ string? name,
+ params string[]? additionalPropertyNames)
{
- entityTypeBuilder.Metadata.SetPartitionKeyPropertyName(name);
+ Check.NullButNotEmpty(name, nameof(name));
+ Check.HasNoEmptyElements(additionalPropertyNames, nameof(additionalPropertyNames));
+
+ if (name is null)
+ {
+ entityTypeBuilder.Metadata.SetPartitionKeyPropertyNames(null);
+ }
+ else
+ {
+ var propertyNames = new List { name };
+ propertyNames.AddRange(additionalPropertyNames);
+ entityTypeBuilder.Metadata.SetPartitionKeyPropertyNames(propertyNames);
+ }
return entityTypeBuilder;
}
///
- /// Configures the property that is used to store the partition key.
+ /// Configures the properties that are used to store the parts of a simple or
+ /// hierarchical partition key.
///
///
/// See Modeling entity types and relationships, and
/// Accessing Azure Cosmos DB with EF Core for more information and examples.
///
/// The builder for the entity type being configured.
- /// The name of the partition key property.
+ /// The name of the first or only partition key property.
+ /// The names of additional properties that will form a hierarchical partition key.
/// The same builder instance so that multiple calls can be chained.
public static EntityTypeBuilder HasPartitionKey(
this EntityTypeBuilder entityTypeBuilder,
- string? name)
+ string? name,
+ params string[]? additionalPropertyNames)
where TEntity : class
- => (EntityTypeBuilder)HasPartitionKey((EntityTypeBuilder)entityTypeBuilder, name);
+ => (EntityTypeBuilder)HasPartitionKey((EntityTypeBuilder)entityTypeBuilder, name, additionalPropertyNames);
///
- /// Configures the property that is used to store the partition key.
+ /// Configures the properties that are used to store the parts of a simple or
+ /// hierarchical partition key.
///
///
/// See Modeling entity types and relationships, and
/// Accessing Azure Cosmos DB with EF Core for more information and examples.
///
/// The builder for the entity type being configured.
- /// The partition key property.
+ /// The properties that will form the partition key.
/// The same builder instance so that multiple calls can be chained.
public static EntityTypeBuilder HasPartitionKey(
this EntityTypeBuilder entityTypeBuilder,
@@ -247,7 +266,10 @@ public static EntityTypeBuilder HasPartitionKey(
{
Check.NotNull(propertyExpression, nameof(propertyExpression));
- return HasPartitionKey(entityTypeBuilder, propertyExpression.GetMemberAccess().GetSimpleMemberName());
+ entityTypeBuilder.Metadata.SetPartitionKeyPropertyNames(
+ propertyExpression.GetMemberAccessList().Select(e => e.GetSimpleMemberName()).ToList());
+
+ return entityTypeBuilder;
}
///
@@ -264,23 +286,61 @@ public static EntityTypeBuilder HasPartitionKey(
/// The same builder instance if the configuration was applied,
/// otherwise.
///
+ [Obsolete("Use HasPartitionKey(IReadOnlyList, bool)")]
public static IConventionEntityTypeBuilder? HasPartitionKey(
this IConventionEntityTypeBuilder entityTypeBuilder,
string? name,
bool fromDataAnnotation = false)
+ => entityTypeBuilder.HasPartitionKey(name == null ? null : [name], fromDataAnnotation);
+
+ ///
+ /// Returns a value indicating whether the property that is used to store the partition key can be set
+ /// from the current configuration source
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing Azure Cosmos DB with EF Core for more information and examples.
+ ///
+ /// 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.
+ /// if the configuration can be applied.
+ [Obsolete("Use HasPartitionKey(IReadOnlyList, bool)")]
+ public static bool CanSetPartitionKey(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? name,
+ bool fromDataAnnotation = false)
+ => entityTypeBuilder.CanSetPartitionKey(name == null ? null : [name], fromDataAnnotation);
+
+ ///
+ /// Configures the properties that are used to store the parts of a
+ /// hierarchical partition key.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing Azure Cosmos DB with EF Core for more information and examples.
+ ///
+ /// The builder for the entity type being configured.
+ /// The names of the properties that will form the hierarchical partition key.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The same builder instance if the configuration was applied, otherwise.
+ public static IConventionEntityTypeBuilder? HasPartitionKey(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ IReadOnlyList? propertyNames,
+ bool fromDataAnnotation = false)
{
- if (!entityTypeBuilder.CanSetPartitionKey(name, fromDataAnnotation))
+ if (!entityTypeBuilder.CanSetPartitionKey(propertyNames, fromDataAnnotation))
{
return null;
}
- entityTypeBuilder.Metadata.SetPartitionKeyPropertyName(name, fromDataAnnotation);
+ entityTypeBuilder.Metadata.SetPartitionKeyPropertyNames(propertyNames, fromDataAnnotation);
return entityTypeBuilder;
}
///
- /// Returns a value indicating whether the property that is used to store the partition key can be set
+ /// Returns a value indicating whether the properties that are used to store the parts of a hierarchical partition key
/// from the current configuration source
///
///
@@ -288,18 +348,17 @@ public static EntityTypeBuilder HasPartitionKey(
/// Accessing Azure Cosmos DB with EF Core for more information and examples.
///
/// The builder for the entity type being configured.
- /// The name of the partition key property.
+ /// The name of the partition key properties.
/// Indicates whether the configuration was specified using a data annotation.
/// if the configuration can be applied.
public static bool CanSetPartitionKey(
this IConventionEntityTypeBuilder entityTypeBuilder,
- string? name,
+ IReadOnlyList? names,
bool fromDataAnnotation = false)
- {
- Check.NullButNotEmpty(name, nameof(name));
-
- return entityTypeBuilder.CanSetAnnotation(CosmosAnnotationNames.PartitionKeyName, name, fromDataAnnotation);
- }
+ => entityTypeBuilder.CanSetAnnotation(
+ CosmosAnnotationNames.PartitionKeyNames,
+ names is null ? names : Check.HasNoEmptyElements(names, nameof(names)),
+ fromDataAnnotation);
///
/// Configures this entity to use CosmosDb etag concurrency checks.
diff --git a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs
index 53b220d21c3..be6ad2a2bc1 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosEntityTypeExtensions.cs
@@ -119,18 +119,18 @@ public static void SetContainingPropertyName(this IMutableEntityType entityType,
///
/// The entity type to get the partition key property name for.
/// The name of the partition key property.
+ [Obsolete("Use SetPartitionKeyPropertyNames")]
public static string? GetPartitionKeyPropertyName(this IReadOnlyEntityType entityType)
- => entityType[CosmosAnnotationNames.PartitionKeyName] as string;
+ => entityType.GetPartitionKeyPropertyNames().FirstOrDefault();
///
/// 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.
+ [Obsolete("Use SetPartitionKeyPropertyNames")]
public static void SetPartitionKeyPropertyName(this IMutableEntityType entityType, string? name)
- => entityType.SetOrRemoveAnnotation(
- CosmosAnnotationNames.PartitionKeyName,
- Check.NullButNotEmpty(name, nameof(name)));
+ => entityType.SetPartitionKeyPropertyNames(name == null ? null : [name]);
///
/// Sets the name of the property that is used to store the partition key.
@@ -138,75 +138,131 @@ public static void SetPartitionKeyPropertyName(this IMutableEntityType entityTyp
/// 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.
+ [Obsolete("Use SetPartitionKeyPropertyNames")]
public static string? SetPartitionKeyPropertyName(
this IConventionEntityType entityType,
string? name,
bool fromDataAnnotation = false)
- => (string?)entityType.SetOrRemoveAnnotation(
- CosmosAnnotationNames.PartitionKeyName,
- Check.NullButNotEmpty(name, nameof(name)),
- fromDataAnnotation)?.Value;
+ => entityType.SetPartitionKeyPropertyNames(name is null ? null : [name], fromDataAnnotation)?.FirstOrDefault();
///
/// 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.
+ [Obsolete("Use GetPartitionKeyPropertyNamesConfigurationSource")]
public static ConfigurationSource? GetPartitionKeyPropertyNameConfigurationSource(this IConventionEntityType entityType)
- => entityType.FindAnnotation(CosmosAnnotationNames.PartitionKeyName)
- ?.GetConfigurationSource();
+ => entityType.GetPartitionKeyPropertyNamesConfigurationSource();
///
/// Returns the property that is used to store the partition key.
///
/// The entity type to get the partition key property for.
/// The name of the partition key property.
+ [Obsolete("Use GetPartitionKeyProperties")]
public static IReadOnlyProperty? GetPartitionKeyProperty(this IReadOnlyEntityType entityType)
- {
- var partitionKeyPropertyName = entityType.GetPartitionKeyPropertyName();
- return partitionKeyPropertyName == null
- ? null
- : entityType.FindProperty(partitionKeyPropertyName);
- }
+ => entityType.GetPartitionKeyProperties().FirstOrDefault();
///
/// Returns the property that is used to store the partition key.
///
/// The entity type to get the partition key property for.
/// The name of the partition key property.
+ [Obsolete("Use GetPartitionKeyProperties")]
public static IMutableProperty? GetPartitionKeyProperty(this IMutableEntityType entityType)
- {
- var partitionKeyPropertyName = entityType.GetPartitionKeyPropertyName();
- return partitionKeyPropertyName == null
- ? null
- : entityType.FindProperty(partitionKeyPropertyName);
- }
+ => entityType.GetPartitionKeyProperties().FirstOrDefault();
///
/// Returns the property that is used to store the partition key.
///
/// The entity type to get the partition key property for.
/// The name of the partition key property.
+ [Obsolete("Use GetPartitionKeyProperties")]
public static IConventionProperty? GetPartitionKeyProperty(this IConventionEntityType entityType)
- {
- var partitionKeyPropertyName = entityType.GetPartitionKeyPropertyName();
- return partitionKeyPropertyName == null
- ? null
- : entityType.FindProperty(partitionKeyPropertyName);
- }
+ => entityType.GetPartitionKeyProperties().FirstOrDefault();
///
/// Returns the property that is used to store the partition key.
///
/// The entity type to get the partition key property for.
/// The name of the partition key property.
+ [Obsolete("Use GetPartitionKeyProperties")]
public static IProperty? GetPartitionKeyProperty(this IEntityType entityType)
- {
- var partitionKeyPropertyName = entityType.GetPartitionKeyPropertyName();
- return partitionKeyPropertyName == null
- ? null
- : entityType.FindProperty(partitionKeyPropertyName);
- }
+ => entityType.GetPartitionKeyProperties().FirstOrDefault();
+
+ ///
+ /// Returns the names of the properties that are used to store the hierarchical partition key, if any.
+ ///
+ /// The entity type.
+ /// The names of the partition key properties, or if not set.
+ public static IReadOnlyList GetPartitionKeyPropertyNames(this IReadOnlyEntityType entityType)
+ => entityType[CosmosAnnotationNames.PartitionKeyNames] as IReadOnlyList ?? Array.Empty();
+
+ ///
+ /// Sets the names of the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type.
+ /// The names to set, or to clear all names.
+ public static void SetPartitionKeyPropertyNames(this IMutableEntityType entityType, IReadOnlyList? names)
+ => entityType.SetOrRemoveAnnotation(
+ CosmosAnnotationNames.PartitionKeyNames, names is null ? names : Check.HasNoEmptyElements(names, nameof(names)));
+
+ ///
+ /// Sets the names of the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type to set the partition key property name for.
+ /// The names to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ public static IReadOnlyList? SetPartitionKeyPropertyNames(
+ this IConventionEntityType entityType,
+ IReadOnlyList? names,
+ bool fromDataAnnotation = false)
+ => (IReadOnlyList?)entityType
+ .SetOrRemoveAnnotation(
+ CosmosAnnotationNames.PartitionKeyNames,
+ names is null ? names : Check.HasNoEmptyElements(names, nameof(names)),
+ fromDataAnnotation)?.Value;
+
+ ///
+ /// Gets the for the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type to find configuration source for.
+ /// The for the partition key properties.
+ public static ConfigurationSource? GetPartitionKeyPropertyNamesConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(CosmosAnnotationNames.PartitionKeyNames)
+ ?.GetConfigurationSource();
+
+ ///
+ /// Returns the the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type.
+ /// The hierarchical partition key properties.
+ public static IReadOnlyList GetPartitionKeyProperties(this IReadOnlyEntityType entityType)
+ => entityType.GetPartitionKeyPropertyNames().Select(n => entityType.FindProperty(n)!).ToList();
+
+ ///
+ /// Returns the the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type.
+ /// The hierarchical partition key properties.
+ public static IReadOnlyList GetPartitionKeyProperties(this IMutableEntityType entityType)
+ => entityType.GetPartitionKeyPropertyNames().Select(n => entityType.FindProperty(n)!).ToList();
+
+ ///
+ /// Returns the the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type.
+ /// The hierarchical partition key properties.
+ public static IReadOnlyList GetPartitionKeyProperties(this IConventionEntityType entityType)
+ => entityType.GetPartitionKeyPropertyNames().Select(n => entityType.FindProperty(n)!).ToList();
+
+ ///
+ /// Returns the the properties that are used to store the hierarchical partition key.
+ ///
+ /// The entity type.
+ /// The hierarchical partition key properties.
+ public static IReadOnlyList GetPartitionKeyProperties(this IEntityType entityType)
+ => entityType.GetPartitionKeyPropertyNames().Select(n => entityType.FindProperty(n)!).ToList();
///
/// Returns the name of the property that is used to store the ETag.
diff --git a/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs
index 603a2eb5cb0..1d6950c8ef3 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs
@@ -18,7 +18,27 @@ namespace Microsoft.EntityFrameworkCore;
public static class CosmosQueryableExtensions
{
internal static readonly MethodInfo WithPartitionKeyMethodInfo
- = typeof(CosmosQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(WithPartitionKey))!;
+ = typeof(CosmosQueryableExtensions).GetTypeInfo()
+ .GetDeclaredMethods(nameof(WithPartitionKey))
+ .Single(mi => mi.GetParameters().Length == 3);
+
+ ///
+ /// Specify the partition key value for partition used for the query. Required when using
+ /// a resource token that provides permission based on a partition key for authentication.
+ ///
+ ///
+ /// See Querying data with EF Core, and
+ /// Accessing Azure Cosmos DB with EF Core for more information and examples.
+ ///
+ /// The type of entity being queried.
+ /// The source query.
+ /// The partition key value.
+ /// A new query with the set partition key.
+ public static IQueryable WithPartitionKey(
+ this IQueryable source,
+ [NotParameterized] string partitionKey)
+ where TEntity : class
+ => WithPartitionKey(source, partitionKey, []);
///
/// Specify the partition key for partition used for the query. Required when using
@@ -30,14 +50,17 @@ internal static readonly MethodInfo WithPartitionKeyMethodInfo
///
/// The type of entity being queried.
/// The source query.
- /// The partition key.
+ /// The partition key value.
+ /// Additional values for hierarchical partitions.
/// A new query with the set partition key.
public static IQueryable WithPartitionKey(
this IQueryable source,
- [NotParameterized] string partitionKey)
+ [NotParameterized] object partitionKeyValue,
+ [NotParameterized] params object[] additionalPartitionKeyValues)
where TEntity : class
{
- Check.NotNull(partitionKey, nameof(partitionKey));
+ Check.NotNull(partitionKeyValue, nameof(partitionKeyValue));
+ Check.HasNoNulls(additionalPartitionKeyValues, nameof(additionalPartitionKeyValues));
return
source.Provider is EntityQueryProvider
@@ -46,7 +69,8 @@ source.Provider is EntityQueryProvider
instance: null,
method: WithPartitionKeyMethodInfo.MakeGenericMethod(typeof(TEntity)),
source.Expression,
- Expression.Constant(partitionKey)))
+ Expression.Constant(partitionKeyValue, typeof(object)),
+ Expression.Constant(additionalPartitionKeyValues, typeof(object[]))))
: source;
}
diff --git a/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs
new file mode 100644
index 00000000000..f367d546489
--- /dev/null
+++ b/src/EFCore.Cosmos/Extensions/Internal/PartitionKeyBuilderExtensions.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Cosmos.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public static class PartitionKeyBuilderExtensions
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public static PartitionKeyBuilder Add(this PartitionKeyBuilder builder, object? value, IProperty? property)
+ {
+ var converter = property?.GetTypeMapping().Converter;
+ if (converter != null)
+ {
+ value = converter.ConvertToProvider(value);
+ }
+
+ if (value == null)
+ {
+ builder.AddNullValue();
+ }
+ else
+ {
+ var expectedType = (converter?.ProviderClrType ?? property?.ClrType)?.UnwrapNullableType();
+ if (value is string stringValue)
+ {
+ if (expectedType != null && expectedType != typeof(string))
+ {
+ CheckType(typeof(string));
+ }
+
+ builder.Add(stringValue);
+ }
+ else if (value is bool boolValue)
+ {
+ if (expectedType != null && expectedType != typeof(bool))
+ {
+ CheckType(typeof(bool));
+ }
+
+ builder.Add(boolValue);
+ }
+ else if (value.GetType().IsNumeric())
+ {
+ if (expectedType != null && !expectedType.IsNumeric())
+ {
+ CheckType(value.GetType());
+ }
+
+ builder.Add(Convert.ToDouble(value));
+ }
+ else
+ {
+ throw new InvalidOperationException(CosmosStrings.PartitionKeyBadValue(value.GetType()));
+ }
+
+ void CheckType(Type actualType)
+ {
+ if (expectedType != null && expectedType != actualType)
+ {
+ throw new InvalidOperationException(
+ CosmosStrings.PartitionKeyBadValueType(
+ expectedType.ShortDisplayName(),
+ property!.DeclaringType.DisplayName(),
+ property.Name,
+ actualType.DisplayName()));
+ }
+ }
+ }
+
+ return builder;
+ }
+}
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs
index 1fb048fd995..59e148b1537 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
@@ -51,6 +52,7 @@ protected virtual void ValidateSharedContainerCompatibility(
IModel model,
IDiagnosticsLogger logger)
{
+ // All entity types mapped to a single container must have the same container-level settings, most notably partition keys.
var containers = new Dictionary>();
foreach (var entityType in model.GetEntityTypes().Where(et => et.FindPrimaryKey() != null))
{
@@ -102,7 +104,7 @@ protected virtual void ValidateSharedContainerCompatibility(
IDiagnosticsLogger logger)
{
var discriminatorValues = new Dictionary
- public const string PartitionKeyName = Prefix + "PartitionKeyName";
+ public const string PartitionKeyNames = Prefix + "PartitionKeyNames";
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
index aecd79717d0..ad7425bf61c 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
@@ -190,12 +190,12 @@ public static string NonETagConcurrencyToken(object? entityType, object? propert
entityType, property);
///
- /// The entity type '{entityType}' does not have a partition key set, but is mapped to the container '{container}' shared by entity types with partition keys. Configure a compatible partition key on '{entityType}'.
+ /// The partition key properties for entity type '{entityType1}' are '{props1}', while the partition key properties for entity type '{entityType2}' are '{props2}', and both entity types are mapped to the container '{containerName}'. All entity types mapped to the same container must have compatible partition keys defined.
///
- public static string NoPartitionKey(object? entityType, object? container)
+ public static string NoPartitionKey(object? entityType1, object? props1, object? entityType2, object? props2, object? containerName)
=> string.Format(
- GetString("NoPartitionKey", nameof(entityType), nameof(container)),
- entityType, container);
+ GetString("NoPartitionKey", nameof(entityType1), nameof(props1), nameof(entityType2), nameof(props2), nameof(containerName)),
+ entityType1, props1, entityType2, props2, containerName);
///
/// The entity type '{entityType}' does not have a key declared on '{partitionKey}' and '{idProperty}' properties. Add a key to '{entityType}' that contains '{partitionKey}' and '{idProperty}'.
@@ -271,6 +271,30 @@ public static string OwnedTypeDifferentContainer(object? entityType, object? own
GetString("OwnedTypeDifferentContainer", nameof(entityType), nameof(owner), nameof(container)),
entityType, owner, container);
+ ///
+ /// The type of the partition key property '{property}' on '{entityType}' is '{propertyType}'. All partition key property types must be numeric, Boolean, or string, or converted to one of these types.
+ ///
+ public static string PartitionKeyBadStoreType(object? property, object? entityType, object? propertyType)
+ => string.Format(
+ GetString("PartitionKeyBadStoreType", nameof(property), nameof(entityType), nameof(propertyType)),
+ property, entityType, propertyType);
+
+ ///
+ /// The partition key value is of type '{valueType}' which is not valid for Cosmos partition keys. All partition key properties values must be numeric, Boolean, or string, or converted to one of these types.
+ ///
+ public static string PartitionKeyBadValue(object? valueType)
+ => string.Format(
+ GetString("PartitionKeyBadValue", nameof(valueType)),
+ valueType);
+
+ ///
+ /// The partition key value supplied for '{propertyType}' property '{entityType}.{property}' is of type '{valueType}'. Partition key values must be of a type assignable to the property.
+ ///
+ public static string PartitionKeyBadValueType(object? propertyType, object? entityType, object? property, object? valueType)
+ => string.Format(
+ GetString("PartitionKeyBadValueType", nameof(propertyType), nameof(entityType), nameof(property), nameof(valueType)),
+ propertyType, entityType, property, valueType);
+
///
/// The partition key specified in the 'WithPartitionKey' call '{partitionKey1}' and the partition key specified in the 'Where' predicate '{partitionKey2}' must be identical to return any results. Remove one of them.
///
@@ -293,14 +317,6 @@ public static string PartitionKeyMissingProperty(object? entityType, object? pro
GetString("PartitionKeyMissingProperty", nameof(entityType), nameof(property)),
entityType, property);
- ///
- /// The type of the partition key property '{property}' on '{entityType}' is '{propertyType}'. All partition key properties need to be strings or have a string value converter.
- ///
- public static string PartitionKeyNonStringStoreType(object? property, object? entityType, object? propertyType)
- => string.Format(
- GetString("PartitionKeyNonStringStoreType", nameof(property), nameof(entityType), nameof(propertyType)),
- property, entityType, propertyType);
-
///
/// 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 for entity types mapped to the same container.
///
@@ -381,6 +397,12 @@ public static string UpdateStoreException(object? itemId)
public static string VisitChildrenMustBeOverridden
=> GetString("VisitChildrenMustBeOverridden");
+ ///
+ /// 'WithPartitionKeyMethodInfo' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information.
+ ///
+ public static string WithPartitionKeyBadNode
+ => GetString("WithPartitionKeyBadNode");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name)!;
@@ -579,6 +601,31 @@ public static FallbackEventDefinition LogExecutedReadNext(IDiagnosticsLogger log
return (EventDefinition)definition;
}
+ ///
+ /// No partition key has been configured for entity type '{entityType}'. It is highly recommended that an appropriate partition key be defined. See https://aka.ms/efdocs-cosmos-partition-keys for more information.
+ ///
+ public static EventDefinition LogNoPartitionKeyDefined(IDiagnosticsLogger logger)
+ {
+ var definition = ((Diagnostics.Internal.CosmosLoggingDefinitions)logger.Definitions).LogNoPartitionKeyDefined;
+ if (definition == null)
+ {
+ definition = NonCapturingLazyInitializer.EnsureInitialized(
+ ref ((Diagnostics.Internal.CosmosLoggingDefinitions)logger.Definitions).LogNoPartitionKeyDefined,
+ logger,
+ static logger => new EventDefinition(
+ logger.Options,
+ CosmosEventId.NoPartitionKeyDefined,
+ LogLevel.Warning,
+ "CosmosEventId.NoPartitionKeyDefined",
+ level => LoggerMessage.Define(
+ level,
+ CosmosEventId.NoPartitionKeyDefined,
+ _resourceManager.GetString("LogNoPartitionKeyDefined")!)));
+ }
+
+ return (EventDefinition)definition;
+ }
+
///
/// Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
///
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
index 0709e132b3a..3243c41e5c1 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
@@ -187,6 +187,10 @@
Executing SQL query for container '{containerId}' in partition '{partitionKey}' [Parameters=[{parameters}]]{newLine}{commandText}
Information CosmosEventId.ExecutingSqlQuery string string? string string string
+
+ No partition key has been configured for entity type '{entityType}'. It is highly recommended that an appropriate partition key be defined. See https://aka.ms/efdocs-cosmos-partition-keys for more information.
+ Warning CosmosEventId.NoPartitionKeyDefined string
+
Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
Error CosmosEventId.SyncNotSupported
@@ -216,7 +220,7 @@
The entity type '{entityType}' has property '{property}' configured as a concurrency token, but only a property mapped to '_etag' is supported as a concurrency token. Consider using 'PropertyBuilder.IsETagConcurrency'.
- The entity type '{entityType}' does not have a partition key set, but is mapped to the container '{container}' shared by entity types with partition keys. Configure a compatible partition key on '{entityType}'.
+ The partition key properties for entity type '{entityType1}' are '{props1}', while the partition key properties for entity type '{entityType2}' are '{props2}', and both entity types are mapped to the container '{containerName}'. All entity types mapped to the same container must have compatible partition keys defined.
The entity type '{entityType}' does not have a key declared on '{partitionKey}' and '{idProperty}' properties. Add a key to '{entityType}' that contains '{partitionKey}' and '{idProperty}'.
@@ -248,6 +252,15 @@
The entity type '{entityType}' is owned by the entity type '{owner}', but is mapped to the container '{container}'. Owned types mapped to a container directly are not supported, remove this configuration to allow the owned type to be embedded in the same document as the owner.
+
+ The type of the partition key property '{property}' on '{entityType}' is '{propertyType}'. All partition key property types must be numeric, Boolean, or string, or converted to one of these types.
+
+
+ The partition key value is of type '{valueType}' which is not valid for Cosmos partition keys. All partition key properties values must be numeric, Boolean, or string, or converted to one of these types.
+
+
+ The partition key value supplied for '{propertyType}' property '{entityType}.{property}' is of type '{valueType}'. Partition key values must be of a type assignable to the property.
+
The partition key specified in the 'WithPartitionKey' call '{partitionKey1}' and the partition key specified in the 'Where' predicate '{partitionKey2}' must be identical to return any results. Remove one of them.
@@ -257,9 +270,6 @@
The partition key for entity type '{entityType}' is set to '{property}', but there is no property with that name.
-
- The type of the partition key property '{property}' on '{entityType}' is '{propertyType}'. All partition key properties need to be strings or have a string value converter.
-
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 for entity types mapped to the same container.
@@ -293,4 +303,7 @@
'VisitChildren' must be overridden in the class deriving from 'SqlExpression'.
+
+ 'WithPartitionKeyMethodInfo' can only be called on a entity query root. See https://aka.ms/efdocs-cosmos-partition-keys for more information.
+
\ No newline at end of file
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs
index 471cb10035e..49d99d3712d 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryCompilationContext.cs
@@ -18,5 +18,5 @@ public class CosmosQueryCompilationContext(QueryCompilationContextDependencies d
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual string? PartitionKeyFromExtension { get; internal set; }
+ public virtual PartitionKey? PartitionKeyValueFromExtension { get; internal set; }
}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs
index 0fac66d6582..4e0dd73b3a9 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Cosmos.Internal;
+using Microsoft.EntityFrameworkCore.Internal;
+
namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
///
@@ -25,7 +28,30 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
var innerQueryable = Visit(methodCallExpression.Arguments[0]);
- cosmosQueryCompilationContext.PartitionKeyFromExtension = methodCallExpression.Arguments[1].GetConstantValue();
+ var firstValue = methodCallExpression.Arguments[1].GetConstantValue
protected override ShapedQueryExpression? TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
{
- if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IEntityType entityType }
- && entityType.GetPartitionKeyPropertyName() is string partitionKeyPropertyName
- && TryExtractPartitionKey(predicate.Body, entityType, out var newPredicate) is Expression partitionKeyValue)
+ if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IEntityType entityType } entityShaperExpression
+ && entityType.GetPartitionKeyPropertyNames().FirstOrDefault() != null)
{
- var partitionKeyProperty = entityType.GetProperty(partitionKeyPropertyName);
- ((SelectExpression)source.QueryExpression).SetPartitionKey(partitionKeyProperty, partitionKeyValue);
-
- if (newPredicate == null)
+ List<(Expression Expression, IProperty Property)?> partitionKeyValues = new();
+ if (TryExtractPartitionKey(predicate.Body, entityType, out var newPredicate, partitionKeyValues))
{
- return source;
- }
+ foreach (var propertyName in entityType.GetPartitionKeyPropertyNames())
+ {
+ var partitionKeyValue = partitionKeyValues.FirstOrDefault(p => p!.Value.Property.Name == propertyName);
+ if (partitionKeyValue == null)
+ {
+ newPredicate = null;
+ break;
+ }
- predicate = Expression.Lambda(newPredicate, predicate.Parameters);
+ ((SelectExpression)source.QueryExpression).AddPartitionKey(
+ partitionKeyValue.Value.Property, partitionKeyValue.Value.Expression);
+ }
+
+ if (newPredicate == null)
+ {
+ return source;
+ }
+
+ predicate = Expression.Lambda(newPredicate, predicate.Parameters);
+ }
}
if (TranslateLambdaExpression(source, predicate) is SqlExpression translation)
@@ -968,22 +966,37 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
return null;
- Expression? TryExtractPartitionKey(Expression expression, IEntityType entityType, out Expression? updatedPredicate)
+ bool TryExtractPartitionKey(
+ Expression expression,
+ IEntityType entityType,
+ out Expression? updatedPredicate,
+ List<(Expression, IProperty)?> partitionKeyValues)
{
+ updatedPredicate = null;
if (expression is BinaryExpression binaryExpression)
{
- if (GetPartitionKeyValue(binaryExpression, entityType) is Expression pkv)
+ if (TryGetPartitionKeyValue(binaryExpression, entityType, out var valueExpression, out var property))
{
- partitionKeyValue = pkv;
- updatedPredicate = null;
- return partitionKeyValue;
+ partitionKeyValues.Add((valueExpression!, property!));
+ return true;
}
if (binaryExpression.NodeType == ExpressionType.AndAlso)
{
- var leftPartitionKeyValue = TryExtractPartitionKey(binaryExpression.Left, entityType, out var leftPredicate);
- var rightPartitionKeyValue = TryExtractPartitionKey(binaryExpression.Right, entityType, out var rightPredicate);
- if ((leftPartitionKeyValue != null) ^ (rightPartitionKeyValue != null))
+ var foundInRight = TryExtractPartitionKey(binaryExpression.Left, entityType, out var leftPredicate, partitionKeyValues);
+
+ var foundInLeft = TryExtractPartitionKey(
+ binaryExpression.Right,
+ entityType,
+ out var rightPredicate,
+ partitionKeyValues);
+
+ if (foundInLeft && foundInRight)
+ {
+ return true;
+ }
+
+ if (foundInLeft || foundInRight)
{
updatedPredicate = leftPredicate != null
? rightPredicate != null
@@ -991,42 +1004,64 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
: leftPredicate
: rightPredicate;
- return leftPartitionKeyValue ?? rightPartitionKeyValue;
+ return true;
}
}
}
+ else if (expression.NodeType == ExpressionType.MemberAccess
+ && expression.Type == typeof(bool))
+ {
+ if (IsPartitionKeyPropertyAccess(expression, entityType, out var property))
+ {
+ partitionKeyValues.Add((Expression.Constant(true), property!));
+ return true;
+ }
+ }
+ else if (expression.NodeType == ExpressionType.Not)
+ {
+ if (IsPartitionKeyPropertyAccess(((UnaryExpression)expression).Operand, entityType, out var property))
+ {
+ partitionKeyValues.Add((Expression.Constant(false), property!));
+ return true;
+ }
+ }
updatedPredicate = expression;
-
- return null;
+ return false;
}
- Expression? GetPartitionKeyValue(BinaryExpression binaryExpression, IEntityType entityType)
+ bool TryGetPartitionKeyValue(
+ BinaryExpression binaryExpression,
+ IEntityType entityType,
+ out Expression? expression,
+ out IProperty? property)
{
if (binaryExpression.NodeType == ExpressionType.Equal)
{
- var valueExpression = IsPartitionKeyPropertyAccess(binaryExpression.Left, entityType)
+ expression = IsPartitionKeyPropertyAccess(binaryExpression.Left, entityType, out property)
? binaryExpression.Right
- : IsPartitionKeyPropertyAccess(binaryExpression.Right, entityType)
+ : IsPartitionKeyPropertyAccess(binaryExpression.Right, entityType, out property)
? binaryExpression.Left
: null;
- if (valueExpression is ConstantExpression
- || (valueExpression is ParameterExpression valueParameterExpression
+ if (expression is ConstantExpression
+ || (expression is ParameterExpression valueParameterExpression
&& valueParameterExpression.Name?
.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal)
== true))
{
- return valueExpression;
+ return true;
}
}
- return null;
+ expression = null;
+ property = null;
+ return false;
}
- bool IsPartitionKeyPropertyAccess(Expression expression, IEntityType entityType)
+ bool IsPartitionKeyPropertyAccess(Expression expression, IEntityType entityType, out IProperty? property)
{
- var property = expression switch
+ property = expression switch
{
MemberExpression memberExpression
=> entityType.FindProperty(memberExpression.Member.GetSimpleMemberName()),
@@ -1038,7 +1073,7 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model,
_ => null
};
- return property != null && property.Name == entityType.GetPartitionKeyPropertyName();
+ return property != null && entityType.GetPartitionKeyPropertyNames().Contains(property.Name);
}
}
@@ -1072,8 +1107,7 @@ private static ShapedQueryExpression AggregateResultShaper(
Type resultType)
{
var selectExpression = (SelectExpression)source.QueryExpression;
- selectExpression.ReplaceProjectionMapping(
- new Dictionary { { new ProjectionMember(), projection } });
+ selectExpression.ReplaceProjectionMapping(new Dictionary { { new ProjectionMember(), projection } });
selectExpression.ClearOrdering();
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs
index 86db21631dd..8fcfe871c05 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs
@@ -27,7 +27,7 @@ private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable
private readonly Func _shaper;
private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory;
private readonly Type _contextType;
- private readonly string _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly IDiagnosticsLogger _queryLogger;
private readonly bool _standAloneStateManager;
private readonly bool _threadSafetyChecksEnabled;
@@ -39,7 +39,7 @@ public QueryingEnumerable(
SelectExpression selectExpression,
Func shaper,
Type contextType,
- string partitionKeyFromExtension,
+ PartitionKey partitionKeyValueFromExtension,
bool standAloneStateManager,
bool threadSafetyChecksEnabled)
{
@@ -53,13 +53,15 @@ public QueryingEnumerable(
_standAloneStateManager = standAloneStateManager;
_threadSafetyChecksEnabled = threadSafetyChecksEnabled;
- var partitionKey = selectExpression.GetPartitionKey(cosmosQueryContext.ParameterValues);
- if (partitionKey != null && partitionKeyFromExtension != null && partitionKeyFromExtension != partitionKey)
+ var partitionKey = selectExpression.GetPartitionKeyValue(cosmosQueryContext.ParameterValues);
+ if (partitionKey != PartitionKey.None
+ && partitionKeyValueFromExtension != PartitionKey.None
+ && !partitionKeyValueFromExtension.Equals(partitionKey))
{
- throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch(partitionKeyFromExtension, partitionKey));
+ throw new InvalidOperationException(CosmosStrings.PartitionKeyMismatch(partitionKeyValueFromExtension, partitionKey));
}
- _partitionKey = partitionKey ?? partitionKeyFromExtension;
+ _partitionKeyValue = partitionKey != PartitionKey.None ? partitionKey : partitionKeyValueFromExtension;
}
public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
@@ -108,7 +110,7 @@ private sealed class Enumerator : IEnumerator
private readonly SelectExpression _selectExpression;
private readonly Func _shaper;
private readonly Type _contextType;
- private readonly string _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly IDiagnosticsLogger _queryLogger;
private readonly bool _standAloneStateManager;
private readonly IConcurrencyDetector _concurrencyDetector;
@@ -123,7 +125,7 @@ public Enumerator(QueryingEnumerable queryingEnumerable)
_shaper = queryingEnumerable._shaper;
_selectExpression = queryingEnumerable._selectExpression;
_contextType = queryingEnumerable._contextType;
- _partitionKey = queryingEnumerable._partitionKey;
+ _partitionKeyValue = queryingEnumerable._partitionKeyValue;
_queryLogger = queryingEnumerable._queryLogger;
_standAloneStateManager = queryingEnumerable._standAloneStateManager;
_exceptionDetector = _cosmosQueryContext.ExceptionDetector;
@@ -155,7 +157,7 @@ public bool MoveNext()
_enumerator = _cosmosQueryContext.CosmosClient
.ExecuteSqlQuery(
_selectExpression.Container,
- _partitionKey,
+ _partitionKeyValue,
sqlQuery)
.GetEnumerator();
_cosmosQueryContext.InitializeStateManager(_standAloneStateManager);
@@ -207,7 +209,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator
private readonly SelectExpression _selectExpression;
private readonly Func _shaper;
private readonly Type _contextType;
- private readonly string _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly IDiagnosticsLogger _queryLogger;
private readonly bool _standAloneStateManager;
private readonly CancellationToken _cancellationToken;
@@ -223,7 +225,7 @@ public AsyncEnumerator(QueryingEnumerable queryingEnumerable, CancellationTok
_shaper = queryingEnumerable._shaper;
_selectExpression = queryingEnumerable._selectExpression;
_contextType = queryingEnumerable._contextType;
- _partitionKey = queryingEnumerable._partitionKey;
+ _partitionKeyValue = queryingEnumerable._partitionKeyValue;
_queryLogger = queryingEnumerable._queryLogger;
_standAloneStateManager = queryingEnumerable._standAloneStateManager;
_exceptionDetector = _cosmosQueryContext.ExceptionDetector;
@@ -253,7 +255,7 @@ public async ValueTask MoveNextAsync()
_enumerator = _cosmosQueryContext.CosmosClient
.ExecuteSqlQueryAsync(
_selectExpression.Container,
- _partitionKey,
+ _partitionKeyValue,
sqlQuery)
.GetAsyncEnumerator(_cancellationToken);
_cosmosQueryContext.InitializeStateManager(_standAloneStateManager);
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs
index fcdaa95da0e..948b67f22d4 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs
@@ -58,30 +58,36 @@ IEnumerator IEnumerable.GetEnumerator()
public string ToQueryString()
{
TryGetResourceId(out var resourceId);
- TryGetPartitionId(out var partitionKey);
+ TryGetPartitionKey(out var partitionKey);
return CosmosStrings.NoReadItemQueryString(resourceId, partitionKey);
}
- private bool TryGetPartitionId(out string partitionKey)
+ private bool TryGetPartitionKey(out PartitionKey partitionKeyValue)
{
- partitionKey = null;
-
- var partitionKeyPropertyName = _readItemExpression.EntityType.GetPartitionKeyPropertyName();
- if (partitionKeyPropertyName == null)
+ var properties = _readItemExpression.EntityType.GetPartitionKeyProperties();
+ if (!properties.Any())
{
+ partitionKeyValue = PartitionKey.None;
return true;
}
- var partitionKeyProperty = _readItemExpression.EntityType.FindProperty(partitionKeyPropertyName);
-
- if (TryGetParameterValue(partitionKeyProperty, out var value))
+ var builder = new PartitionKeyBuilder();
+ foreach (var property in properties)
{
- partitionKey = GetString(partitionKeyProperty, value);
-
- return !string.IsNullOrEmpty(partitionKey);
+ if (TryGetParameterValue(property, out var value))
+ {
+ if (value == null)
+ {
+ partitionKeyValue = PartitionKey.Null;
+ return false;
+ }
+ builder.Add(value, property);
+ }
}
- return false;
+ partitionKeyValue = builder.Build();
+
+ return true;
}
private bool TryGetResourceId(out string resourceId)
@@ -213,7 +219,7 @@ public bool MoveNext()
throw new InvalidOperationException(CosmosStrings.ResourceIdMissing);
}
- if (!_readItemEnumerable.TryGetPartitionId(out var partitionKey))
+ if (!_readItemEnumerable.TryGetPartitionKey(out var partitionKeyValue))
{
throw new InvalidOperationException(CosmosStrings.PartitionKeyMissing);
}
@@ -222,7 +228,7 @@ public bool MoveNext()
_item = _cosmosQueryContext.CosmosClient.ExecuteReadItem(
_readItemExpression.Container,
- partitionKey,
+ partitionKeyValue,
resourceId);
return ShapeResult();
@@ -265,7 +271,7 @@ public async ValueTask MoveNextAsync()
throw new InvalidOperationException(CosmosStrings.ResourceIdMissing);
}
- if (!_readItemEnumerable.TryGetPartitionId(out var partitionKey))
+ if (!_readItemEnumerable.TryGetPartitionKey(out var partitionKeyValue))
{
throw new InvalidOperationException(CosmosStrings.PartitionKeyMissing);
}
@@ -274,7 +280,7 @@ public async ValueTask MoveNextAsync()
_item = await _cosmosQueryContext.CosmosClient.ExecuteReadItemAsync(
_readItemExpression.Container,
- partitionKey,
+ partitionKeyValue,
resourceId,
_cancellationToken)
.ConfigureAwait(false);
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs
index c8eaf2d1120..3acf05dc059 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs
@@ -23,7 +23,9 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor(
{
private readonly Type _contextType = cosmosQueryCompilationContext.ContextType;
private readonly bool _threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled;
- private readonly string _partitionKeyFromExtension = cosmosQueryCompilationContext.PartitionKeyFromExtension;
+
+ private readonly PartitionKey _partitionKeyValueFromExtension = cosmosQueryCompilationContext.PartitionKeyValueFromExtension
+ ?? PartitionKey.None;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -62,7 +64,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
Constant(selectExpression),
Constant(shaperLambda.Compile()),
Constant(_contextType),
- Constant(_partitionKeyFromExtension, typeof(string)),
+ Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)),
Constant(
QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution),
Constant(_threadSafetyChecksEnabled));
diff --git a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs
index 47535b31410..305ed971f9a 100644
--- a/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SelectExpression.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
@@ -19,8 +20,7 @@ public class SelectExpression : Expression
private readonly List _projection = [];
private readonly List _orderings = [];
- private ValueConverter? _partitionKeyValueConverter;
- private Expression? _partitionKeyValue;
+ private readonly List<(Expression ValueExpression, IProperty Property)> _partitionKeyValues = new();
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -144,11 +144,8 @@ public virtual Expression GetMappedProjection(ProjectionMember projectionMember)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual void SetPartitionKey(IProperty partitionKeyProperty, Expression expression)
- {
- _partitionKeyValueConverter = partitionKeyProperty.GetTypeMapping().Converter;
- _partitionKeyValue = expression;
- }
+ public virtual void AddPartitionKey(IProperty partitionKeyProperty, Expression expression)
+ => _partitionKeyValues.Add((expression, partitionKeyProperty));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -156,21 +153,28 @@ public virtual void SetPartitionKey(IProperty partitionKeyProperty, Expression e
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual string? GetPartitionKey(IReadOnlyDictionary parameterValues)
+ public virtual PartitionKey GetPartitionKeyValue(IReadOnlyDictionary parameterValues)
{
- return _partitionKeyValue switch
+ if (!_partitionKeyValues.Any())
{
- ConstantExpression constantExpression
- => GetString(_partitionKeyValueConverter, constantExpression.Value),
- ParameterExpression parameterExpression when parameterValues.TryGetValue(parameterExpression.Name!, out var value)
- => GetString(_partitionKeyValueConverter, value),
- _ => null
- };
+ return PartitionKey.None;
+ }
+
+ var builder = new PartitionKeyBuilder();
+ foreach (var tuple in _partitionKeyValues)
+ {
+ var rawKeyValue = tuple.ValueExpression switch
+ {
+ ConstantExpression constantExpression
+ => constantExpression.Value,
+ ParameterExpression parameterExpression when parameterValues.TryGetValue(parameterExpression.Name!, out var value)
+ => value,
+ _ => null
+ };
+ builder.Add(rawKeyValue, tuple.Property);
+ }
- static string? GetString(ValueConverter? converter, object? value)
- => converter is null
- ? (string?)value
- : (string?)converter.ConvertToProvider(value);
+ return builder.Build();
}
///
diff --git a/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs
index f761557b183..d0761cd9ecb 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ContainerProperties.cs
@@ -25,7 +25,7 @@ public readonly record struct ContainerProperties
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public readonly string PartitionKey;
+ public readonly IReadOnlyList PartitionKeyStoreNames;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -59,13 +59,13 @@ public readonly record struct ContainerProperties
///
public ContainerProperties(
string containerId,
- string partitionKey,
+ IReadOnlyList partitionKeyStoreNames,
int? analyticalTtl,
int? defaultTtl,
ThroughputProperties? throughput)
{
Id = containerId;
- PartitionKey = partitionKey;
+ PartitionKeyStoreNames = partitionKeyStoreNames;
AnalyticalStoreTimeToLiveInSeconds = analyticalTtl;
DefaultTimeToLive = defaultTtl;
Throughput = throughput;
@@ -79,13 +79,13 @@ public ContainerProperties(
///
public void Deconstruct(
out string containerId,
- out string partitionKey,
+ out IReadOnlyList partitionKeyStoreNames,
out int? analyticalTtl,
out int? defaultTtl,
out ThroughputProperties? throughput)
{
containerId = Id;
- partitionKey = PartitionKey;
+ partitionKeyStoreNames = PartitionKeyStoreNames;
analyticalTtl = AnalyticalStoreTimeToLiveInSeconds;
defaultTtl = DefaultTimeToLive;
throughput = Throughput;
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index 2f19ee60b13..62aa38750f8 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -8,6 +8,7 @@
using System.Text;
using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
+using Microsoft.EntityFrameworkCore.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -201,8 +202,9 @@ private static async Task CreateContainerIfNotExistsOnceAsync(
CancellationToken cancellationToken = default)
{
var (parameters, wrapper) = parametersTuple;
+ var partitionKeyPaths = parameters.PartitionKeyStoreNames.Select(e => "/" + e).ToList();
var response = await wrapper.Client.GetDatabase(wrapper._databaseId).CreateContainerIfNotExistsAsync(
- new Azure.Cosmos.ContainerProperties(parameters.Id, "/" + parameters.PartitionKey)
+ new Azure.Cosmos.ContainerProperties(parameters.Id, partitionKeyPaths)
{
PartitionKeyDefinitionVersion = PartitionKeyDefinitionVersion.V2,
DefaultTimeToLive = parameters.DefaultTimeToLive,
@@ -267,11 +269,11 @@ private static async Task CreateItemOnceAsync(
var wrapper = parameters.Wrapper;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite);
- var partitionKey = CreatePartitionKey(entry);
+ var partitionKeyValue = ExtractPartitionKeyValue(entry);
var response = await container.CreateItemStreamAsync(
stream,
- partitionKey == null ? PartitionKey.None : new PartitionKey(partitionKey),
+ partitionKeyValue,
itemRequestOptions,
cancellationToken)
.ConfigureAwait(false);
@@ -282,7 +284,7 @@ private static async Task CreateItemOnceAsync(
response.Headers.ActivityId,
parameters.Document["id"].ToString(),
parameters.ContainerId,
- partitionKey);
+ partitionKeyValue);
ProcessResponse(response, entry);
@@ -343,12 +345,12 @@ private static async Task ReplaceItemOnceAsync(
var wrapper = parameters.Wrapper;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite);
- var partitionKey = CreatePartitionKey(entry);
+ var partitionKeyValue = ExtractPartitionKeyValue(entry);
using var response = await container.ReplaceItemStreamAsync(
stream,
parameters.ResourceId,
- partitionKey == null ? PartitionKey.None : new PartitionKey(partitionKey),
+ partitionKeyValue,
itemRequestOptions,
cancellationToken)
.ConfigureAwait(false);
@@ -359,7 +361,7 @@ private static async Task ReplaceItemOnceAsync(
response.Headers.ActivityId,
parameters.ResourceId,
parameters.ContainerId,
- partitionKey);
+ partitionKeyValue);
ProcessResponse(response, entry);
@@ -410,11 +412,11 @@ private static async Task DeleteItemOnceAsync(
var items = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite);
- var partitionKey = CreatePartitionKey(entry);
+ var partitionKeyValue = ExtractPartitionKeyValue(entry);
using var response = await items.DeleteItemStreamAsync(
parameters.ResourceId,
- partitionKey == null ? PartitionKey.None : new PartitionKey(partitionKey),
+ partitionKeyValue,
itemRequestOptions,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -425,7 +427,7 @@ private static async Task DeleteItemOnceAsync(
response.Headers.ActivityId,
parameters.ResourceId,
parameters.ContainerId,
- partitionKey);
+ partitionKeyValue);
ProcessResponse(response, entry);
@@ -477,23 +479,21 @@ private static async Task DeleteItemOnceAsync(
return new ItemRequestOptions { IfMatchEtag = (string?)etag, EnableContentResponseOnWrite = enabledContentResponse };
}
- private static string? CreatePartitionKey(IUpdateEntry entry)
+ private static PartitionKey ExtractPartitionKeyValue(IUpdateEntry entry)
{
- object? partitionKey = null;
- var partitionKeyPropertyName = entry.EntityType.GetPartitionKeyPropertyName();
- if (partitionKeyPropertyName != null)
+ var partitionKeyProperties = entry.EntityType.GetPartitionKeyProperties();
+ if (!partitionKeyProperties.Any())
{
- var partitionKeyProperty = entry.EntityType.FindProperty(partitionKeyPropertyName)!;
- partitionKey = entry.GetCurrentValue(partitionKeyProperty);
+ return PartitionKey.None;
+ }
- var converter = partitionKeyProperty.GetTypeMapping().Converter;
- if (converter != null)
- {
- partitionKey = converter.ConvertToProvider(partitionKey);
- }
+ var builder = new PartitionKeyBuilder();
+ foreach (var property in partitionKeyProperties)
+ {
+ builder.Add(entry.GetCurrentValue(property), property);
}
- return (string?)partitionKey;
+ return builder.Build();
}
private static void ProcessResponse(ResponseMessage response, IUpdateEntry entry)
@@ -527,14 +527,14 @@ private static void ProcessResponse(ResponseMessage response, IUpdateEntry entry
///
public virtual IEnumerable ExecuteSqlQuery(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery query)
{
_databaseLogger.SyncNotSupported();
- _commandLogger.ExecutingSqlQuery(containerId, partitionKey, query);
+ _commandLogger.ExecutingSqlQuery(containerId, partitionKeyValue, query);
- return new DocumentEnumerable(this, containerId, partitionKey, query);
+ return new DocumentEnumerable(this, containerId, partitionKeyValue, query);
}
///
@@ -545,12 +545,12 @@ public virtual IEnumerable ExecuteSqlQuery(
///
public virtual IAsyncEnumerable ExecuteSqlQueryAsync(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery query)
{
- _commandLogger.ExecutingSqlQuery(containerId, partitionKey, query);
+ _commandLogger.ExecutingSqlQuery(containerId, partitionKeyValue, query);
- return new DocumentAsyncEnumerable(this, containerId, partitionKey, query);
+ return new DocumentAsyncEnumerable(this, containerId, partitionKeyValue, query);
}
///
@@ -561,14 +561,14 @@ public virtual IAsyncEnumerable ExecuteSqlQueryAsync(
///
public virtual JObject? ExecuteReadItem(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
string resourceId)
{
_databaseLogger.SyncNotSupported();
- _commandLogger.ExecutingReadItem(containerId, partitionKey, resourceId);
+ _commandLogger.ExecutingReadItem(containerId, partitionKeyValue, resourceId);
- var response = _executionStrategy.Execute((containerId, partitionKey, resourceId, this), CreateSingleItemQuery, null);
+ var response = _executionStrategy.Execute((containerId, partitionKeyValue, resourceId, this), CreateSingleItemQuery, null);
_commandLogger.ExecutedReadItem(
response.Diagnostics.GetClientElapsedTime(),
@@ -576,7 +576,7 @@ public virtual IAsyncEnumerable ExecuteSqlQueryAsync(
response.Headers.ActivityId,
resourceId,
containerId,
- partitionKey);
+ partitionKeyValue);
return JObjectFromReadItemResponseMessage(response);
}
@@ -589,14 +589,14 @@ public virtual IAsyncEnumerable ExecuteSqlQueryAsync(
///
public virtual async Task ExecuteReadItemAsync(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
string resourceId,
CancellationToken cancellationToken = default)
{
- _commandLogger.ExecutingReadItem(containerId, partitionKey, resourceId);
+ _commandLogger.ExecutingReadItem(containerId, partitionKeyValue, resourceId);
var response = await _executionStrategy.ExecuteAsync(
- (containerId, partitionKey, resourceId, this),
+ (containerId, partitionKeyValue, resourceId, this),
CreateSingleItemQueryAsync,
null,
cancellationToken)
@@ -608,27 +608,27 @@ public virtual IAsyncEnumerable ExecuteSqlQueryAsync(
response.Headers.ActivityId,
resourceId,
containerId,
- partitionKey);
+ partitionKeyValue);
return JObjectFromReadItemResponseMessage(response);
}
private static ResponseMessage CreateSingleItemQuery(
DbContext? context,
- (string ContainerId, string? PartitionKey, string ResourceId, CosmosClientWrapper Wrapper) parameters)
+ (string ContainerId, PartitionKey PartitionKeyValue, string ResourceId, CosmosClientWrapper Wrapper) parameters)
=> CreateSingleItemQueryAsync(context, parameters).GetAwaiter().GetResult();
private static Task CreateSingleItemQueryAsync(
DbContext? _,
- (string ContainerId, string? PartitionKey, string ResourceId, CosmosClientWrapper Wrapper) parameters,
+ (string ContainerId, PartitionKey PartitionKeyValue, string ResourceId, CosmosClientWrapper Wrapper) parameters,
CancellationToken cancellationToken = default)
{
- var (containerId, partitionKey, resourceId, wrapper) = parameters;
+ var (containerId, partitionKeyValue, resourceId, wrapper) = parameters;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(containerId);
return container.ReadItemStreamAsync(
resourceId,
- string.IsNullOrEmpty(partitionKey) ? PartitionKey.None : new PartitionKey(partitionKey),
+ partitionKeyValue,
cancellationToken: cancellationToken);
}
@@ -658,7 +658,7 @@ private static Task CreateSingleItemQueryAsync(
///
public virtual FeedIterator CreateQuery(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery query)
{
var container = Client.GetDatabase(_databaseId).GetContainer(containerId);
@@ -669,12 +669,12 @@ public virtual FeedIterator CreateQuery(
queryDefinition,
(current, parameter) => current.WithParameter(parameter.Name, parameter.Value));
- if (string.IsNullOrEmpty(partitionKey))
+ if (partitionKeyValue == PartitionKey.None)
{
return container.GetItemQueryStreamIterator(queryDefinition);
}
- var queryRequestOptions = new QueryRequestOptions { PartitionKey = new PartitionKey(partitionKey) };
+ var queryRequestOptions = new QueryRequestOptions { PartitionKey = partitionKeyValue };
return container.GetItemQueryStreamIterator(queryDefinition, requestOptions: queryRequestOptions);
}
@@ -722,18 +722,18 @@ private sealed class DocumentEnumerable : IEnumerable
{
private readonly CosmosClientWrapper _cosmosClient;
private readonly string _containerId;
- private readonly string? _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly CosmosSqlQuery _cosmosSqlQuery;
public DocumentEnumerable(
CosmosClientWrapper cosmosClient,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery cosmosSqlQuery)
{
_cosmosClient = cosmosClient;
_containerId = containerId;
- _partitionKey = partitionKey;
+ _partitionKeyValue = partitionKeyValue;
_cosmosSqlQuery = cosmosSqlQuery;
}
@@ -747,7 +747,7 @@ private sealed class Enumerator : IEnumerator
{
private readonly CosmosClientWrapper _cosmosClientWrapper;
private readonly string _containerId;
- private readonly string? _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly CosmosSqlQuery _cosmosSqlQuery;
private JObject? _current;
@@ -762,7 +762,7 @@ public Enumerator(DocumentEnumerable documentEnumerable)
{
_cosmosClientWrapper = documentEnumerable._cosmosClient;
_containerId = documentEnumerable._containerId;
- _partitionKey = documentEnumerable._partitionKey;
+ _partitionKeyValue = documentEnumerable._partitionKeyValue;
_cosmosSqlQuery = documentEnumerable._cosmosSqlQuery;
}
@@ -777,7 +777,7 @@ public bool MoveNext()
{
if (_jsonReader == null)
{
- _query ??= _cosmosClientWrapper.CreateQuery(_containerId, _partitionKey, _cosmosSqlQuery);
+ _query ??= _cosmosClientWrapper.CreateQuery(_containerId, _partitionKeyValue, _cosmosSqlQuery);
if (!_query.HasMoreResults)
{
@@ -792,7 +792,7 @@ public bool MoveNext()
_responseMessage.Headers.RequestCharge,
_responseMessage.Headers.ActivityId,
_containerId,
- _partitionKey,
+ _partitionKeyValue,
_cosmosSqlQuery);
_responseMessage.EnsureSuccessStatusCode();
@@ -840,18 +840,18 @@ private sealed class DocumentAsyncEnumerable : IAsyncEnumerable
{
private readonly CosmosClientWrapper _cosmosClient;
private readonly string _containerId;
- private readonly string? _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly CosmosSqlQuery _cosmosSqlQuery;
public DocumentAsyncEnumerable(
CosmosClientWrapper cosmosClient,
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery cosmosSqlQuery)
{
_cosmosClient = cosmosClient;
_containerId = containerId;
- _partitionKey = partitionKey;
+ _partitionKeyValue = partitionKeyValue;
_cosmosSqlQuery = cosmosSqlQuery;
}
@@ -862,7 +862,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator
{
private readonly CosmosClientWrapper _cosmosClientWrapper;
private readonly string _containerId;
- private readonly string? _partitionKey;
+ private readonly PartitionKey _partitionKeyValue;
private readonly CosmosSqlQuery _cosmosSqlQuery;
private readonly CancellationToken _cancellationToken;
@@ -881,7 +881,7 @@ public AsyncEnumerator(DocumentAsyncEnumerable documentEnumerable, CancellationT
{
_cosmosClientWrapper = documentEnumerable._cosmosClient;
_containerId = documentEnumerable._containerId;
- _partitionKey = documentEnumerable._partitionKey;
+ _partitionKeyValue = documentEnumerable._partitionKeyValue;
_cosmosSqlQuery = documentEnumerable._cosmosSqlQuery;
_cancellationToken = cancellationToken;
}
@@ -893,7 +893,7 @@ public async ValueTask MoveNextAsync()
if (_jsonReader == null)
{
- _query ??= _cosmosClientWrapper.CreateQuery(_containerId, _partitionKey, _cosmosSqlQuery);
+ _query ??= _cosmosClientWrapper.CreateQuery(_containerId, _partitionKeyValue, _cosmosSqlQuery);
if (!_query.HasMoreResults)
{
@@ -908,7 +908,7 @@ public async ValueTask MoveNextAsync()
_responseMessage.Headers.RequestCharge,
_responseMessage.Headers.ActivityId,
_containerId,
- _partitionKey,
+ _partitionKeyValue,
_cosmosSqlQuery);
_responseMessage.EnsureSuccessStatusCode();
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs
index 826c9c759f4..9ea755ce142 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs
@@ -108,14 +108,17 @@ private static IEnumerable GetContainersToCreate(IModel mod
foreach (var (containerName, mappedTypes) in containers)
{
- string? partitionKey = null;
+ IReadOnlyList partitionKeyStoreNames = Array.Empty();
int? analyticalTtl = null;
int? defaultTtl = null;
ThroughputProperties? throughput = null;
foreach (var entityType in mappedTypes)
{
- partitionKey ??= GetPartitionKeyStoreName(entityType);
+ if (!partitionKeyStoreNames.Any())
+ {
+ partitionKeyStoreNames = GetPartitionKeyStoreNames(entityType);
+ }
analyticalTtl ??= entityType.GetAnalyticalStoreTimeToLive();
defaultTtl ??= entityType.GetDefaultTimeToLive();
throughput ??= entityType.GetThroughput();
@@ -123,7 +126,7 @@ private static IEnumerable GetContainersToCreate(IModel mod
yield return new ContainerProperties(
containerName,
- partitionKey!,
+ partitionKeyStoreNames,
analyticalTtl,
defaultTtl,
throughput);
@@ -213,6 +216,7 @@ public virtual Task CanConnectAsync(CancellationToken cancellationToken =
///
/// The entity type to get the partition key property name for.
/// The name of the partition key property.
+ [Obsolete("Use GetPartitionKeyStoreNames")]
private static string GetPartitionKeyStoreName(IEntityType entityType)
{
var name = entityType.GetPartitionKeyPropertyName();
@@ -220,4 +224,17 @@ private static string GetPartitionKeyStoreName(IEntityType entityType)
? entityType.FindProperty(name)!.GetJsonPropertyName()
: CosmosClientWrapper.DefaultPartitionKey;
}
+
+ ///
+ /// Returns the store names of the properties that is used to store the partition keys.
+ ///
+ /// The entity type to get the partition key property names for.
+ /// The names of the partition key property.
+ private static IReadOnlyList GetPartitionKeyStoreNames(IEntityType entityType)
+ {
+ var properties = entityType.GetPartitionKeyProperties();
+ return properties.Any()
+ ? properties.Select(p => p.GetJsonPropertyName()).ToList()
+ : [CosmosClientWrapper.DefaultPartitionKey];
+ }
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
index 149fc7ad2f7..fa8ba51f089 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
@@ -135,7 +135,7 @@ Task DeleteItemAsync(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- FeedIterator CreateQuery(string containerId, string? partitionKey, CosmosSqlQuery query);
+ FeedIterator CreateQuery(string containerId, PartitionKey partitionKeyValue, CosmosSqlQuery query);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -145,7 +145,7 @@ Task DeleteItemAsync(
///
JObject? ExecuteReadItem(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
string resourceId);
///
@@ -156,7 +156,7 @@ Task DeleteItemAsync(
///
Task ExecuteReadItemAsync(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
string resourceId,
CancellationToken cancellationToken = default);
@@ -168,7 +168,7 @@ Task DeleteItemAsync(
///
IEnumerable ExecuteSqlQuery(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery query);
///
@@ -179,6 +179,6 @@ IEnumerable ExecuteSqlQuery(
///
IAsyncEnumerable ExecuteSqlQueryAsync(
string containerId,
- string? partitionKey,
+ PartitionKey partitionKeyValue,
CosmosSqlQuery query);
}
diff --git a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs
index 70bb2c43bc1..4b7725d55d2 100644
--- a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs
+++ b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs
@@ -52,10 +52,10 @@ protected override object NextValue(EntityEntry entry)
builder.Append('|');
}
- var partitionKey = entityType.GetPartitionKeyPropertyName();
+ var partitionKeyNames = entityType.GetPartitionKeyPropertyNames();
foreach (var property in primaryKey.Properties)
{
- if (property.Name == partitionKey
+ if (partitionKeyNames.Contains(property.Name)
&& primaryKey.Properties.Count > 1)
{
continue;
diff --git a/src/Shared/ExpressionExtensions.cs b/src/Shared/ExpressionExtensions.cs
index aa46cfaa56e..0835ed7eb3f 100644
--- a/src/Shared/ExpressionExtensions.cs
+++ b/src/Shared/ExpressionExtensions.cs
@@ -46,7 +46,7 @@ private static Expression RemoveConvert(Expression expression)
: expression;
public static T GetConstantValue(this Expression expression)
- => expression switch
+ => RemoveConvert(expression) switch
{
ConstantExpression constantExpression => (T)constantExpression.Value!,
#pragma warning disable EF9100 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
diff --git a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs
index 3f495ae7291..ed27624a809 100644
--- a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs
@@ -92,6 +92,10 @@ public override DateTime DefaultDateTime
public override bool PreservesDateTimeKind
=> true;
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(
+ w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs
index 24b7cc8f91e..d118974f945 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs
@@ -43,6 +43,7 @@ public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => builder.EnableThreadSafetyChecks(enableChecks: false);
+ => base.AddOptions(builder.EnableThreadSafetyChecks(enableChecks: false))
+ .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs
index 44afb345745..03c35306ac1 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs
@@ -19,5 +19,8 @@ protected override ITestStoreFactory TestStoreFactory
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
index 344cbbe1f50..c369bdbfeab 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
@@ -148,6 +148,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
public class CosmosFixture : ServiceProviderFixtureBase
{
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 5e04d803e0c..0c67b0c6e25 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -199,6 +199,7 @@ protected override void OnModelCreating(ModelBuilder builder)
b.HasKey(c => c.Id);
b.Property(c => c.ETag).IsETagConcurrency();
b.OwnsMany(x => x.Children);
+ b.HasPartitionKey(c => c.Id);
});
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs
index d1c24275fa3..cd96d45af64 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs
@@ -177,6 +177,9 @@ public override bool PreservesDateTimeKind
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);
diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
index bd9f333f820..c316e339b06 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
@@ -705,6 +705,9 @@ await TestStore.InitializeAsync(
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
=> ((EmbeddedTransportationContext)context).Options.OnModelCreating?.Invoke(modelBuilder);
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override object GetAdditionalModelCacheKey(DbContext context)
{
var options = ((EmbeddedTransportationContext)context).Options;
diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
index 5f5a6627b0e..99e997bc69f 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
@@ -19,7 +19,7 @@ public async Task Can_add_update_delete_end_to_end()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -104,7 +104,7 @@ public async Task Can_add_update_delete_end_to_end_async()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -182,7 +182,7 @@ public async Task Can_add_update_delete_detached_entity_end_to_end_async()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
string storeId = null;
@@ -261,7 +261,7 @@ public async Task Can_add_update_untracked_properties()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -357,7 +357,7 @@ public async Task Can_add_update_untracked_properties_async()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -588,14 +588,17 @@ private class Customer
{
public int Id { get; set; }
public string Name { get; set; }
- public int PartitionKey { get; set; }
+ public int PartitionKey1 { get; set; }
+ public bool PartitionKey3 { get; set; }
+ public string PartitionKey2 { get; set; }
}
private class CustomerWithResourceId
{
public string id { get; set; }
public string Name { get; set; }
- public int PartitionKey { get; set; }
+ public int PartitionKey1 { get; set; }
+ public decimal PartitionKey2 { get; set; }
}
private class CustomerGuid
@@ -625,7 +628,7 @@ public async Task Can_add_update_delete_with_dateTime_string_end_to_end_async()
var contextFactory = await InitializeAsync(
b => b.Entity(),
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "2021-08-23T06:23:40+00:00" };
@@ -686,7 +689,7 @@ public async Task Entities_with_null_PK_can_be_added_with_normal_use_of_DbContex
var contextFactory = await InitializeAsync(
usePooling: false,
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var context = contextFactory.CreateContext();
var item = new GItem();
@@ -711,7 +714,7 @@ public async Task
var contextFactory = await InitializeAsync(
usePooling: false,
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var context = contextFactory.CreateContext();
@@ -940,7 +943,7 @@ private async Task Can_add_update_delete_with_collection(
var contextFactory = await InitializeAsync>(
shouldLogCategory: _ => true,
onModelCreating: onModelBuilder,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new CustomerWithCollection
{
@@ -1022,7 +1025,8 @@ public async Task Can_read_with_find_with_resource_id_async()
{
id = "42",
Name = "Theon",
- PartitionKey = pk1
+ PartitionKey1 = pk1,
+ PartitionKey2 = 3.15m
};
await using (var context = contextFactory.CreateContext())
@@ -1039,7 +1043,8 @@ await context.AddAsync(
{
id = "42",
Name = "Theon Twin",
- PartitionKey = pk2
+ PartitionKey1 = pk2,
+ PartitionKey2 = 3.15m
});
await context.SaveChangesAsync();
@@ -1048,12 +1053,13 @@ await context.AddAsync(
await using (var context = contextFactory.CreateContext())
{
var customerFromStore = await context.Set()
- .FindAsync(pk1, "42");
+ .FindAsync(pk1, 3.15m, "42");
Assert.Equal("42", customerFromStore.id);
Assert.Equal("Theon", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
- AssertSql(context, @"ReadItem(1, 42)");
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal(3.15m, customerFromStore.PartitionKey2);
+ AssertSql(context, """ReadItem([1.0,3.15], 42)""");
customerFromStore.Name = "Theon Greyjoy";
@@ -1063,12 +1069,13 @@ await context.AddAsync(
await using (var context = contextFactory.CreateContext())
{
var customerFromStore = await context.Set()
- .WithPartitionKey(partitionKey: pk1.ToString())
+ .WithPartitionKey(pk1, 3.15m)
.FirstAsync();
Assert.Equal("42", customerFromStore.id);
Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal(3.15m, customerFromStore.PartitionKey2);
}
}
@@ -1086,7 +1093,8 @@ public async Task Can_read_with_find_with_resource_id()
{
id = "42",
Name = "Theon",
- PartitionKey = pk1
+ PartitionKey1 = pk1,
+ PartitionKey2 = 3.15m
};
using (var context = contextFactory.CreateContext())
@@ -1099,7 +1107,8 @@ public async Task Can_read_with_find_with_resource_id()
{
id = "42",
Name = "Theon Twin",
- PartitionKey = pk2
+ PartitionKey1 = pk2,
+ PartitionKey2 = 3.15m
});
context.SaveChanges();
@@ -1108,12 +1117,13 @@ public async Task Can_read_with_find_with_resource_id()
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .Find(pk1, "42");
+ .Find(pk1, 3.15m, "42");
Assert.Equal("42", customerFromStore.id);
Assert.Equal("Theon", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
- AssertSql(context, @"ReadItem(1, 42)");
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal(3.15m, customerFromStore.PartitionKey2);
+ AssertSql(context, """ReadItem([1.0,3.15], 42)""");
customerFromStore.Name = "Theon Greyjoy";
@@ -1123,12 +1133,13 @@ public async Task Can_read_with_find_with_resource_id()
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .WithPartitionKey(partitionKey: pk1.ToString())
+ .WithPartitionKey(pk1, 3.15m)
.First();
Assert.Equal("42", customerFromStore.id);
Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal(3.15m, customerFromStore.PartitionKey2);
}
}
@@ -1145,7 +1156,7 @@ public async Task Find_with_empty_resource_id_throws()
Assert.Equal(
CosmosStrings.InvalidResourceId,
- Assert.Throws(() => context.Set().Find(1, "")).Message);
+ Assert.Throws(() => context.Set().Find(1, 3.15m, "")).Message);
}
}
@@ -1163,7 +1174,9 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator_asyn
{
Id = 42,
Name = "Theon",
- PartitionKey = pk1
+ PartitionKey1 = pk1,
+ PartitionKey2 = "One",
+ PartitionKey3 = true
};
await using (var context = contextFactory.CreateContext())
@@ -1176,7 +1189,9 @@ await context.AddAsync(
{
Id = 42,
Name = "Theon Twin",
- PartitionKey = pk2
+ PartitionKey1 = pk2,
+ PartitionKey2 = "Two",
+ PartitionKey3 = false
});
await context.SaveChangesAsync();
@@ -1185,11 +1200,13 @@ await context.AddAsync(
await using (var context = contextFactory.CreateContext())
{
var customerFromStore = await context.Set()
- .FindAsync(pk1, 42);
+ .FindAsync(pk1, 42, "One", true);
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
customerFromStore.Name = "Theon Greyjoy";
@@ -1199,12 +1216,14 @@ await context.AddAsync(
await using (var context = contextFactory.CreateContext())
{
var customerFromStore = await context.Set()
- .WithPartitionKey(partitionKey: pk1.ToString())
+ .WithPartitionKey(pk1, "One", true)
.FirstAsync();
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
}
}
@@ -1222,7 +1241,9 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator()
{
Id = 42,
Name = "Theon",
- PartitionKey = pk1
+ PartitionKey1 = pk1,
+ PartitionKey2 = "One",
+ PartitionKey3 = true
};
using (var context = contextFactory.CreateContext())
@@ -1235,7 +1256,9 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator()
{
Id = 42,
Name = "Theon Twin",
- PartitionKey = pk2
+ PartitionKey1 = pk2,
+ PartitionKey2 = "Two",
+ PartitionKey3 = false
});
context.SaveChanges();
@@ -1244,12 +1267,14 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator()
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .Find(pk1, 42);
+ .Find(pk1, 42, "One", true);
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
- AssertSql(context, @"ReadItem(1, Customer-42)");
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
+ AssertSql(context, """ReadItem([1.0,"One",true], Customer-42)""");
customerFromStore.Name = "Theon Greyjoy";
@@ -1259,12 +1284,14 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator()
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .WithPartitionKey(partitionKey: pk1.ToString())
+ .WithPartitionKey(pk1, "One", true)
.First();
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
}
}
@@ -1281,7 +1308,9 @@ public async Task Can_read_with_find_with_partition_key_without_value_generator(
{
Id = 42,
Name = "Theon",
- PartitionKey = pk1
+ PartitionKey1 = pk1,
+ PartitionKey2 = "One",
+ PartitionKey3 = true
};
using (var context = contextFactory.CreateContext())
@@ -1298,19 +1327,21 @@ public async Task Can_read_with_find_with_partition_key_without_value_generator(
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .Find(pk1, 42);
+ .Find(pk1, "One", true, 42);
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
AssertSql(
context,
"""
-@__p_1='42'
+@__p_3='42'
SELECT c
FROM root c
-WHERE ((c["Discriminator"] = "Customer") AND (c["Id"] = @__p_1))
+WHERE ((c["Discriminator"] = "Customer") AND (c["Id"] = @__p_3))
OFFSET 0 LIMIT 1
""");
@@ -1322,12 +1353,14 @@ OFFSET 0 LIMIT 1
using (var context = contextFactory.CreateContext())
{
var customerFromStore = context.Set()
- .WithPartitionKey(partitionKey: pk1.ToString())
+ .WithPartitionKey(pk1, "One", true)
.First();
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- Assert.Equal(pk1, customerFromStore.PartitionKey);
+ Assert.Equal(pk1, customerFromStore.PartitionKey1);
+ Assert.Equal("One", customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
}
}
@@ -1336,13 +1369,15 @@ public async Task Can_read_with_find_with_partition_key_not_part_of_primary_key(
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer
{
Id = 42,
Name = "Theon",
- PartitionKey = 1
+ PartitionKey1 = 1,
+ PartitionKey2 = "One",
+ PartitionKey3 = true
};
using (var context = contextFactory.CreateContext())
@@ -1360,7 +1395,7 @@ public async Task Can_read_with_find_with_partition_key_not_part_of_primary_key(
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- AssertSql(context, "ReadItem(, Customer|42)");
+ AssertSql(context, """ReadItem(None, Customer|42)""");
}
}
@@ -1369,7 +1404,7 @@ public async Task Can_read_with_find_without_partition_key()
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new CustomerNoPartitionKey { Id = 42, Name = "Theon" };
@@ -1388,7 +1423,7 @@ public async Task Can_read_with_find_without_partition_key()
Assert.Equal(42, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- AssertSql(context, @"ReadItem(, CustomerNoPartitionKey|42)");
+ AssertSql(context, @"ReadItem(None, CustomerNoPartitionKey|42)");
}
}
@@ -1416,7 +1451,7 @@ public async Task Can_read_with_find_with_PK_partition_key()
Assert.Equal(customer.Id, customerFromStore.Id);
Assert.Equal("Theon", customerFromStore.Name);
- AssertSql(context, @$"ReadItem({customer.Id}, {customer.Id})");
+ AssertSql(context, @$"ReadItem([""{customer.Id}""], {customer.Id})");
}
}
@@ -1463,9 +1498,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity(
cb =>
{
- cb.HasPartitionKey(c => c.PartitionKey);
- cb.Property(c => c.PartitionKey).HasConversion();
- cb.HasKey(c => new { c.Id, c.PartitionKey });
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
+ cb.HasKey(c => new { c.Id, c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
});
}
@@ -1484,10 +1518,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
cb.Property(StoreKeyConvention.DefaultIdPropertyName)
.HasValueGeneratorFactory(typeof(CustomPartitionKeyIdValueGeneratorFactory));
- cb.Property(c => c.PartitionKey).HasConversion();
-
- cb.HasPartitionKey(c => c.PartitionKey);
- cb.HasKey(c => new { c.PartitionKey, c.Id });
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
+ cb.HasKey(c => new { c.PartitionKey1, c.Id, c.PartitionKey2, c.PartitionKey3 });
});
}
@@ -1499,10 +1531,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
cb.Property(StoreKeyConvention.DefaultIdPropertyName).HasValueGenerator((Type)null);
- cb.Property(c => c.PartitionKey).HasConversion();
-
- cb.HasPartitionKey(c => c.PartitionKey);
- cb.HasKey(c => new { c.PartitionKey, c.Id });
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
+ cb.HasKey(c => new { c.PartitionKey1, c.PartitionKey2, c.PartitionKey3, c.Id });
});
}
@@ -1529,8 +1559,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity(
cb =>
{
- cb.HasPartitionKey(c => c.PartitionKey);
- cb.Property(c => c.PartitionKey).HasConversion();
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2 } );
cb.Property(c => c.id).HasConversion();
cb.HasKey(c => new { c.id });
});
@@ -1542,9 +1571,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity(
cb =>
{
- cb.HasPartitionKey(c => c.PartitionKey);
- cb.Property(c => c.PartitionKey).HasConversion();
- cb.HasKey(c => new { c.PartitionKey, c.id });
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2 } );
+ cb.HasKey(c => new { c.PartitionKey1, c.PartitionKey2, c.id });
});
}
@@ -1553,7 +1581,7 @@ public async Task Can_use_detached_entities_without_discriminators()
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -1604,7 +1632,7 @@ public async Task Can_update_unmapped_properties()
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -1665,7 +1693,7 @@ public async Task Can_use_non_persisted_properties()
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var customer = new Customer { Id = 42, Name = "Theon" };
@@ -1787,7 +1815,7 @@ public async Task Can_add_update_delete_end_to_end_with_conflicting_id()
{
var contextFactory = await InitializeAsync(
shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
var entity = new ConflictingId { id = "42", Name = "Theon" };
@@ -1870,7 +1898,7 @@ public async Task Can_have_non_string_property_named_Discriminator(bool useDiscr
b.Entity();
}
},
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)));
+ onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported, CosmosEventId.NoPartitionKeyDefined)));
using var context = contextFactory.CreateContext();
context.Database.EnsureCreated();
diff --git a/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs
index 6e4167c5cce..804a3eda4ac 100644
--- a/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs
@@ -16,6 +16,9 @@ protected override ITestStoreFactory TestStoreFactory
public override TestHelpers TestHelpers
=> CosmosTestHelpers.Instance;
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override void BuildModelExternal(ModelBuilder modelBuilder)
{
base.BuildModelExternal(modelBuilder);
diff --git a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs
index 2bfef6105d8..3897e3be08d 100644
--- a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs
@@ -92,6 +92,9 @@ public class FindCosmosFixture : FindFixtureBase
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs
new file mode 100644
index 00000000000..1292167d7d5
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/HierarchicalPartitionKeyTest.cs
@@ -0,0 +1,293 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore;
+
+public class HierarchicalPartitionKeyTest : IClassFixture
+{
+ private const string DatabaseName = nameof(HierarchicalPartitionKeyTest);
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+
+ protected void ClearLog()
+ => Fixture.TestSqlLoggerFactory.Clear();
+
+ protected CosmosHierarchicalPartitionKeyFixture Fixture { get; }
+
+ public HierarchicalPartitionKeyTest(CosmosHierarchicalPartitionKeyFixture fixture)
+ {
+ Fixture = fixture;
+ ClearLog();
+ }
+
+ [ConditionalFact]
+ public virtual async Task Can_add_update_delete_end_to_end_with_partition_key()
+ {
+ const string read1Sql =
+ """
+SELECT c
+FROM root c
+WHERE (c["Discriminator"] = "Customer")
+ORDER BY c["PartitionKey1"]
+OFFSET 0 LIMIT 1
+""";
+
+ const string read2Sql =
+ """
+@__p_0='1'
+
+SELECT c
+FROM root c
+WHERE (c["Discriminator"] = "Customer")
+ORDER BY c["PartitionKey1"]
+OFFSET @__p_0 LIMIT 1
+""";
+
+ await PartitionKeyTestAsync(
+ ctx => ctx.Customers.OrderBy(c => c.PartitionKey1).FirstAsync(),
+ read1Sql,
+ ctx => ctx.Customers.OrderBy(c => c.PartitionKey1).Skip(1).FirstAsync(),
+ read2Sql,
+ ctx => ctx.Customers.OrderBy(c => c.PartitionKey1).LastAsync(),
+ ctx => ctx.Customers.OrderBy(c => c.PartitionKey1).ToListAsync(),
+ 2);
+ }
+
+ [ConditionalFact]
+ public virtual async Task Can_add_update_delete_end_to_end_with_with_partition_key_extension()
+ {
+ const string readSql =
+ """
+SELECT c
+FROM root c
+WHERE (c["Discriminator"] = "Customer")
+OFFSET 0 LIMIT 1
+""";
+
+ await PartitionKeyTestAsync(
+ ctx => ctx.Customers.WithPartitionKey("A", 1.1, true).FirstAsync(),
+ readSql,
+ ctx => ctx.Customers.WithPartitionKey("B", 2.1, false).FirstAsync(),
+ readSql,
+ ctx => ctx.Customers.WithPartitionKey("B", 2.1, false).LastAsync(),
+ ctx => ctx.Customers.WithPartitionKey("B", 2.1, false).ToListAsync(),
+ 1);
+ }
+
+ [ConditionalFact]
+ public async Task Can_query_with_implicit_partition_key_filter()
+ {
+ const string readSql =
+ """
+SELECT c
+FROM root c
+WHERE (c["Discriminator"] = "Customer")
+OFFSET 0 LIMIT 1
+""";
+
+ await PartitionKeyTestAsync(
+ ctx => ctx.Customers
+ .Where(
+ b => (b.Id == 42 || b.Name == "John Snow")
+ && b.PartitionKey1 == "A"
+ && b.PartitionKey2 == 1.1
+ && b.PartitionKey3)
+ .FirstAsync(),
+ readSql,
+ ctx => ctx.Customers
+ .Where(
+ b => (b.Id == 42 || b.Name == "John Snow")
+ && b.PartitionKey1 == "B"
+ && b.PartitionKey2 == 2.1
+ && !b.PartitionKey3)
+ .FirstAsync(),
+ readSql,
+ ctx => ctx.Customers.WithPartitionKey("B", 2.1, false).LastAsync(),
+ ctx => ctx.Customers
+ .Where(
+ b => b.Id == 42
+ && ((b.PartitionKey1 == "A" && b.PartitionKey2 == 1.1 && b.PartitionKey3)
+ || (b.PartitionKey1 == "B" && b.PartitionKey2 == 2.1 && !b.PartitionKey3)))
+ .ToListAsync(),
+ 2);
+ }
+
+ protected virtual async Task PartitionKeyTestAsync(
+ Func> read1SingleTask,
+ string read1Sql,
+ Func> read2SingleTask,
+ string read2Sql,
+ Func> readLastTask,
+ Func>> readListTask,
+ int listCount)
+ {
+ var customer1 = new Customer
+ {
+ Id = 42,
+ Name = "Theon",
+ PartitionKey1 = "A",
+ PartitionKey2 = 1.1,
+ PartitionKey3 = true,
+ };
+
+ var customer2 = new Customer
+ {
+ Id = 42,
+ Name = "Theon Twin",
+ PartitionKey1 = "B",
+ PartitionKey2 = 2.1,
+ PartitionKey3 = false,
+ };
+
+ await using (var innerContext = CreateContext())
+ {
+ await innerContext.Database.EnsureCreatedAsync();
+
+ await innerContext.AddAsync(customer1);
+ await innerContext.AddAsync(customer2);
+ await innerContext.SaveChangesAsync();
+ }
+
+ // Read & update in first partition
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read1SingleTask(innerContext);
+
+ AssertSql(read1Sql);
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal("Theon", customerFromStore.Name);
+ Assert.Equal("A", customerFromStore.PartitionKey1);
+ Assert.Equal(1.1, customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
+
+ customerFromStore.Name = "Theon Greyjoy";
+
+ await innerContext.SaveChangesAsync();
+ }
+
+ // Read & update in second partition
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read2SingleTask(innerContext);
+
+ AssertSql(read2Sql);
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal("Theon Twin", customerFromStore.Name);
+ Assert.Equal("B", customerFromStore.PartitionKey1);
+ Assert.Equal(2.1, customerFromStore.PartitionKey2);
+ Assert.False(customerFromStore.PartitionKey3);
+
+ customerFromStore.Name = "Theon Bluejoy";
+
+ await innerContext.SaveChangesAsync();
+ }
+
+ // Read list from all partitions
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await readListTask(innerContext);
+
+ Assert.Equal(listCount, customerFromStore.Count);
+ }
+
+ // Test exceptions
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read1SingleTask(innerContext);
+ customerFromStore.PartitionKey1 = "C";
+
+ Assert.Equal(
+ CoreStrings.KeyReadOnly(nameof(Customer.PartitionKey1), nameof(Customer)),
+ Assert.Throws(() => innerContext.SaveChanges()).Message);
+ }
+
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read1SingleTask(innerContext);
+ customerFromStore.PartitionKey2 = 2.1;
+
+ Assert.Equal(
+ CoreStrings.KeyReadOnly(nameof(Customer.PartitionKey2), nameof(Customer)),
+ Assert.Throws(() => innerContext.SaveChanges()).Message);
+ }
+
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read1SingleTask(innerContext);
+ customerFromStore.PartitionKey3 = false;
+
+ Assert.Equal(
+ CoreStrings.KeyReadOnly(nameof(Customer.PartitionKey3), nameof(Customer)),
+ Assert.Throws(() => innerContext.SaveChanges()).Message);
+ }
+
+ // Read update & delete
+ await using (var innerContext = CreateContext())
+ {
+ var customerFromStore = await read1SingleTask(innerContext);
+
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal("Theon Greyjoy", customerFromStore.Name);
+ Assert.Equal("A", customerFromStore.PartitionKey1);
+ Assert.Equal(1.1, customerFromStore.PartitionKey2);
+ Assert.True(customerFromStore.PartitionKey3);
+
+ innerContext.Remove(customerFromStore);
+
+ var lastTask = await readLastTask(innerContext);
+
+ innerContext.Remove(lastTask);
+
+ await innerContext.SaveChangesAsync();
+ }
+
+ await using (var innerContext = CreateContext())
+ {
+ Assert.Empty(await readListTask(innerContext));
+ }
+ }
+
+ protected HierarchicalPartitionKeyContext CreateContext()
+ => Fixture.CreateContext();
+
+ public class CosmosHierarchicalPartitionKeyFixture : SharedStoreFixtureBase
+ {
+ protected override string StoreName
+ => DatabaseName;
+
+ protected override bool UsePooling
+ => false;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService();
+ }
+
+ public class HierarchicalPartitionKeyContext(DbContextOptions dbContextOptions) : DbContext(dbContextOptions)
+ {
+ public virtual DbSet Customers
+ => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity(
+ cb =>
+ {
+ cb.HasPartitionKey(c => new { c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
+ cb.HasKey(c => new { c.Id, c.PartitionKey1, c.PartitionKey2, c.PartitionKey3 });
+ });
+ }
+
+ public class Customer
+ {
+ public int Id { get; set; }
+ public string? Name { get; set; }
+ public string? PartitionKey1 { get; set; }
+ public double PartitionKey2 { get; set; }
+ public bool PartitionKey3 { get; set; }
+ }
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs
index 29140b79416..c06af99c669 100644
--- a/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/KeysWithConvertersCosmosTest.cs
@@ -545,6 +545,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
}
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder.ConfigureWarnings(w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning)));
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning, CosmosEventId.NoPartitionKeyDefined)));
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs
index b58c881d792..c94bf70fc5f 100644
--- a/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/MaterializationInterceptionCosmosTest.cs
@@ -20,7 +20,28 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
- modelBuilder.Entity();
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id).ValueGeneratedOnAdd();
+ b.HasPartitionKey(e => e.Title);
+ b.HasKey(e => new { e.Id, e.Title });
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.Property(e => e.Id).ValueGeneratedOnAdd();
+ b.HasPartitionKey(e => e.Title);
+ b.HasKey(e => new { e.Id, e.Title });
+ });
+
+ modelBuilder.Entity(
+ b =>
+ {
+ b.HasPartitionKey(e => e.Title);
+ b.HasKey(e => new { e.Id, e.Title });
+ });
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs
index 8c0fb56dbf7..4b609fa503e 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs
@@ -92,6 +92,59 @@ public virtual void Partition_key_is_added_to_the_keys()
Assert.NotNull(idProperty.GetValueGeneratorFactory());
}
+ [ConditionalFact]
+ public virtual void Hierarchical_partition_key_is_added_to_the_keys()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity()
+ .Ignore(b => b.Details)
+ .Ignore(b => b.Orders)
+ .HasPartitionKey(b => new { b.Title, b.Name });
+
+ var model = modelBuilder.FinalizeModel();
+
+ var entity = model.FindEntityType(typeof(Customer))!;
+
+ Assert.Equal(
+ new[] { nameof(Customer.Title), nameof(Customer.Name) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
+ Assert.Equal(
+ new[] { StoreKeyConvention.DefaultIdPropertyName, nameof(Customer.Title), nameof(Customer.Name) },
+ entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
+
+ var idProperty = entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)!;
+ Assert.Single(idProperty.GetContainingKeys());
+ Assert.NotNull(idProperty.GetValueGeneratorFactory());
+ }
+
+ [ConditionalFact]
+ public virtual void Three_level_hierarchical_partition_key_is_added_to_the_keys()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity()
+ .Ignore(b => b.Details)
+ .Ignore(b => b.Orders)
+ .HasPartitionKey(b => new { b.Title, b.Name, b.AlternateKey })
+ .Property(b => b.AlternateKey).HasConversion();
+
+ var model = modelBuilder.FinalizeModel();
+
+ var entity = model.FindEntityType(typeof(Customer))!;
+
+ Assert.Equal(
+ new[] { nameof(Customer.Title), nameof(Customer.Name), nameof(Customer.AlternateKey) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
+ Assert.Equal(
+ new[] { StoreKeyConvention.DefaultIdPropertyName, nameof(Customer.Title), nameof(Customer.Name), nameof(Customer.AlternateKey) },
+ entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
+
+ var idProperty = entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)!;
+ Assert.Single(idProperty.GetContainingKeys());
+ Assert.NotNull(idProperty.GetValueGeneratorFactory());
+ }
+
[ConditionalFact]
public virtual void Partition_key_is_added_to_the_alternate_key_if_primary_key_contains_id()
{
@@ -116,6 +169,45 @@ public virtual void Partition_key_is_added_to_the_alternate_key_if_primary_key_c
entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
}
+ [ConditionalFact]
+ public virtual void Hierarchical_partition_key_is_added_to_the_alternate_key_if_primary_key_contains_id()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Entity().HasKey(StoreKeyConvention.DefaultIdPropertyName);
+ modelBuilder.Entity()
+ .Ignore(b => b.Details)
+ .Ignore(b => b.Orders)
+ .HasPartitionKey(
+ b => new
+ {
+ b.AlternateKey,
+ b.Name,
+ b.Title
+ })
+ .Property(b => b.AlternateKey).HasConversion();
+
+ var model = modelBuilder.FinalizeModel();
+
+ var entity = model.FindEntityType(typeof(Customer))!;
+
+ Assert.Equal(
+ new[] { nameof(Customer.AlternateKey), nameof(Customer.Name), nameof(Customer.Title) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
+ Assert.Equal(
+ new[] { StoreKeyConvention.DefaultIdPropertyName },
+ entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
+ Assert.Equal(
+ new[]
+ {
+ StoreKeyConvention.DefaultIdPropertyName,
+ nameof(Customer.AlternateKey),
+ nameof(Customer.Name),
+ nameof(Customer.Title)
+ },
+ entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
+ }
+
[ConditionalFact]
public virtual void No_id_property_created_if_another_property_mapped_to_id()
{
@@ -214,81 +306,67 @@ public virtual void No_alternate_key_is_created_if_primary_key_contains_id_and_p
}
[ConditionalFact]
- public virtual void No_alternate_key_is_created_if_id_is_partition_key()
+ public virtual void No_alternate_key_is_created_if_primary_key_contains_id_and_hierarchical_partition_key()
{
var modelBuilder = CreateModelBuilder();
- modelBuilder.Entity().HasKey(nameof(Customer.AlternateKey));
+ modelBuilder.Entity().HasKey(
+ nameof(Customer.AlternateKey),
+ nameof(Customer.Name),
+ nameof(Customer.Title),
+ StoreKeyConvention.DefaultIdPropertyName);
modelBuilder.Entity()
.Ignore(b => b.Details)
.Ignore(b => b.Orders)
- .HasPartitionKey(b => b.AlternateKey)
- .Property(b => b.AlternateKey).HasConversion().ToJsonProperty("id");
+ .HasPartitionKey(
+ b => new
+ {
+ b.AlternateKey,
+ b.Name,
+ b.Title
+ })
+ .Property(b => b.AlternateKey).HasConversion();
var model = modelBuilder.FinalizeModel();
var entity = model.FindEntityType(typeof(Customer))!;
Assert.Equal(
- new[] { nameof(Customer.AlternateKey) },
- entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
- Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
- }
-
- protected override TestModelBuilder CreateModelBuilder(Action? configure = null)
- => new GenericTestModelBuilder(Fixture, configure);
- }
-
- public class CosmosGenericComplexType(CosmosModelBuilderFixture fixture) : ComplexTypeTestBase(fixture), IClassFixture
- {
- public override void Properties_can_have_custom_type_value_converter_type_set()
- => Properties_can_have_custom_type_value_converter_type_set();
+ new[] { nameof(Customer.AlternateKey), nameof(Customer.Name), nameof(Customer.Title) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
- public override void Properties_can_have_non_generic_value_converter_set()
- => Properties_can_have_non_generic_value_converter_set();
-
- public override void Properties_can_have_provider_type_set()
- => Properties_can_have_provider_type_set();
-
- public override void Can_set_complex_property_annotation()
- {
- var modelBuilder = CreateModelBuilder();
-
- var complexPropertyBuilder = modelBuilder
- .Ignore()
- .Entity()
- .ComplexProperty(e => e.Customer)
- .HasTypeAnnotation("foo", "bar")
- .HasPropertyAnnotation("foo2", "bar2")
- .Ignore(c => c.Details)
- .Ignore(c => c.Orders);
-
- var model = modelBuilder.FinalizeModel();
- var complexProperty = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single();
-
- Assert.Equal("bar", complexProperty.ComplexType["foo"]);
- Assert.Equal("bar2", complexProperty["foo2"]);
- Assert.Equal(typeof(Customer).Name, complexProperty.Name);
Assert.Equal(
- @"Customer (Customer) Required
- ComplexType: ComplexProperties.Customer#Customer
- Properties: "
- + @"
- AlternateKey (Guid) Required
- Id (int) Required
- Name (string)
- Notes (List) Element type: string Required", complexProperty.ToDebugString(), ignoreLineEndingDifferences: true);
+ new[]
+ {
+ nameof(Customer.AlternateKey),
+ nameof(Customer.Name),
+ nameof(Customer.Title),
+ StoreKeyConvention.DefaultIdPropertyName
+ },
+ entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
+ Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
}
[ConditionalFact]
- public virtual void Partition_key_is_added_to_the_keys()
+ public virtual void No_alternate_key_is_created_if_primary_key_contains_id_and_hierarchical_partition_key_in_different_order()
{
var modelBuilder = CreateModelBuilder();
+ modelBuilder.Entity().HasKey(
+ nameof(Customer.Title),
+ nameof(Customer.Name),
+ nameof(Customer.AlternateKey),
+ StoreKeyConvention.DefaultIdPropertyName);
modelBuilder.Entity()
.Ignore(b => b.Details)
.Ignore(b => b.Orders)
- .HasPartitionKey(b => b.AlternateKey)
+ .HasPartitionKey(
+ b => new
+ {
+ b.AlternateKey,
+ b.Name,
+ b.Title
+ })
.Property(b => b.AlternateKey).HasConversion();
var model = modelBuilder.FinalizeModel();
@@ -296,27 +374,41 @@ public virtual void Partition_key_is_added_to_the_keys()
var entity = model.FindEntityType(typeof(Customer))!;
Assert.Equal(
- new[] { nameof(Customer.Id), nameof(Customer.AlternateKey) },
- entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
- Assert.Equal(
- new[] { StoreKeyConvention.DefaultIdPropertyName, nameof(Customer.AlternateKey) },
- entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
+ new[] { nameof(Customer.AlternateKey), nameof(Customer.Name), nameof(Customer.Title) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
- var idProperty = entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)!;
- Assert.Single(idProperty.GetContainingKeys());
- Assert.NotNull(idProperty.GetValueGeneratorFactory());
+ Assert.Equal(
+ new[]
+ {
+ nameof(Customer.Title),
+ nameof(Customer.Name),
+ nameof(Customer.AlternateKey),
+ StoreKeyConvention.DefaultIdPropertyName
+ },
+ entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
+ Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
}
[ConditionalFact]
- public virtual void Partition_key_is_added_to_the_alternate_key_if_primary_key_contains_id()
+ public virtual void Hierarchical_partition_key_is_added_to_the_alternate_key_if_primary_key_contains_part_of_partition_key()
{
var modelBuilder = CreateModelBuilder();
- modelBuilder.Entity().HasKey(StoreKeyConvention.DefaultIdPropertyName);
+ modelBuilder.Entity().HasKey(
+ nameof(Customer.Title),
+ nameof(Customer.AlternateKey),
+ StoreKeyConvention.DefaultIdPropertyName);
+
modelBuilder.Entity()
.Ignore(b => b.Details)
.Ignore(b => b.Orders)
- .HasPartitionKey(b => b.AlternateKey)
+ .HasPartitionKey(
+ b => new
+ {
+ b.Title,
+ b.AlternateKey,
+ b.Name
+ })
.Property(b => b.AlternateKey).HasConversion();
var model = modelBuilder.FinalizeModel();
@@ -324,130 +416,117 @@ public virtual void Partition_key_is_added_to_the_alternate_key_if_primary_key_c
var entity = model.FindEntityType(typeof(Customer))!;
Assert.Equal(
- new[] { StoreKeyConvention.DefaultIdPropertyName },
+ new[] { nameof(Customer.Title), nameof(Customer.AlternateKey), nameof(Customer.Name) },
+ entity.GetPartitionKeyProperties().Select(p => p.Name));
+
+ Assert.Equal(
+ new[]
+ {
+ nameof(Customer.Title),
+ nameof(Customer.AlternateKey),
+ StoreKeyConvention.DefaultIdPropertyName
+ },
entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
+
Assert.Equal(
- new[] { StoreKeyConvention.DefaultIdPropertyName, nameof(Customer.AlternateKey) },
+ new[]
+ {
+ StoreKeyConvention.DefaultIdPropertyName,
+ nameof(Customer.Title),
+ nameof(Customer.AlternateKey),
+ nameof(Customer.Name)
+ },
entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name));
}
[ConditionalFact]
- public virtual void No_id_property_created_if_another_property_mapped_to_id()
+ public virtual void No_alternate_key_is_created_if_id_is_partition_key()
{
var modelBuilder = CreateModelBuilder();
- modelBuilder.Entity()
- .Property(c => c.Name)
- .ToJsonProperty(StoreKeyConvention.IdPropertyJsonName);
+ modelBuilder.Entity().HasKey(nameof(Customer.AlternateKey));
modelBuilder.Entity()
.Ignore(b => b.Details)
- .Ignore(b => b.Orders);
-
- var model = modelBuilder.FinalizeModel();
-
- var entity = model.FindEntityType(typeof(Customer))!;
-
- Assert.Null(entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName));
- Assert.Single(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
-
- var idProperty = entity.GetDeclaredProperties()
- .Single(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName);
- Assert.Single(idProperty.GetContainingKeys());
- Assert.NotNull(idProperty.GetValueGeneratorFactory());
- }
-
- [ConditionalFact]
- public virtual void No_id_property_created_if_another_property_mapped_to_id_in_pk()
- {
- var modelBuilder = CreateModelBuilder();
-
- modelBuilder.Entity()
- .Property(c => c.Name)
- .ToJsonProperty(StoreKeyConvention.IdPropertyJsonName);
- modelBuilder.Entity()
- .Ignore(c => c.Details)
- .Ignore(c => c.Orders)
- .HasKey(c => c.Name);
+ .Ignore(b => b.Orders)
+ .HasPartitionKey(b => b.AlternateKey)
+ .Property(b => b.AlternateKey).HasConversion().ToJsonProperty("id");
var model = modelBuilder.FinalizeModel();
var entity = model.FindEntityType(typeof(Customer))!;
- Assert.Null(entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName));
+ Assert.Equal(
+ new[] { nameof(Customer.AlternateKey) },
+ entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
-
- var idProperty = entity.GetDeclaredProperties()
- .Single(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName);
- Assert.Single(idProperty.GetContainingKeys());
- Assert.Null(idProperty.GetValueGeneratorFactory());
}
[ConditionalFact]
- public virtual void No_alternate_key_is_created_if_primary_key_contains_id()
+ public virtual void No_alternate_key_is_created_if_id_is_hierarchical_partition_key()
{
var modelBuilder = CreateModelBuilder();
- modelBuilder.Entity().HasKey(StoreKeyConvention.DefaultIdPropertyName);
+ modelBuilder.Entity().HasKey(e => new { e.Name, e.AlternateKey, e.Title });
modelBuilder.Entity()
.Ignore(b => b.Details)
- .Ignore(b => b.Orders);
+ .Ignore(b => b.Orders)
+ .HasPartitionKey(b => new { b.Name, b.AlternateKey, b.Title })
+ .Property(b => b.AlternateKey).HasConversion().ToJsonProperty("id");
var model = modelBuilder.FinalizeModel();
var entity = model.FindEntityType(typeof(Customer))!;
Assert.Equal(
- new[] { StoreKeyConvention.DefaultIdPropertyName },
+ new[] { nameof(Customer.Name), nameof(Customer.AlternateKey), nameof(Customer.Title) },
entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
-
- var idProperty = entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)!;
- Assert.Single(idProperty.GetContainingKeys());
- Assert.Null(idProperty.GetValueGeneratorFactory());
}
- [ConditionalFact]
- public virtual void No_alternate_key_is_created_if_primary_key_contains_id_and_partition_key()
- {
- var modelBuilder = CreateModelBuilder();
-
- modelBuilder.Entity().HasKey(nameof(Customer.AlternateKey), StoreKeyConvention.DefaultIdPropertyName);
- modelBuilder.Entity()
- .Ignore(b => b.Details)
- .Ignore(b => b.Orders)
- .HasPartitionKey(b => b.AlternateKey)
- .Property(b => b.AlternateKey).HasConversion();
+ protected override TestModelBuilder CreateModelBuilder(Action? configure = null)
+ => new GenericTestModelBuilder(Fixture, configure);
+ }
- var model = modelBuilder.FinalizeModel();
+ public class CosmosGenericComplexType(CosmosModelBuilderFixture fixture) : ComplexTypeTestBase(fixture), IClassFixture
+ {
+ public override void Properties_can_have_custom_type_value_converter_type_set()
+ => Properties_can_have_custom_type_value_converter_type_set();
- var entity = model.FindEntityType(typeof(Customer))!;
+ public override void Properties_can_have_non_generic_value_converter_set()
+ => Properties_can_have_non_generic_value_converter_set();
- Assert.Equal(
- new[] { nameof(Customer.AlternateKey), StoreKeyConvention.DefaultIdPropertyName },
- entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
- Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
- }
+ public override void Properties_can_have_provider_type_set()
+ => Properties_can_have_provider_type_set();
- [ConditionalFact]
- public virtual void No_alternate_key_is_created_if_id_is_partition_key()
+ public override void Can_set_complex_property_annotation()
{
var modelBuilder = CreateModelBuilder();
- modelBuilder.Entity().HasKey(nameof(Customer.AlternateKey));
- modelBuilder.Entity()
- .Ignore(b => b.Details)
- .Ignore(b => b.Orders)
- .HasPartitionKey(b => b.AlternateKey)
- .Property(b => b.AlternateKey).HasConversion().ToJsonProperty("id");
+ var complexPropertyBuilder = modelBuilder
+ .Ignore()
+ .Entity()
+ .ComplexProperty(e => e.Customer)
+ .HasTypeAnnotation("foo", "bar")
+ .HasPropertyAnnotation("foo2", "bar2")
+ .Ignore(c => c.Details)
+ .Ignore(c => c.Orders);
var model = modelBuilder.FinalizeModel();
+ var complexProperty = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single();
- var entity = model.FindEntityType(typeof(Customer))!;
-
+ Assert.Equal("bar", complexProperty.ComplexType["foo"]);
+ Assert.Equal("bar2", complexProperty["foo2"]);
+ Assert.Equal(typeof(Customer).Name, complexProperty.Name);
Assert.Equal(
- new[] { nameof(Customer.AlternateKey) },
- entity.FindPrimaryKey()!.Properties.Select(p => p.Name));
- Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey()));
+ @"Customer (Customer) Required
+ ComplexType: ComplexProperties.Customer#Customer
+ Properties: "
+ + @"
+ AlternateKey (Guid) Required
+ Id (int) Required
+ Name (string)
+ Notes (List) Element type: string Required
+ Title (string) Required", complexProperty.ToDebugString(), ignoreLineEndingDifferences: true);
}
protected override TestModelBuilder CreateModelBuilder(Action? configure = null)
@@ -586,8 +665,76 @@ public virtual void Can_use_shared_type_as_join_entity_with_partition_keys()
Assert.Equal(3, joinType.FindPrimaryKey()!.Properties.Count);
Assert.Equal(6, joinType.GetProperties().Count());
Assert.Equal("DbContext", joinType.GetContainer());
- Assert.Equal("PartitionId", joinType.GetPartitionKeyPropertyName());
+ Assert.Equal(["PartitionId"], joinType.GetPartitionKeyPropertyNames());
Assert.Equal("PartitionId", joinType.FindPrimaryKey()!.Properties.Last().Name);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ Assert.Equal("PartitionId", joinType.GetPartitionKeyPropertyName());
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ [ConditionalFact]
+ public virtual void Can_use_shared_type_as_join_entity_with_hierarchical_partition_keys()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+
+ modelBuilder.Entity(
+ mb =>
+ {
+ mb.Property("PartitionId1");
+ mb.Property("PartitionId2");
+ mb.Property("PartitionId3");
+ mb.HasPartitionKey("PartitionId1", "PartitionId2", "PartitionId3");
+ });
+
+ modelBuilder.Entity(
+ mb =>
+ {
+ mb.Property("PartitionId1");
+ mb.Property("PartitionId2");
+ mb.Property("PartitionId3");
+ mb.HasPartitionKey("PartitionId1", "PartitionId2", "PartitionId3");
+ });
+
+ modelBuilder.Entity()
+ .HasMany(e => e.Dependents)
+ .WithMany(e => e.ManyToManyPrincipals)
+ .UsingEntity>(
+ "JoinType",
+ e => e.HasOne().WithMany().HasAnnotation("Right", "Foo"),
+ e => e.HasOne().WithMany().HasAnnotation("Left", "Bar"));
+
+ modelBuilder.Entity()
+ .HasMany(e => e.Dependents)
+ .WithMany(e => e.ManyToManyPrincipals)
+ .UsingEntity>(
+ "JoinType",
+ e => e.HasOne().WithMany().HasForeignKey("DependentId", "PartitionId1", "PartitionId2", "PartitionId3"),
+ e => e.HasOne().WithMany().HasForeignKey("PrincipalId", "PartitionId1", "PartitionId2", "PartitionId3"),
+ e => e.HasPartitionKey("PartitionId1", "PartitionId2", "PartitionId3"));
+
+ var model = modelBuilder.FinalizeModel();
+
+ var joinType = model.FindEntityType("JoinType")!;
+ Assert.NotNull(joinType);
+ Assert.Collection(joinType.GetForeignKeys(),
+ fk => Assert.Equal("Foo", fk["Right"]),
+ fk => Assert.Equal("Bar", fk["Left"]));
+
+ Assert.Equal(
+ new[] { "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.GetPartitionKeyProperties().Select(p => p.Name));
+
+ Assert.Equal(
+ new[] { "DependentId", "PrincipalId", "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.FindPrimaryKey()!.Properties.Select(p => p.Name));
+
+ Assert.Equal(
+ new[] { "__id", "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.GetKeys().Single(k => k != joinType.FindPrimaryKey()).Properties.Select(p => p.Name));
}
[ConditionalFact]
@@ -625,8 +772,67 @@ public virtual void Can_use_implicit_join_entity_with_partition_keys()
Assert.Equal(3, joinType.FindPrimaryKey()!.Properties.Count);
Assert.Equal(6, joinType.GetProperties().Count());
Assert.Equal("DbContext", joinType.GetContainer());
- Assert.Equal("PartitionId", joinType.GetPartitionKeyPropertyName());
+ Assert.Equal(["PartitionId"], joinType.GetPartitionKeyPropertyNames());
Assert.Equal("PartitionId", joinType.FindPrimaryKey()!.Properties.Last().Name);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ Assert.Equal("PartitionId", joinType.GetPartitionKeyPropertyName());
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ [ConditionalFact]
+ public virtual void Can_use_implicit_join_entity_with_hierarchical_partition_keys()
+ {
+ var modelBuilder = CreateModelBuilder();
+
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+
+ modelBuilder.Entity(
+ mb =>
+ {
+ mb.Ignore(e => e.Dependents);
+ mb.Property("PartitionId1");
+ mb.Property("PartitionId2");
+ mb.Property("PartitionId3");
+ mb.HasPartitionKey("PartitionId1", "PartitionId2", "PartitionId3");
+ });
+
+ modelBuilder.Entity(
+ mb =>
+ {
+ mb.Property("PartitionId1");
+ mb.Property("PartitionId2");
+ mb.Property("PartitionId3");
+ mb.HasPartitionKey("PartitionId1", "PartitionId2", "PartitionId3");
+ });
+
+ modelBuilder.Entity()
+ .HasMany(e => e.Dependents)
+ .WithMany(e => e.ManyToManyPrincipals);
+
+ var model = modelBuilder.FinalizeModel();
+
+ var joinType = model.FindEntityType("ManyToManyNavPrincipalNavDependent");
+ Assert.NotNull(joinType);
+
+ Assert.Equal(
+ new[] { "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.GetPartitionKeyProperties().Select(p => p.Name));
+
+ Assert.Equal(
+ new[] { "Id", "Id1", "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.FindPrimaryKey()!.Properties.Select(p => p.Name));
+
+ Assert.Equal(
+ new[] { "__id", "PartitionId1", "PartitionId2", "PartitionId3" },
+ joinType.GetKeys().Single(k => k != joinType.FindPrimaryKey()).Properties.Select(p => p.Name));
+
+
+ Assert.Equal(2, joinType.GetForeignKeys().Count());
+ Assert.Equal(5, joinType.FindPrimaryKey()!.Properties.Count);
+ Assert.Equal(8, joinType.GetProperties().Count());
+ Assert.Equal("DbContext", joinType.GetContainer());
}
[ConditionalFact]
@@ -673,8 +879,12 @@ public virtual void Can_use_implicit_join_entity_with_partition_keys_changed()
Assert.Equal(3, joinType.FindPrimaryKey()!.Properties.Count);
Assert.Equal(6, joinType.GetProperties().Count());
Assert.Equal("DbContext", joinType.GetContainer());
- Assert.Equal("Partition2Id", joinType.GetPartitionKeyPropertyName());
+ Assert.Equal(["Partition2Id"], joinType.GetPartitionKeyPropertyNames());
Assert.Equal("Partition2Id", joinType.FindPrimaryKey()!.Properties.Last().Name);
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ Assert.Equal("Partition2Id", joinType.GetPartitionKeyPropertyName());
+#pragma warning restore CS0618 // Type or member is obsolete
}
public override void Join_type_is_automatically_configured_by_convention()
diff --git a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosTestModelBuilderExtensions.cs b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosTestModelBuilderExtensions.cs
index 02f6e01bdad..ac71f179084 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosTestModelBuilderExtensions.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosTestModelBuilderExtensions.cs
@@ -18,8 +18,8 @@ public static ModelBuilderTest.TestEntityTypeBuilder HasPartitionKey nonGenericBuilder:
- var memberInfo = propertyExpression.GetMemberAccess();
- nonGenericBuilder.Instance.HasPartitionKey(memberInfo.Name);
+ var names = propertyExpression.GetMemberAccessList().Select(e => e.GetSimpleMemberName()).ToList();
+ nonGenericBuilder.Instance.HasPartitionKey(names.FirstOrDefault(), names.Count > 1 ? names.Skip(1).ToArray() : []);
break;
}
@@ -28,16 +28,17 @@ public static ModelBuilderTest.TestEntityTypeBuilder HasPartitionKey HasPartitionKey(
this ModelBuilderTest.TestEntityTypeBuilder builder,
- string name)
+ string name,
+ params string[] additionalPropertyNames)
where TEntity : class
{
switch (builder)
{
case IInfrastructure> genericBuilder:
- genericBuilder.Instance.HasPartitionKey(name);
+ genericBuilder.Instance.HasPartitionKey(name, additionalPropertyNames);
break;
case IInfrastructure nonGenericBuilder:
- nonGenericBuilder.Instance.HasPartitionKey(name);
+ nonGenericBuilder.Instance.HasPartitionKey(name, additionalPropertyNames);
break;
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/OverzealousInitializationCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/OverzealousInitializationCosmosTest.cs
index 095f877ed0a..f0b85ef9408 100644
--- a/test/EFCore.Cosmos.FunctionalTests/OverzealousInitializationCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/OverzealousInitializationCosmosTest.cs
@@ -15,6 +15,10 @@ public override void Fixup_ignores_eagerly_initialized_reference_navs()
public class OverzealousInitializationCosmosFixture : OverzealousInitializationFixtureBase
{
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)));
+
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosFixture.cs
index d1e8a045de6..a446aa89dee 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosFixture.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/InheritanceQueryCosmosFixture.cs
@@ -21,4 +21,8 @@ public override bool EnableComplexTypes
public Task NoSyncTest(bool async, Func testCode)
=> CosmosTestHelpers.Instance.NoSyncTest(async, testCode);
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CoreEventId.MappedEntityTypeIgnoredWarning, CosmosEventId.NoPartitionKeyDefined)));
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs
index 8e928c2d234..748a0c8eb5f 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindQueryCosmosFixture.cs
@@ -28,6 +28,10 @@ public Task NoSyncTest(bool async, Func testCode)
public void NoSyncTest(Action testCode)
=> CosmosTestHelpers.Instance.NoSyncTest(testCode);
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)));
+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
base.OnModelCreating(modelBuilder, context);
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
index 5e8cf8886aa..2b1155b1400 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
@@ -741,6 +741,10 @@ protected override ITestStoreFactory TestStoreFactory
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ServiceProvider.GetRequiredService();
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder.ConfigureWarnings(
+ w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)));
+
protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
modelBuilder.Entity(
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/QueryLoggingCosmosTestBase.cs b/test/EFCore.Cosmos.FunctionalTests/Query/QueryLoggingCosmosTestBase.cs
index 180f222cccf..f7b0aba7adb 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/QueryLoggingCosmosTestBase.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/QueryLoggingCosmosTestBase.cs
@@ -43,7 +43,7 @@ public virtual async Task Queryable_simple()
{
Assert.Equal(
CosmosResources.LogExecutingSqlQuery(new TestLogger()).GenerateMessage(
- "NorthwindContext", "(null)", "", Environment.NewLine,
+ "NorthwindContext", "None", "", Environment.NewLine,
"""
SELECT c
FROM root c
@@ -89,7 +89,7 @@ public virtual async Task Queryable_with_parameter_outputs_parameter_value_loggi
{
Assert.Equal(
CosmosResources.LogExecutingSqlQuery(new TestLogger()).GenerateMessage(
- "NorthwindContext", "(null)", "@__city_0='Redmond'", Environment.NewLine,
+ "NorthwindContext", "None", "@__city_0='Redmond'", Environment.NewLine,
"""
SELECT c
FROM root c
diff --git a/test/EFCore.Cosmos.FunctionalTests/QueryExpressionInterceptionWithDiagnosticsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/QueryExpressionInterceptionWithDiagnosticsCosmosTest.cs
index 02638ce97f9..b75a2e4645b 100644
--- a/test/EFCore.Cosmos.FunctionalTests/QueryExpressionInterceptionWithDiagnosticsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/QueryExpressionInterceptionWithDiagnosticsCosmosTest.cs
@@ -24,6 +24,9 @@ public class InterceptionCosmosFixture : InterceptionFixtureBase
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
protected override IServiceCollection InjectInterceptors(
IServiceCollection serviceCollection,
IEnumerable injectedInterceptors)
diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
index 87e38aadf10..3822e069ea0 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
@@ -78,15 +78,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
partitionId.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer(
(Nullable v1, Nullable v2) => v1.HasValue && v2.HasValue && (long)v1 == (long)v2 || !v1.HasValue && !v2.HasValue,
- (Nullable v) => v.HasValue ? ((long)v).GetHashCode() : 0,
+ (Nullable v) => v.HasValue ? ((object)(long)v).GetHashCode() : 0,
(Nullable v) => v.HasValue ? (Nullable)(long)v : default(Nullable)),
keyComparer: new ValueComparer(
(Nullable v1, Nullable v2) => v1.HasValue && v2.HasValue && (long)v1 == (long)v2 || !v1.HasValue && !v2.HasValue,
- (Nullable v) => v.HasValue ? ((long)v).GetHashCode() : 0,
+ (Nullable v) => v.HasValue ? ((object)(long)v).GetHashCode() : 0,
(Nullable v) => v.HasValue ? (Nullable)(long)v : default(Nullable)),
providerValueComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
converter: new ValueConverter(
(long v) => string.Format(CultureInfo.InvariantCulture, "{0}", (object)v),
@@ -127,19 +127,25 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
storeGenerationIndex: -1);
blob.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer(
- (byte[] v1, byte[] v2) => v1 == null ? v2 == null : v2 != null && v1.Length == v2.Length && v1 == v2 || v1.Zip(v2, (byte v1, byte v2) => v1 == v2).All((bool v) => v),
- (byte[] v) => v.Aggregate(new HashCode(), (HashCode h, byte e) => ValueComparer.Add(h, (int)e), (HashCode h) => h.ToHashCode()),
- (byte[] v) => v.Select((byte v) => v).ToArray()),
- keyComparer: new ValueComparer(
(byte[] v1, byte[] v2) => StructuralComparisons.StructuralEqualityComparer.Equals((object)v1, (object)v2),
- (byte[] v) => StructuralComparisons.StructuralEqualityComparer.GetHashCode((object)v),
- (byte[] source) => source.ToArray()),
- providerValueComparer: new ValueComparer(
+ (byte[] v) => ((object)v).GetHashCode(),
+ (byte[] v) => v),
+ keyComparer: new ValueComparer(
(byte[] v1, byte[] v2) => StructuralComparisons.StructuralEqualityComparer.Equals((object)v1, (object)v2),
(byte[] v) => StructuralComparisons.StructuralEqualityComparer.GetHashCode((object)v),
(byte[] source) => source.ToArray()),
- clrType: typeof(byte[]),
- jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance);
+ providerValueComparer: new ValueComparer(
+ (string v1, string v2) => v1 == v2,
+ (string v) => ((object)v).GetHashCode(),
+ (string v) => v),
+ converter: new ValueConverter(
+ (byte[] v) => Convert.ToBase64String(v),
+ (string v) => Convert.FromBase64String(v)),
+ jsonValueReaderWriter: new JsonConvertedValueReaderWriter(
+ JsonStringReaderWriter.Instance,
+ new ValueConverter(
+ (byte[] v) => Convert.ToBase64String(v),
+ (string v) => Convert.FromBase64String(v))));
blob.AddAnnotation("Cosmos:PropertyName", "JsonBlob");
blob.AddRuntimeAnnotation("UnsafeAccessors", new[] { ("DataEntityType.UnsafeAccessor_Microsoft_EntityFrameworkCore_Scaffolding_Data_Blob", "TestNamespace") });
@@ -157,15 +163,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
__id.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
keyComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
providerValueComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
clrType: typeof(string),
jsonValueReaderWriter: JsonStringReaderWriter.Instance);
@@ -188,15 +194,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
__jObject.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer(
(JObject v1, JObject v2) => object.Equals(v1, v2),
- (JObject v) => v.GetHashCode(),
+ (JObject v) => ((object)v).GetHashCode(),
(JObject v) => v),
keyComparer: new ValueComparer(
(JObject v1, JObject v2) => object.Equals(v1, v2),
- (JObject v) => v.GetHashCode(),
+ (JObject v) => ((object)v).GetHashCode(),
(JObject v) => v),
providerValueComparer: new ValueComparer(
(JObject v1, JObject v2) => object.Equals(v1, v2),
- (JObject v) => v.GetHashCode(),
+ (JObject v) => ((object)v).GetHashCode(),
(JObject v) => v),
clrType: typeof(JObject));
__jObject.AddAnnotation("Cosmos:PropertyName", "");
@@ -218,15 +224,15 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
_etag.TypeMapping = CosmosTypeMapping.Default.Clone(
comparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
keyComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
providerValueComparer: new ValueComparer(
(string v1, string v2) => v1 == v2,
- (string v) => v.GetHashCode(),
+ (string v) => ((object)v).GetHashCode(),
(string v) => v),
clrType: typeof(string),
jsonValueReaderWriter: JsonStringReaderWriter.Instance);
@@ -253,10 +259,10 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
(InternalEntityEntry source) =>
{
var entity = (CompiledModelTestBase.Data)source.Entity;
- return (ISnapshot)new Snapshot, byte[], string, JObject, string>(((ValueComparer)id.GetValueComparer()).Snapshot(source.GetCurrentValue(id)), source.GetCurrentValue>(partitionId) == null ? null : ((ValueComparer>)partitionId.GetValueComparer()).Snapshot(source.GetCurrentValue>(partitionId)), source.GetCurrentValue(blob) == null ? null : ((ValueComparer)blob.GetValueComparer()).Snapshot(source.GetCurrentValue(blob)), source.GetCurrentValue(__id) == null ? null : ((ValueComparer)__id.GetValueComparer()).Snapshot(source.GetCurrentValue(__id)), source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)__jObject.GetValueComparer()).Snapshot(source.GetCurrentValue(__jObject)), source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)_etag.GetValueComparer()).Snapshot(source.GetCurrentValue(_etag)));
+ return (ISnapshot)new Snapshot, byte[], string, JObject, string>(((ValueComparer)((IProperty)id).GetValueComparer()).Snapshot(source.GetCurrentValue(id)), source.GetCurrentValue>(partitionId) == null ? null : ((ValueComparer>)((IProperty)partitionId).GetValueComparer()).Snapshot(source.GetCurrentValue>(partitionId)), source.GetCurrentValue(blob) == null ? null : ((ValueComparer)((IProperty)blob).GetValueComparer()).Snapshot(source.GetCurrentValue(blob)), source.GetCurrentValue(__id) == null ? null : ((ValueComparer)((IProperty)__id).GetValueComparer()).Snapshot(source.GetCurrentValue(__id)), source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)((IProperty)__jObject).GetValueComparer()).Snapshot(source.GetCurrentValue(__jObject)), source.GetCurrentValue(_etag) == null ? null : ((ValueComparer)((IProperty)_etag).GetValueComparer()).Snapshot(source.GetCurrentValue(_etag)));
});
runtimeEntityType.SetStoreGeneratedValuesFactory(
- () => (ISnapshot)new Snapshot(default(JObject) == null ? null : ((ValueComparer)__jObject.GetValueComparer()).Snapshot(default(JObject)), default(string) == null ? null : ((ValueComparer)_etag.GetValueComparer()).Snapshot(default(string))));
+ () => (ISnapshot)new Snapshot(default(JObject) == null ? null : ((ValueComparer)((IProperty)__jObject).GetValueComparer()).Snapshot(default(JObject)), default(string) == null ? null : ((ValueComparer)((IProperty)_etag).GetValueComparer()).Snapshot(default(string))));
runtimeEntityType.SetTemporaryValuesFactory(
(InternalEntityEntry source) => (ISnapshot)new Snapshot(default(JObject), default(string)));
runtimeEntityType.SetShadowValuesFactory(
@@ -267,7 +273,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
(InternalEntityEntry source) =>
{
var entity = (CompiledModelTestBase.Data)source.Entity;
- return (ISnapshot)new Snapshot, string>(((ValueComparer)id.GetKeyValueComparer()).Snapshot(source.GetCurrentValue(id)), source.GetCurrentValue>(partitionId) == null ? null : ((ValueComparer