skills/microservice/cosmos/transactional-batch/SKILL.md
Use when performing atomic multi-document operations with Cosmos DB TransactionalBatch.
npx skillsauth add faysilalshareef/dotnet-ai-kit transactional-batchInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 conflictsdata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.