dotnet/efcore · error · InvalidOperationException

When using AutoTransactionBehavior.Always with the Cosmos DB

Error message

When using AutoTransactionBehavior.Always with the Cosmos DB provider, only 1 entity can be saved at a time when using pre- or post- triggers to ensure atomicity.

What it means

When AutoTransactionBehavior is set to Always, the Cosmos provider wraps every SaveChanges in a transactional batch for atomicity. However, entities that have pre- or post-save triggers cannot be batched (they must execute as single writes so the trigger fires). If there is at least one triggered entity AND more than one root document is being saved, the provider cannot guarantee atomicity and throws (CosmosDatabaseWrapper.cs:282-287). Only one entity can be saved at a time in this configuration.

Source

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

        foreach (var entry in cosmosUpdateEntries)
        {
            if (entry.Entry.EntityType.GetTriggers().Any())
            {
                singleUpdateEntries.Add(entry);
            }
            else
            {
                batchableEntries.Add(entry);
            }
        }

        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

View on GitHub (pinned to dbf9771522)

Solutions

  1. Save triggered entities one at a time: call SaveChangesAsync once per root entity when AutoTransactionBehavior is Always and triggers are present.
  2. Remove triggers from the entity types involved in the batch, or refactor the trigger logic into application code.
  3. Switch AutoTransactionBehavior back to WhenNeeded (default) if atomic multi-entity batches are more important than the trigger semantics.

Example fix

// before
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
context.Blogs.Add(blog1); // Blog has a trigger
context.Blogs.Add(blog2);
await context.SaveChangesAsync(); // throws

// after — save one at a time
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
context.Blogs.Add(blog1);
await context.SaveChangesAsync();
context.Blogs.Add(blog2);
await context.SaveChangesAsync();
Defensive patterns

Strategy: validation

Validate before calling

// Before SaveChanges with Always + triggers, ensure only 1 root entity is being saved.
if (context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)
{
    var rootEntries = context.ChangeTracker.Entries()
        .Where(e => e.EntityType.IsDocumentRoot() && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
        .ToList();
    var hasTrigger = rootEntries.Any(e => e.EntityType.GetTriggers().Any());
    if (hasTrigger && rootEntries.Count > 1)
        throw new InvalidOperationException("Save one triggered entity at a time with AutoTransactionBehavior.Always.");
}

Prevention

When it happens

Trigger: Setting context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always, having at least one entity type with a trigger (OnConfiguring/OnSaving/OnSaved), and calling SaveChanges with 2+ root document entries where at least one has a trigger. The conflict is: triggered entries go to single-update path, but Always requires everything in one transaction.

Common situations: Enabling Always for cross-document atomicity while using triggers for audit/logging. Batch-saving multiple aggregates where one has a trigger. Upgrading from default behavior to Always without reviewing trigger usage.

Related errors


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