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 parallelView on GitHub (pinned to dbf9771522)
Solutions
- Save triggered entities one at a time: call SaveChangesAsync once per root entity when AutoTransactionBehavior is Always and triggers are present.
- Remove triggers from the entity types involved in the batch, or refactor the trigger logic into application code.
- 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 AutoTransactionBehavior is Always and triggers exist, call SaveChangesAsync once per root entity.
- Avoid mixing triggered and non-triggered entities in the same SaveChanges under Always.
- Consider whether triggers are necessary; move logic to application code if batching is required.
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
- When using AutoTransactionBehavior.Always with the Cosmos DB
- Trigger '{trigger}' is defined on entity type '{entityType}'
- Trigger '{trigger}' on entity type '{entityType}' does not h
- Trigger '{trigger}' on entity type '{entityType}' does not h
- The entity of type '{entityType}' is mapped as part of the d
AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06).
Data as JSON: /api/errors/96b4ae9260d511f5.
Report an issue: GitHub.