dotnet/efcore · error · InvalidOperationException

When using AutoTransactionBehavior.Always with the Cosmos DB

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

When AutoTransactionBehavior is Always, the Cosmos provider must put all changed entities into a single transactional batch. Cosmos transactional batches are limited to a single container AND a single partition key AND a maximum of 100 operations. If the batch exceeds 100 entities or spans multiple containers/partitions, the provider throws (CosmosDatabaseWrapper.cs:293-298) because it cannot create a valid atomic batch.

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 dbf9771522)

Solutions

  1. Group SaveChanges calls by partition key and container so each call stays within one partition and under 100 entities.
  2. Switch AutoTransactionBehavior to WhenNeeded (default) to allow automatic multi-batch processing without the atomicity constraint.
  3. Reduce the number of entities per SaveChanges call to 100 or fewer, all sharing the same partition key.

Example fix

// before
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
foreach (var item in items) context.Items.Add(item); // 150 items, mixed partitions
await context.SaveChangesAsync(); // throws

// after — batch by partition, max 100 each
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded;
await context.SaveChangesAsync();
// or manually group:
foreach (var group in items.GroupBy(i => i.PartitionKey).Select(g => g.Take(100)))
{
    // save each group in its own SaveChanges
}
Defensive patterns

Strategy: validation

Validate before calling

// Before SaveChanges with Always, verify all root entries share one partition+container and are <= 100.
if (context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)
{
    var entries = context.ChangeTracker.Entries()
        .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
        .ToList();
    if (entries.Count > 100)
        throw new InvalidOperationException("More than 100 entities with AutoTransactionBehavior.Always.");
    // Further check that all share the same container + partition key value.
}

Prevention

When it happens

Trigger: Setting AutoTransactionBehavior = Always and calling SaveChanges with: (a) more than 100 root document entries, or (b) entries spread across different containers, or (c) entries in different partition key values within the same container. The provider checks that all batchable entries share the same (ContainerId, PartitionKeyValue) and that the count is at most 100.

Common situations: Bulk-importing records across multiple partitions with Always enabled. Mixing entities mapped to different containers in one SaveChanges with Always. Exceeding the 100-entity batch limit during data migration.

Related errors


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