DynamoDBv2.Transactions
4.0.14.104
See the version list below for details.
dotnet add package DynamoDBv2.Transactions --version 4.0.14.104
NuGet\Install-Package DynamoDBv2.Transactions -Version 4.0.14.104
<PackageReference Include="DynamoDBv2.Transactions" Version="4.0.14.104" />
<PackageVersion Include="DynamoDBv2.Transactions" Version="4.0.14.104" />
<PackageReference Include="DynamoDBv2.Transactions" />
paket add DynamoDBv2.Transactions --version 4.0.14.104
#r "nuget: DynamoDBv2.Transactions, 4.0.14.104"
#:package DynamoDBv2.Transactions@4.0.14.104
#addin nuget:?package=DynamoDBv2.Transactions&version=4.0.14.104
#tool nuget:?package=DynamoDBv2.Transactions&version=4.0.14.104
DynamoDBv2.Transactions
A high-performance .NET library for Amazon DynamoDB transactions with compile-time source generation — up to 60x faster than reflection-based mapping with 32% fewer allocations.
<p align="center">
</p>
<p align="center">
</p>
<p align="center">
</p>
Why This Library Instead of the Standard AWS SDK?
The standard AWS SDK DynamoDBContext has fundamental design limitations for transactions:
| DynamoDBv2.Transactions | Standard AWS SDK (DynamoDBContext) |
|
|---|---|---|
| Transaction model | Single TransactWriteItems call — all operations atomic |
No built-in transaction support; manual TransactWriteItemsRequest construction |
| Key resolution | Reads [DynamoDBHashKey] at compile time (source gen) or from cached reflection |
DescribeTable network call per table on first access |
| Mapping | Source-generated inline AttributeValue construction — zero reflection, zero boxing |
Runtime reflection + Document model conversion |
| Versioning | Automatic [DynamoDBVersion] increment with condition expression |
Manual version handling |
| Condition checks | Type-safe expressions: ConditionGreaterThan<Order, decimal>(...) |
Raw string expressions |
| Batch efficiency | Up to 100 items in one request | One SaveAsync call per item |
| Allocation | Pre-sized dictionaries, zero-alloc key lookups | Unbounded allocations per operation |
Performance: This Library vs Standard SDK
<table> <tr> <td width="50%">
End-to-End Transactions (with network I/O)
| Scenario | This Library | Standard SDK | Speedup |
|---|---|---|---|
| 1-item write | 12.0 ms / 81 KB | 15.8 ms / 84 KB | 1.3x |
| 3-item write | 13.4 ms / 115 KB | 46.4 ms / 251 KB | 3.5x |
The gap grows with more items because the standard SDK issues separate DescribeTable + write calls per item, while this library batches everything into a single TransactWriteItems request.
</td> <td width="50%">
Mapper: Source-Generated vs Reflection Fallback
| Operation | Source-Gen | Reflection | Speedup |
|---|---|---|---|
GetTableName |
13 ns / 0 B | 796 ns / 144 B | 60x |
MapToAttribute (15 props) |
2,452 ns / 2,464 B | 13,056 ns / 3,616 B | 5.3x |
MapToAttribute (19 props) |
13,209 ns / 8,144 B | 25,020 ns / 9,361 B | 1.9x |
GetPropertyAttributedName |
21 ns / 0 B | 59 ns / 0 B | 2.7x |
</td> </tr> </table>
Zero-allocation key lookups via compile-time switch expressions. Inline
AttributeValueconstruction for primitives — no virtual dispatch, no boxing. Just addpartialto your entity class.
Installation
dotnet add package DynamoDBv2.Transactions
The package includes the source generator automatically — no additional packages required.
Quick Start
1. Define Your Entity
Add partial and standard DynamoDB attributes. The source generator handles everything at compile time:
[DynamoDBTable("Orders")]
public partial class Order : ITransactional
{
[DynamoDBHashKey("PK")]
public string OrderId { get; set; }
[DynamoDBProperty("CustomerName")]
public string CustomerName { get; set; }
[DynamoDBProperty("Total")]
public decimal Total { get; set; }
[DynamoDBProperty("Status")]
public string Status { get; set; }
[DynamoDBVersion]
public long? Version { get; set; }
}
2. Write a Transaction
Transactions execute atomically on DisposeAsync — if any operation fails, they all roll back:
var client = new AmazonDynamoDBClient();
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.CreateOrUpdate(new Order
{
OrderId = "ORD-001",
CustomerName = "Alice",
Total = 99.99m,
Status = "Confirmed"
});
}
That's it. The source generator wires up all mapping at compile time, the transaction executes on dispose.
How It Works
Your Code Compile Time Runtime
| | |
v v v
[DynamoDBTable("Orders")] Source Generator DynamoDbTransactor
public partial class Order generates switch-expr batches operations
| mappings (zero alloc) |
v | v
[DynamoDBHashKey("PK")] [ModuleInitializer] TransactWriteItems
public string OrderId auto-registers at (single call)
app startup
Two mapping strategies, one API:
| Strategy | When | Performance | Setup |
|---|---|---|---|
| Source Generator | Entity class is partial |
Up to 60x faster, inline AttributeValue construction |
Just add partial |
| Cached Reflection | Non-partial classes | Good (warmed caches) | No changes needed |
Both strategies are transparent — the same DynamoDbTransactor API works with either. The source generator is automatically discovered at startup via [ModuleInitializer].
Features
| Feature | Description |
|---|---|
| Source Generator | Compile-time DynamoDB attribute mapping with zero reflection and zero-allocation key lookups |
| AWS SDK v4 | Built for AWSSDK.DynamoDBv2 v4.x, auto-tracked via Dependabot with automated releases |
| Transactional Operations | CreateOrUpdate, Delete, Patch, Update, ConditionCheck — all in a single atomic request |
| Optimistic Concurrency | Automatic version increment via ITransactional — no manual version tracking needed |
| Condition Expressions | Type-safe Equals, NotEquals, GreaterThan, LessThan, VersionEquals checks |
| 100-Item Validation | Enforces DynamoDB's transaction limit before sending the request |
| TransactionOptions | ClientRequestToken (idempotency), ReturnConsumedCapacity, ReturnItemCollectionMetrics |
| Multi-targeting | .NET 8.0, .NET 9.0, and .NET 10.0 |
| Source Link + Deterministic | Step-into debugging from NuGet, reproducible builds |
When to Use This Library vs the Standard SDK
| Use Case | Recommendation |
|---|---|
| Multiple writes that must succeed or fail together | This library — atomic TransactWriteItems in one call |
| Optimistic concurrency / versioning | This library — automatic [DynamoDBVersion] with zero boilerplate |
| High-throughput services writing many items per request | This library — up to 100 items per transaction, 3.5x faster for multi-item writes |
| Condition checks before writes | This library — type-safe expressions vs raw strings |
| Simple single-item CRUD with no transaction needs | Standard SDK DynamoDBContext is fine |
| Query / Scan operations | Standard SDK (this library is write-focused) |
| Full DynamoDB Document model | Standard SDK |
Code Comparison: This Library vs Standard SDK
<table> <tr> <th>DynamoDBv2.Transactions</th> <th>Standard AWS SDK</th> </tr> <tr> <td>
// Atomic: create order + update inventory
await using var tx = new DynamoDbTransactor(client);
tx.CreateOrUpdate(new Order
{
OrderId = "ORD-001",
Total = 99.99m,
Version = null // auto-managed
});
tx.ConditionGreaterThan<Inventory, int>(
"SKU-100", i => i.Quantity, 0);
tx.PatchAsync<Inventory, int>(
"SKU-100", i => i.Quantity, newQty);
// executes on DisposeAsync
</td> <td>
// Manual: build request by hand
var request = new TransactWriteItemsRequest
{
TransactItems = new List<TransactWriteItem>
{
new() { Put = new Put {
TableName = "Orders",
Item = new Dictionary<string, AttributeValue>
{
["PK"] = new() { S = "ORD-001" },
["Total"] = new() { N = "99.99" },
// manual version logic...
}
}},
new() { ConditionCheck = new ConditionCheck {
TableName = "Inventory",
Key = new() { ["PK"] = new() { S = "SKU-100" } },
ConditionExpression = "#qty > :zero",
ExpressionAttributeNames = new() { ["#qty"] = "Quantity" },
ExpressionAttributeValues = new() { [":zero"] = new() { N = "0" } }
}},
new() { Update = new Update {
TableName = "Inventory",
Key = new() { ["PK"] = new() { S = "SKU-100" } },
UpdateExpression = "SET #qty = :val",
ExpressionAttributeNames = new() { ["#qty"] = "Quantity" },
ExpressionAttributeValues = new() { [":val"] = new() { N = newQty.ToString() } }
}}
}
};
await client.TransactWriteItemsAsync(request);
</td> </tr> </table>
Usage Examples
Create or Update
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.CreateOrUpdate(new Order
{
OrderId = "ORD-001",
CustomerName = "Alice",
Total = 149.99m,
Status = "Confirmed"
});
}
// Transaction executes atomically on DisposeAsync
Delete
// Delete by hash key (attribute name inferred from [DynamoDBHashKey])
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.DeleteAsync<Order>("ORD-001");
}
// Delete with explicit key name
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.DeleteAsync<Order>("PK", "ORD-001");
}
// Delete with expression (type-safe)
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.DeleteAsync<Order, string>(o => o.OrderId, "ORD-001");
}
Patch a Single Property
Update one field without touching the rest of the item:
// Expression-based (type-safe)
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.PatchAsync<Order, string>("ORD-001", o => o.Status, "Shipped");
}
// Instance-based (patch from an existing object)
await using (var transactor = new DynamoDbTransactor(client))
{
order.Status = "Shipped";
transactor.PatchAsync(order, nameof(order.Status));
}
Conditional Checks
Assert conditions before writing — the entire transaction rolls back if any check fails:
await using (var transactor = new DynamoDbTransactor(client))
{
// Only update if the order total is above the threshold
transactor.ConditionGreaterThan<Order, decimal>("ORD-001", o => o.Total, 50.0m);
transactor.PatchAsync<Order, string>("ORD-001", o => o.Status, "Priority");
}
Available condition methods:
ConditionEquals<TModel, TValue>— field must equal valueConditionNotEquals<TModel, TValue>— field must not equal valueConditionGreaterThan<TModel, TValue>— field must be greater than valueConditionLessThan<TModel, TValue>— field must be less than valueConditionVersionEquals<TModel>— optimistic concurrency version check
Optimistic Concurrency (Versioning)
Implement ITransactional to get automatic version management:
[DynamoDBTable("Orders")]
public partial class Order : ITransactional
{
[DynamoDBHashKey("PK")]
public string OrderId { get; set; }
[DynamoDBVersion]
public long? Version { get; set; } // Auto-incremented on every write
}
// First write: Version is set to 0
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.CreateOrUpdate(new Order { OrderId = "ORD-001" });
}
// Second write: Version auto-increments to 1, with a condition check
// that the current version in DynamoDB matches the expected version.
// If another writer modified the item, this transaction fails.
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.CreateOrUpdate(existingOrder); // Version check is automatic
}
// Explicit version check
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.ConditionVersionEquals<Order>("ORD-001", o => o.Version, 1);
transactor.PatchAsync<Order, string>("ORD-001", o => o.Status, "Completed");
}
Complex Multi-Operation Transaction
Combine multiple operations in a single atomic transaction (up to 100 items):
await using (var transactor = new DynamoDbTransactor(client))
{
// Check: inventory must be sufficient
transactor.ConditionGreaterThan<Inventory, int>(
"SKU-100", i => i.Quantity, 0);
// Create the order
transactor.CreateOrUpdate(new Order
{
OrderId = "ORD-042",
CustomerName = "Bob",
Total = 299.99m,
Status = "Confirmed"
});
// Update inventory count
transactor.PatchAsync<Inventory, int>(
"SKU-100", i => i.Quantity, currentQuantity - 1);
// Log the transaction
transactor.CreateOrUpdate(new AuditLog
{
LogId = Guid.NewGuid().ToString(),
Action = "OrderCreated",
Timestamp = DateTime.UtcNow
});
}
// All 4 operations succeed or fail together
Transaction Options
Configure idempotency and capacity tracking:
await using (var transactor = new DynamoDbTransactor(client))
{
transactor.Options = new TransactionOptions
{
// Idempotency token — retrying with the same token within 10 minutes
// returns the original result instead of executing again
ClientRequestToken = "order-042-confirm-v1",
// Track consumed read/write capacity units
ReturnConsumedCapacity = ReturnConsumedCapacity.TOTAL,
// Track item collection metrics (useful for tables with LSIs)
ReturnItemCollectionMetrics = ReturnItemCollectionMetrics.SIZE
};
transactor.CreateOrUpdate(order);
transactor.PatchAsync<Inventory, int>("SKU-100", i => i.Quantity, newQty);
}
Dependency Injection
Use ITransactionManager for testable, DI-friendly code:
// Registration
services.AddSingleton<IAmazonDynamoDB>(new AmazonDynamoDBClient());
services.AddSingleton<ITransactionManager, TransactionManager>();
// Usage
public class OrderService(ITransactionManager transactionManager)
{
public async Task ConfirmOrder(Order order)
{
await using var transactor = new DynamoDbTransactor(transactionManager);
transactor.CreateOrUpdate(order);
}
}
Source Generator Deep Dive
How Discovery Works
The source generator runs at compile time and discovers entities via two pipelines:
- Auto-discovery — any
partialclass with a[DynamoDBHashKey]property - Explicit opt-in — classes decorated with
[DynamoDbGenerateMapping]
// Auto-discovered (recommended)
[DynamoDBTable("Orders")]
public partial class Order
{
[DynamoDBHashKey("PK")]
public string OrderId { get; set; }
}
// Explicit opt-in
[DynamoDbGenerateMapping]
[DynamoDBTable("Orders")]
public partial class Order
{
[DynamoDBHashKey("PK")]
public string OrderId { get; set; }
}
What Gets Generated
For each discovered entity, the generator emits:
__DynamoDbMetadatanested class with:GetPropertyAttributeName(string)— zero-allocation switch expressionMapToAttributes(T)— inlineAttributeValueconstruction for known types (string,int,long,decimal,float,double,bool,DateTime,Guid, and their nullable variants) — no virtual dispatch, no boxing. Complex types (nested objects, collections, dictionaries) fall back to the runtime converter.- Pre-sized
Dictionary<string, AttributeValue>with the exact property count known at compile time GetVersion(T)— direct property access
DynamoDbMappingRegistration.g.cs—[ModuleInitializer]that registers all types at app startup
Example of generated code for a string and decimal property:
// Generated — no function call, no boxing, no null check for value types
attributeMap["CustomerName"] = new AttributeValue { S = obj.CustomerName };
attributeMap["Total"] = new AttributeValue { N = obj.Total.ToString(CultureInfo.InvariantCulture) };
Fallback Behavior
Non-partial classes work seamlessly via cached reflection. You can mix both in the same project:
// Source-generated (60x faster lookups, inline AttributeValue)
public partial class FastEntity { ... }
// Reflection fallback (still fast with warmed caches)
public class LegacyEntity { ... }
Detailed Benchmark Results
Simple Entity (15 properties)
Isolated mapping operations — no DynamoDB I/O. Entity with 15 properties including all common types.
Reflection results use warmed-up ConcurrentDictionary caches (best-case reflection).
BenchmarkDotNet v0.15.8, Linux Ubuntu 25.10
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), .NET 10.0.3, X64 RyuJIT x86-64-v3
IterationCount=20 LaunchCount=3 WarmupCount=5
| Method | Mean | Allocated | vs Reflection |
|---|---|---|---|
| MapToAttribute (source-generated) | 2,452 ns | 2,464 B | 5.3x faster, 32% less alloc |
| MapToAttribute (reflection) | 13,056 ns | 3,616 B | baseline |
| GetPropertyAttributedName (source-gen) | 21 ns | 0 B | 2.7x faster |
| GetPropertyAttributedName (reflection) | 59 ns | 0 B | baseline |
| GetHashKeyAttributeName (source-gen) | 16 ns | 0 B | 1.7x faster |
| GetHashKeyAttributeName (reflection) | 27 ns | 0 B | baseline |
| GetVersion (source-generated) | 64 ns | 56 B | 1.7x faster |
| GetVersion (reflection) | 106 ns | 56 B | baseline |
| GetTableName (source-generated) | 13 ns | 0 B | 60x faster |
| GetTableName (reflection) | 796 ns | 144 B | baseline |
Complex Entity (19 properties, nested objects, collections)
Realistic e-commerce order entity with strings, decimals, booleans, DateTimes, nested Address object, List<OrderItem>, and Dictionary<string, string>.
| Method | Mean | Allocated | vs Reflection |
|---|---|---|---|
| MapToAttribute (source-generated) | 13,209 ns | 8,144 B | 1.9x faster, 13% less alloc |
| MapToAttribute (reflection) | 25,020 ns | 9,361 B | baseline |
| GetPropertyAttributedName (source-gen) | 18 ns | 0 B | 3.3x faster |
| GetPropertyAttributedName (reflection) | 57 ns | 0 B | baseline |
| GetVersion (source-generated) | 53 ns | 56 B | 1.5x faster |
| GetVersion (reflection) | 80 ns | 56 B | baseline |
The complex entity gap is smaller for
MapToAttributebecause nested objects and collections still go through the runtime converter — the source generator inlines only primitive types. As more of your entity consists of primitive fields, the speedup approaches the 5x+ range.
End-to-End: Full Transaction (includes network I/O)
BenchmarkDotNet v0.13.12, Windows 11
AMD Ryzen 9 6900HS, .NET 8.0.2
Job=OutOfProc IterationCount=15 LaunchCount=3 WarmupCount=10
| Method | Mean | Error | StdDev | Allocated |
|---|---|---|---|---|
| DynamoDBv2.Transactions (1 item) | 11.99 ms | 0.046 ms | 0.087 ms | 80.96 KB |
| Standard SDK Wrapper (1 item) | 15.83 ms | 0.236 ms | 0.442 ms | 83.77 KB |
| DynamoDBv2.Transactions (3 items) | 13.37 ms | 0.066 ms | 0.123 ms | 114.74 KB |
| Standard SDK Wrapper (3 items) | 46.44 ms | 0.444 ms | 0.834 ms | 251.01 KB |
The 3-item gap is dramatic because the standard SDK issues separate
DescribeTable+ write calls per item, while this library sends a singleTransactWriteItemsrequest.
What the Source Generator Optimizes
For primitive types, the generator emits direct AttributeValue construction — no method calls, no boxing, no virtual dispatch:
| Property Type | Generated Code | Reflection Path |
|---|---|---|
string |
new AttributeValue { S = obj.Name } |
GetAttributeValue(object) → switch → ConvertToAttributeValueV2 |
int, long, decimal |
new AttributeValue { N = obj.Total.ToString(InvariantCulture) } |
Boxing → switch → Convert.ToString |
bool |
new AttributeValue { BOOL = obj.IsActive } |
Boxing → switch → type check |
DateTime |
new AttributeValue { S = obj.CreatedAt.ToUniversalTime().ToString(...) } |
Boxing → switch → format |
int?, DateTime? etc. |
Null check + .Value + inline |
Boxing → null check → type dispatch |
| Nested objects, collections | Falls back to DynamoDbMapper.GetAttributeValue |
Same |
Run Benchmarks Yourself
# Simple entity benchmarks (source-gen vs reflection)
dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*MapperBenchmark*'
# Complex entity benchmarks (19-property entity with nested objects)
dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*ComplexEntity*'
# End-to-end benchmarks (requires localstack)
dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*Benchmark*'
Running Tests
# Integration tests (requires Docker + localstack)
docker-compose up --exit-code-from tests tests localstack
# Unit tests
docker-compose up --exit-code-from unittests unittests
Test coverage: 259 unit tests, 33 integration tests, 16 source generator tests — all validated in CI on every push.
Versioning
This library's version tracks the underlying AWSSDK.DynamoDBv2 version:
Format: {aws_major}.{aws_minor}.{aws_patch}.{aws_rev * 100 + lib_rev}
| AWS SDK Version | Library Release | Meaning |
|---|---|---|
| 4.0.14.1 | 4.0.14.100 | Initial release for AWS SDK 4.0.14.1 |
| 4.0.14.1 | 4.0.14.101 | Library-only fix (same AWS SDK) |
| 4.0.15.0 | 4.0.15.0 | New AWS SDK version, lib rev 0 |
The first three segments always tell you which AWS SDK version is inside. New AWS SDK versions are auto-released via Dependabot + CI pipeline.
Contributing
When creating PRs, please ensure:
- No sensitive information (tokens, keys, credentials) is included
- All existing tests pass (
docker-compose up --exit-code-from tests tests localstack) - New features include appropriate test coverage
License
Copyright © 2026, Vitali Bibikov. Code released under the MIT license.
Contact
Vitali Bibikov — bibikovvitaly@gmail.com
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.14.109 | 2,192 | 4/9/2026 |
| 4.0.14.108 | 115 | 4/9/2026 |
| 4.0.14.107 | 163 | 4/9/2026 |
| 4.0.14.106 | 922 | 3/10/2026 |
| 4.0.14.105 | 104 | 3/9/2026 |
| 4.0.14.104 | 100 | 3/9/2026 |
| 4.0.14.103 | 101 | 3/9/2026 |
| 4.0.14.102 | 98 | 3/9/2026 |
| 4.0.14.101 | 103 | 3/9/2026 |
| 4.0.14.100 | 101 | 3/9/2026 |
| 4.0.14.1 | 108 | 3/9/2026 |
| 3.7.407 | 102 | 3/8/2026 |
| 3.7.406.3 | 313 | 3/20/2025 |