Cosmos DB TransactionalBatch for atomic multi-document operations. Covers batch creation, ETag concurrency, chunked batches for large operations, and error handling. Trigger: transactional batch, atomic operations, cosmos batch, ETag.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
TransactionalBatch ensures all-or-nothing semantics within a single partitionIfMatchEtag enables optimistic concurrency on replace/upsertnamespace {Company}.{Domain}.Cosmos.Infrastructure;
public sealed class CosmosUnitOfWork(Container container)
{
private readonly List<(IContainerDocument Document, BatchOperation Op)> _operations = [];
public void Create(IContainerDocument document)
=> _operations.Add((document, BatchOperation.Create));
public void Upsert(IContainerDocument document)
=> _operations.Add((document, BatchOperation.Upsert));
public void Replace(IContainerDocument document)
=> _operations.Add((document, BatchOperation.Replace));
public void Delete(IContainerDocument document)
=> _operations.Add((document, BatchOperation.Delete));
public async Task CommitAsync(PartitionKey pk, CancellationToken ct)
{
if (_operations.Count == 0) return;
if (_operations.Count == 1)
{
await ExecuteSingleAsync(_operations[0], pk, ct);
return;
}
// Chunk into batches of 100 (Cosmos limit)
foreach (var chunk in _operations.Chunk(100))
{
var batch = container.CreateTransactionalBatch(pk);
foreach (var (doc, op) in chunk)
{
switch (op)
{
case BatchOperation.Create:
batch.CreateItem(doc);
break;
case BatchOperation.Upsert:
batch.UpsertItem(doc, new TransactionalBatchItemRequestOptions
{
IfMatchEtag = doc.ETag
});
break;
case BatchOperation.Replace:
batch.ReplaceItem(doc.id, doc, new TransactionalBatchItemRequestOptions
{
IfMatchEtag = doc.ETag
});
break;
case BatchOperation.Delete:
batch.DeleteItem(doc.id);
break;
}
}
var response = await batch.ExecuteAsync(ct);
if (!response.IsSuccessStatusCode)
{
throw new CosmosBatchException(
$"Batch failed: {response.StatusCode}",
response.StatusCode);
}
}
_operations.Clear();
}
private async Task ExecuteSingleAsync(
(IContainerDocument Doc, BatchOperation Op) item,
PartitionKey pk, CancellationToken ct)
{
switch (item.Op)
{
case BatchOperation.Create:
await container.CreateItemAsync(item.Doc, pk, cancellationToken: ct);
break;
case BatchOperation.Upsert:
await container.UpsertItemAsync(item.Doc, pk,
new ItemRequestOptions { IfMatchEtag = item.Doc.ETag }, ct);
break;
}
_operations.Clear();
}
}
public enum BatchOperation { Create, Upsert, Replace, Delete }
// Read document — ETag is populated from response
var invoice = await repo.GetByIdAsync(id, pk, ct);
// invoice.ETag is set automatically
// Modify
invoice.Apply(updateEvent);
// Write with ETag check — fails if document changed since read
uow.Replace(invoice);
await uow.CommitAsync(invoice.PartitionKeys, ct);
// Handle concurrency conflict
try
{
await uow.CommitAsync(pk, ct);
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
{
// ETag mismatch — re-read and retry
logger.LogWarning("Concurrency conflict, retrying...");
// Re-read and retry logic
}
// Invoice created → update invoice + update monthly report atomically
public async Task HandleInvoiceCreated(Event<InvoiceCreatedData> @event, CancellationToken ct)
{
var invoice = SaleInvoice.FromInvoiceCreated(@event);
var pk = invoice.PartitionKeys;
// Load or create report in same partition
var report = await repo.GetByIdAsync(reportId, pk, ct)
?? new MerchantSalesReport { id = reportId, ... };
report.AddInvoice(invoice.TotalAmount);
uow.Create(invoice);
uow.Upsert(report);
await uow.CommitAsync(pk, ct);
}
| Anti-Pattern | Correct Approach |
|---|---|
| Batch across different partitions | All operations must share partition key |
| More than 100 operations unbatched | Chunk into batches of 100 |
| Missing ETag on replace/upsert | Always pass ETag for concurrency control |
| Ignoring batch response status | Check IsSuccessStatusCode and per-item results |
| Using batch for single operations | Direct API call is simpler and clearer |
# Find TransactionalBatch usage
grep -r "CreateTransactionalBatch\|TransactionalBatch" --include="*.cs" src/
# Find ETag usage
grep -r "IfMatchEtag\|ETag" --include="*.cs" src/Cosmos/
# Find CosmosUnitOfWork
grep -r "CosmosUnitOfWork" --include="*.cs" src/
CosmosUnitOfWork already exists — match its APIPreconditionFailed (412) for ETag conflicts