{"record":{"id":"96b4ae9260d511f5","repo":"dotnet/efcore","slug":"when-using-autotransactionbehavior-always-with-the","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs","lineNumber":286,"sourceCode":"        foreach (var entry in cosmosUpdateEntries)\n        {\n            if (entry.Entry.EntityType.GetTriggers().Any())\n            {\n                singleUpdateEntries.Add(entry);\n            }\n            else\n            {\n                batchableEntries.Add(entry);\n            }\n        }\n\n        if (_currentDbContext.Context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)\n        {\n            if (singleUpdateEntries.Count >= 1)\n            {\n                if (rootEntriesToSave.Count >= 2)\n                {\n                    throw new InvalidOperationException(CosmosStrings.SaveChangesAutoTransactionBehaviorAlwaysTriggerAtomicity);\n                }\n\n                // There is only 1 entry, and it has a trigger\n                return new SaveGroups { BatchableUpdateEntries = [], SingleUpdateEntries = singleUpdateEntries };\n            }\n\n            var firstEntry = batchableEntries[0];\n            var key = new Grouping(firstEntry.CollectionId, _cosmosClient.GetPartitionKeyValue(firstEntry.Entry));\n            return batchableEntries.Count > 100\n                || !batchableEntries.All(entry =>\n                    entry.CollectionId == key.ContainerId && _cosmosClient.GetPartitionKeyValue(entry.Entry) == key.PartitionKeyValue)\n                    ? throw new InvalidOperationException(CosmosStrings.SaveChangesAutoTransactionBehaviorAlwaysAtomicity)\n                    : new SaveGroups { BatchableUpdateEntries = [(key, batchableEntries)], SingleUpdateEntries = [] };\n        }\n\n        var batches = CreateBatches(batchableEntries);\n\n        // For bulk it is important that single writes are always classified as singleUpdateEntries so that they will be executed in parallel","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/dotnet/efcore/blob/dbf9771522148d61a2467854921bd5dc6f6e6916/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs#L268-L304","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ncontext.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;\ncontext.Blogs.Add(blog1); // Blog has a trigger\ncontext.Blogs.Add(blog2);\nawait context.SaveChangesAsync(); // throws\n\n// after — save one at a time\ncontext.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;\ncontext.Blogs.Add(blog1);\nawait context.SaveChangesAsync();\ncontext.Blogs.Add(blog2);\nawait context.SaveChangesAsync();","handlingStrategy":"validation","validationCode":"// Before SaveChanges with Always + triggers, ensure only 1 root entity is being saved.\nif (context.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)\n{\n    var rootEntries = context.ChangeTracker.Entries()\n        .Where(e => e.EntityType.IsDocumentRoot() && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)\n        .ToList();\n    var hasTrigger = rootEntries.Any(e => e.EntityType.GetTriggers().Any());\n    if (hasTrigger && rootEntries.Count > 1)\n        throw new InvalidOperationException(\"Save one triggered entity at a time with AutoTransactionBehavior.Always.\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["cosmos","transactions","triggers","savechanges"],"analyzedSha":"dbf9771522148d61a2467854921bd5dc6f6e6916","analyzedAt":"2026-08-06T20:46:03.226Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}