dotnet/efcore · error · InvalidOperationException

When using AutoTransactionBehavior.Always with the Cosmos…

Error message

When using AutoTransactionBehavior.Always with the Cosmos DB provider, all changed entities in a SaveChanges call must be in the same collection and partition and not exceed 100 entities to ensure atomicity.

What it means

With AutoTransactionBehavior.Always, EF Core wraps the batch into one Cosmos transactional batch to keep it atomic. A Cosmos transactional batch is limited to 100 operations and must target a single logical partition key in one container. If the batchable entries exceed 100 or span multiple containers/partition keys, the provider cannot keep them atomic and throws.

Solutions

  1. Reduce the batch to 100 or fewer entities per SaveChanges call.
  2. Ensure every entity in the batch shares the same container and the same partition-key value.
  3. Switch AutoTransactionBehavior away from Always for cross-partition or large bulk operations.

Example fix

// before
dbContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
foreach (var item in manyItems) dbContext.Add(item); // >100 or mixed partitions
await dbContext.SaveChangesAsync();

// after
foreach (var chunk in manyItems.Chunk(100))
{
    // ensure all in chunk share partition key + container
    using var scope = new DbContextScope(dbContext);
    foreach (var item in chunk) scope.Ctx.Add(item);
    await scope.Ctx.SaveChangesAsync();
}
Defensive patterns

Strategy: validation

Validate before calling

// Before SaveChangesAsync, enforce Always limits
if (dbContext.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)
{
    if (dbContext.ChangeTracker.Entries().Count(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) > 100)
        throw new InvalidOperationException("Chunk into <=100 entities per save under Always.");
    // also assert all entries share one container + partition key
}

Try / catch

try { await dbContext.SaveChangesAsync(ct); }
catch (InvalidOperationException ex) when (ex.Message.Contains("same collection and partition"))
{
    // re-batch: group by container/partition, chunk to <=100, then save each group.
}

Prevention

When it happens

Trigger: Calling SaveChangesAsync with AutoTransactionBehavior == Always where either batchableEntries.Count > 100 OR the entries are not all in the same container and partition key. See CosmosDatabaseWrapper.cs:295-298.

Common situations: Bulk-inserting >100 documents with Always set. Saving entities mapped to different Cosmos containers, or different partition-key values, in one SaveChanges call while demanding atomicity.

Related errors


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

Appendix: source

Thrown at src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs:298

        if (_currentDbContext.Context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)
        {
            if (singleUpdateEntries.Count >= 1)
            {
                if (rootEntriesToSave.Count >= 2)
                {
                    throw new InvalidOperationException(CosmosStrings.SaveChangesAutoTransactionBehaviorAlwaysTriggerAtomicity);
                }

                // There is only 1 entry, and it has a trigger
                return new SaveGroups { BatchableUpdateEntries = [], SingleUpdateEntries = singleUpdateEntries };
            }

            var firstEntry = batchableEntries[0];
            var key = new Grouping(firstEntry.CollectionId, _cosmosClient.GetPartitionKeyValue(firstEntry.Entry));
            return batchableEntries.Count > 100
                || !batchableEntries.All(entry =>
                    entry.CollectionId == key.ContainerId && _cosmosClient.GetPartitionKeyValue(entry.Entry) == key.PartitionKeyValue)
                    ? throw new InvalidOperationException(CosmosStrings.SaveChangesAutoTransactionBehaviorAlwaysAtomicity)
                    : new SaveGroups { BatchableUpdateEntries = [(key, batchableEntries)], SingleUpdateEntries = [] };
        }

        var batches = CreateBatches(batchableEntries);

        // For bulk it is important that single writes are always classified as singleUpdateEntries so that they will be executed in parallel
        if (_bulkExecutionEnabled && _currentDbContext.Context.Database.AutoTransactionBehavior != AutoTransactionBehavior.Always)
        {
            for (var i = batches.Count - 1; i >= 0; i--)
            {
                var (Key, UpdateEntries) = batches[i];
                if (UpdateEntries.Count == 1)
                {
                    batches.RemoveAt(i);
                    singleUpdateEntries.Add(UpdateEntries[0]);
                }
            }
        }

View on GitHub (pinned to 3a2006ef56)