dotnet/efcore · error · InvalidOperationException

'WithPartitionKey' only accepts simple constant or…

Error message

'WithPartitionKey' only accepts simple constant or parameter arguments. See https://aka.ms/efdocs-cosmos-partition-keys for more information.

What it means

After translating each WithPartitionKey argument with applyDefaultTypeMapping:false, the visitor requires the result to be a SqlConstantExpression or SqlParameterExpression. Any other shape (a column reference, a computed expression, a client value that cannot be mapped) triggers InvalidOperationException.

Solutions

  1. Pass a simple constant or a parameter captured in a local variable (string, int, etc.) to WithPartitionKey.
  2. Resolve any computed partition key to a concrete value before calling WithPartitionKey.
  3. Ensure the partition key CLR type has a Cosmos type mapping registered.

Example fix

// before
var q = db.Users.WithPartitionKey(otherUser.TenantId);
// after
var tenantId = otherUser.TenantId;
var q = db.Users.WithPartitionKey(tenantId);
Defensive patterns

Strategy: validation

Validate before calling

static IQueryable<T> SafePartition<T>(IQueryable<T> q, object key)
    where T : class
{
    if (key is null) throw new ArgumentNullException(nameof(key));
    if (key is string s && s.Length == 0) throw new ArgumentException("empty partition key", nameof(key));
    if (!key.GetType().IsPrimitive && key is not string and not Guid and not DateTime and not DateTimeOffset)
        throw new ArgumentException("partition key must be a simple constant or parameter", nameof(key));
    return q.WithPartitionKey(key);
}

Type guard

static bool IsSimplePartitionValue(object v)
    => v is string or Guid or DateTime or DateTimeOffset || v?.GetType().IsPrimitive == true;

Prevention

When it happens

Trigger: Passing a non-constant/non-parameter value to WithPartitionKey — e.g. a property of another entity, the result of a method call, or an expression that requires translation rather than binding.

Common situations: Computing the partition key at runtime from another query result and passing it in, or capturing a non-simple value (struct without type mapping, dynamic) as the partition key.

Related errors


AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11). Data as JSON: /api/errors/464a038635598622. Report an issue: GitHub.

Appendix: source

Thrown at src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs:185

        {
            if (_queryCompilationContext.PartitionKeyPropertyValues.Count > 0)
            {
                throw new InvalidOperationException(CosmosStrings.WithPartitionKeyAlreadyCalled);
            }

            if (methodCallExpression.Arguments[0] is not EntityQueryRootExpression)
            {
                throw new InvalidOperationException(CosmosStrings.WithPartitionKeyBadNode);
            }

            var innerQueryable = Visit(methodCallExpression.Arguments[0]);

            for (var i = 1; i < methodCallExpression.Arguments.Count; i++)
            {
                var value = _sqlTranslator.Translate(methodCallExpression.Arguments[i], applyDefaultTypeMapping: false);
                if (value is not SqlConstantExpression and not SqlParameterExpression)
                {
                    throw new InvalidOperationException(CosmosStrings.WithPartitionKeyNotConstantOrParameter);
                }

                _queryCompilationContext.PartitionKeyPropertyValues.Add(value);
            }

            return innerQueryable;
        }

        if (method.DeclaringType == typeof(Queryable) && method.IsGenericMethod)
        {
            switch (methodCallExpression.Method.Name)
            {
                // The following is a bad hack to account for https://github.com/dotnet/efcore/issues/32957#issuecomment-2165864086.
                // Basically for the query form Where(b => b.Posts.GetElementAt(0).Id == 1), nav expansion moves the property access
                // forward, generating Where(b => b.Posts.Select(p => p.Id).GetElementAt(0)); unfortunately that means that GetElementAt()
                // over a bare array in Cosmos doesn't get translated to a simple indexer as it should (b["Posts"][0].Id), since the
                // reordering messes things up.
                case nameof(Queryable.ElementAt) or nameof(Queryable.ElementAtOrDefault)

View on GitHub (pinned to 3a2006ef56)