{"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/3a2006ef569de08368d59db5e1468aa8f407e4f8/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs#L268-L304","documentation":"When AutoTransactionBehavior.Always is set, EF Core attempts to save all changes as a single atomic Cosmos transactional batch. If any entity has a database trigger, it must be written individually (single-write) to honor the trigger, and atomicity can then only be guaranteed when there is exactly one document root being saved. With 2+ root entries plus a triggered entry, the provider cannot preserve atomicity and throws.","triggerScenarios":"Calling SaveChangesAsync with AutoTransactionBehavior == Always, where at least one changed entity has a trigger (entry.Entry.EntityType.GetTriggers().Any() is true) AND rootEntriesToSave.Count >= 2. See CosmosDatabaseWrapper.cs:284-286.","commonSituations":"Configuring Database.AutoTransactionBehavior = AutoTransactionBehavior.Always globally, then saving a batch where one entity has a pre/post trigger and others do not. Trigger-using audit/_vector scenarios combined with batch saves.","solutions":["Split the SaveChanges call so that triggered root entities are saved one at a time (one root per SaveChanges).","Revert AutoTransactionBehavior to the default (Never) for batches that mix triggered and non-triggered entities.","Remove the triggers from the affected entity types if atomicity across multiple roots is required."],"exampleFix":"// before\ndbContext.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;\nawait dbContext.SaveChangesAsync(); // mixes triggered + multiple roots\n\n// after - save each triggered root separately\nforeach (var root in rootsToSave)\n{\n    dbContext.ChangeTracker.Entries().ToList().ForEach(e => e.State = EntityState.Detached);\n    dbContext.Attach(root);\n    await dbContext.SaveChangesAsync();\n}","handlingStrategy":"validation","validationCode":"// Before SaveChangesAsync, verify the Always + trigger + multi-root condition\nif (dbContext.Database.AutoTransactionBehavior == AutoTransactionBehavior.Always)\n{\n    var hasTrigger = dbContext.ChangeTracker.Entries()\n        .Any(e => e.Metadata.GetTriggers().Any());\n    var rootCount = dbContext.ChangeTracker.Entries()\n        .Where(e => e.Metadata.IsDocumentRoot())\n        .Select(e => e.Metadata)\n        .Distinct()\n        .Count();\n    if (hasTrigger && rootCount >= 2)\n        throw new InvalidOperationException(\"Save triggered entities one root at a time under Always.\");\n}","typeGuard":null,"tryCatchPattern":"try { await dbContext.SaveChangesAsync(ct); }\ncatch (InvalidOperationException ex)\n    when (ex.Message.Contains(\"AutoTransactionBehavior.Always\") && ex.Message.Contains(\"trigger\"))\n{\n    // split the batch: save one triggered root per SaveChanges, then retry.\n}","preventionTips":["Avoid setting AutoTransactionBehavior.Always when entities have triggers.","Save triggered root entities one at a time.","Audit trigger usage whenever enabling Always."],"tags":["cosmos","autotransactionbehavior","triggers","savechanges","atomicity"],"backgroundTag":null,"analyzedSha":"3a2006ef569de08368d59db5e1468aa8f407e4f8","analyzedAt":"2026-08-11T23:42:04.146Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}