GitPedia

DynamoDBv2.Transactions

DynamoDB Transactions liibrary written using Source Generators, allows to execute calls to DynamoDB in a single transaction

From vitalybibikov·Updated June 25, 2026·View on GitHub·

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**. The project is written primarily in C#, distributed under the MIT License license, first published in 2024. Key topics include: aws-dynamodb, dotnet, dotnet8, dynamodb, dynamodb-client.

Latest release: v4.0.14.109
April 9, 2026View Changelog →

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">

CI
Auto-Release
codecov
NuGet
NuGet Downloads
License: MIT

</p> <p align="center">

Unit Tests
Integration Tests
Source Generator Tests
Benchmarks

</p> <p align="center">

.NET 8
.NET 9
.NET 10
AWS SDK v4
Source Link
Deterministic

</p>

What It Covers

  • Typed transactional writes through DynamoDbTransactor
  • Typed transactional reads through DynamoDbReadTransactor
  • Source-generated mapping for partial entities
  • Cached reflection fallback for existing non-partial entities
  • Optimistic concurrency via [DynamoDBVersion]
  • Expression-based condition checks
  • Composite-key support for reads and request-level write operations
  • Table-name prefixing through DynamoDbMapper.TableNamePrefix

Comparison with Standard AWS SDK

This library is focused on transactional DynamoDB workflows and tries to keep those workflows typed and compact.

AreaDynamoDBv2.TransactionsTypical AWS SDK transaction path
Write transactionsQueue typed operations and execute one TransactWriteItems callBuild TransactWriteItemsRequest and TransactWriteItem objects manually
Transactional readsQueue Get<T>() calls and materialize typed resultsBuild TransactGetItemsRequest and deserialize AttributeValue maps manually
MappingSource-generated for partial entities, cached reflection otherwiseUsually manual Dictionary<string, AttributeValue> work in transaction code
Versioning[DynamoDBVersion] is incremented and checked automaticallyManual version increment and condition-expression bookkeeping
Condition checksExpression-based helpers like ConditionEquals<TModel, TValue>()Raw condition strings, placeholder names, and placeholder values
Table namesResolved from DynamoDB model attributes plus optional prefixUsually hard-coded in low-level transaction requests

Performance Comparison

At a glance

Published benchmark snapshot from this repository:

  • 1-item transactional write: 11.99 ms vs 15.83 ms
  • 3-item transactional write: 13.37 ms vs 46.44 ms
  • MapToAttribute on a simple entity: 2,452 ns vs 13,056 ns
  • MapFromAttributes on a simple entity: 1,225 ns vs 6,261 ns

These values come from the benchmark suite in this repository. Treat them as directional measurements, not production guarantees.

End-to-End Write Benchmark

Comparison target: the existing AWS SDK wrapper benchmark in this repository. These measurements include client, serializer, and local DynamoDB test-environment overhead.

ScenarioDynamoDBv2.TransactionsAWS SDK wrapper benchmarkRelative result
1-item write11.99 ms / 80.96 KB15.83 ms / 83.77 KB1.3x faster
3-item write13.37 ms / 114.74 KB46.44 ms / 251.01 KB3.5x faster

The 3-item scenario is where the transactional wrapper shows the biggest practical gain in the published suite: one transactional write request versus per-item save work in the comparison benchmark.

Mapper Comparison: Simple Entity

Entity shape: 15 properties, mostly primitives.

OperationSource-generatedReflection fallbackRelative result
MapToAttribute2,452 ns / 2,464 B13,056 ns / 3,616 B5.3x faster, 32% fewer allocations
GetPropertyAttributedName21 ns / 0 B59 ns / 0 B2.7x faster
GetHashKeyAttributeName16 ns / 0 B27 ns / 0 B1.7x faster
GetVersion64 ns / 56 B106 ns / 56 B1.7x faster
GetTableName13 ns / 0 B796 ns / 144 Babout 60x faster

Mapper Comparison: Complex Entity

Entity shape: 19 properties with nested objects, collections, and dictionaries.

OperationSource-generatedReflection fallbackRelative result
MapToAttribute13,209 ns / 8,144 B25,020 ns / 9,361 B1.9x faster, 13% fewer allocations
GetPropertyAttributedName18 ns / 0 B57 ns / 0 B3.3x faster
GetVersion53 ns / 56 B80 ns / 56 B1.5x faster

Deserialization Comparison

OperationSource-generatedReflection fallbackRelative result
MapFromAttributes1,225 ns / 168 B6,261 ns / 952 B5.1x faster, 82% fewer allocations
MapFromAttributes complex1,109 ns / 216 B4,162 ns / 592 B3.8x faster, 63% fewer allocations
Round-trip serialize + deserialize3,436 ns / 2,656 B17,483 ns / 4,569 B5.1x faster, 42% fewer allocations

What These Numbers Mean

  • Primitive-heavy entities benefit the most because the generator emits direct AttributeValue construction.
  • Complex entities still benefit, but the gap narrows because nested objects and collections use the runtime mapper.
  • Metadata lookups like GetTableName() and GetPropertyAttributedName() are effectively free on the generated path.
  • The end-to-end benchmark in this repo compares against the existing AWS SDK wrapper benchmark, not a hand-tuned low-level request implementation.

Published benchmark environments:

  • Mapper and deserialization benchmarks: BenchmarkDotNet 0.15.8, Linux Ubuntu 25.10, Intel Core i7-8700, .NET 10.0.3, launchCount=3, warmupCount=5, iterationCount=20
  • End-to-end benchmark: BenchmarkDotNet 0.13.12, Windows 11, AMD Ryzen 9 6900HS, .NET 8.0.2

Installation

bash
dotnet add package DynamoDBv2.Transactions

The NuGet package includes the source generator automatically.

Supported target frameworks:

  • net8.0
  • net9.0
  • net10.0

Define an Entity

The library uses standard DynamoDB attributes from Amazon.DynamoDBv2.DataModel.

csharp
using Amazon.DynamoDBv2.DataModel; using DynamoDBv2.Transactions; [DynamoDBTable("Orders")] public partial class Order : ITransactional { [DynamoDBHashKey("PK")] public string OrderId { get; set; } = ""; [DynamoDBProperty("CustomerName")] public string CustomerName { get; set; } = ""; [DynamoDBProperty("Status")] public string Status { get; set; } = ""; [DynamoDBProperty("Total")] public decimal Total { get; set; } [DynamoDBVersion] public long? Version { get; set; } }

Notes:

  • partial enables source-generated mapping.
  • Non-partial classes still work through reflection.
  • ITransactional is optional. Versioning is driven by [DynamoDBVersion].

Quick Start

Write transaction

DynamoDbTransactor queues operations and sends a single TransactWriteItems request when it is disposed.

csharp
var client = new AmazonDynamoDBClient(); await using (var tx = new DynamoDbTransactor(client)) { tx.CreateOrUpdate(new Order { OrderId = "ORD-001", CustomerName = "Alice", Status = "Pending", Total = 149.99m }); }

Transactional read

DynamoDbReadTransactor queues Get operations and executes them when you call ExecuteAsync().

csharp
var reader = new DynamoDbReadTransactor(client); reader.Get<Order>("ORD-001"); var result = await reader.ExecuteAsync(); var order = result.GetItem<Order>(0);

Write API

Create or update

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.CreateOrUpdate(order); }

If the model has a [DynamoDBVersion] property, the library increments it automatically and adds the corresponding condition expression.

Delete

Delete by inferred hash key:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.DeleteAsync<Order>("ORD-001"); }

Delete by explicit property:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.DeleteAsync<Order, string>(x => x.OrderId, "ORD-001"); }

Delete by explicit key name:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.DeleteAsync<Order>("PK", "ORD-001"); }

Patch a single property

Patch by hash key and expression:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.PatchAsync<Order, string>("ORD-001", x => x.Status, "Shipped"); }

Patch from an existing model instance:

csharp
order.Status = "Shipped"; await using (var tx = new DynamoDbTransactor(client)) { tx.PatchAsync(order, nameof(order.Status)); }

Condition checks

Standalone condition check:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.ConditionEquals<Order, string>("ORD-001", x => x.Status, "Pending"); }

Composite-key condition check:

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.ConditionVersionEquals<OrderLine>( "ORD-001", "LINE-001", x => x.Version, 3); }

Available helper methods:

  • ConditionEquals<TModel, TValue>
  • ConditionNotEquals<TModel, TValue>
  • ConditionGreaterThan<TModel, TValue>
  • ConditionLessThan<TModel, TValue>
  • ConditionVersionEquals<TModel>

Important DynamoDB rule:

  • A transaction cannot contain multiple operations on the same item.
  • For example, a ConditionCheck and a Patch against the same key in the same transaction are invalid.

Transaction options

csharp
await using (var tx = new DynamoDbTransactor(client)) { tx.Options = new TransactionOptions { ClientRequestToken = "order-001-confirm-v1", ReturnConsumedCapacity = ReturnConsumedCapacity.TOTAL, ReturnItemCollectionMetrics = ReturnItemCollectionMetrics.SIZE }; tx.CreateOrUpdate(order); }

Read API

Full item

csharp
var reader = new DynamoDbReadTransactor(client); reader.Get<Order>("ORD-001"); var result = await reader.ExecuteAsync(); var order = result.GetItem<Order>(0);

Projection

csharp
var reader = new DynamoDbReadTransactor(client); reader.Get<Order>("ORD-001", x => new { x.Status, x.Total }); var result = await reader.ExecuteAsync(); var order = result.GetItem<Order>(0);

Composite keys

csharp
var reader = new DynamoDbReadTransactor(client); reader.Get<OrderLine>("ORD-001", "LINE-001"); reader.Get<OrderLine>("ORD-001", "LINE-002"); var result = await reader.ExecuteAsync();

Read options and raw access

csharp
var reader = new DynamoDbReadTransactor(client) { Options = new ReadTransactionOptions { ReturnConsumedCapacity = ReturnConsumedCapacity.TOTAL } }; reader.Get<Order>("ORD-001"); var result = await reader.ExecuteAsync(); var order = result.GetItem<Order>(0); var raw = result.GetRawItem(0); var capacity = result.ConsumedCapacity;

TransactionGetResult gives you:

  • GetItem<T>(index) for a typed item
  • GetRawItem(index) for raw DynamoDB attributes
  • GetItems<T>() for all result items requested as T
  • ConsumedCapacity when requested

Composite Keys

Read helpers support composite keys directly.

For write operations, request-level constructors provide the most complete composite-key coverage:

csharp
using DynamoDBv2.Transactions.Requests; using DynamoDBv2.Transactions.Requests.Properties; await using (var tx = new DynamoDbTransactor(client)) { tx.AddRawRequest(new DeleteTransactionRequest<OrderLine>( "ORD-001", "LINE-001")); tx.AddRawRequest(new PatchTransactionRequest<OrderLine>( "ORD-001", "LINE-002", new Property { Name = nameof(OrderLine.Status), Value = "Packed" })); }

If you need full control over request composition, AddRawRequest() is the escape hatch.

Mapping Modes

Source-generated mapping

Recommended for new entities.

Use a partial class with DynamoDB attributes:

csharp
[DynamoDBTable("Orders")] public partial class Order { [DynamoDBHashKey("PK")] public string OrderId { get; set; } = ""; }

You can also opt in explicitly:

csharp
[DynamoDbGenerateMapping] [DynamoDBTable("Orders")] public partial class Order { [DynamoDBHashKey("PK")] public string OrderId { get; set; } = ""; }

Reflection fallback

Existing entities do not need to be changed:

csharp
[DynamoDBTable("LegacyOrders")] public class LegacyOrder { [DynamoDBHashKey("PK")] public string OrderId { get; set; } = ""; }

Both modes use the same public API.

Supported Mapping Features

  • [DynamoDBHashKey]
  • [DynamoDBRangeKey]
  • [DynamoDBProperty]
  • [DynamoDBVersion]
  • [DynamoDBIgnore]
  • enum values
  • DateTimeOffset
  • nested classes and records
  • dictionaries and collections through the runtime mapper

Global table prefixing is also supported:

csharp
DynamoDbMapper.TableNamePrefix = "dev-";

Advanced Requests

You can build request objects directly when the convenience API is not enough.

Example: return the old item when a condition check fails.

csharp
using Amazon.DynamoDBv2.Model; using DynamoDBv2.Transactions.Requests; var request = new ConditionCheckTransactionRequest<Order>("ORD-001"); request.Equals<Order, string>(x => x.Status, "Pending"); request.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure.ALL_OLD; await using (var tx = new DynamoDbTransactor(client)) { tx.AddRawRequest(request); }

Current Limitations

This README reflects the current codebase, including a few important constraints:

  • Convenience key-based APIs are string-oriented. Tables with Number or Binary keys are only partially supported today.
  • Get helpers currently assume string hash and range key values.
  • For composite-key patch and delete workflows, prefer explicit request constructors through AddRawRequest().
  • Query, Scan, and non-transactional CRUD are out of scope.
  • Write transactions are executed on DisposeAsync(). If the transactor is never disposed, nothing is sent.

Development

Unit tests

bash
dotnet test test/DynamoDBv2.Transactions.UnitTests/DynamoDBv2.Transactions.UnitTests.csproj -c Release

Integration tests

Start LocalStack:

bash
docker compose up -d localstack

Then run:

bash
dotnet test test/DynamoDBv2.Transactions.IntegrationTests/DynamoDBv2.Transactions.IntegrationTests.csproj -c Release

Benchmarks

bash
dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release

Useful filters:

bash
dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*MapperBenchmark*' dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*DeserializationBenchmark*' dotnet run --project test/DynamoDBv2.Transactions.Benchmarks -c Release -- --filter '*Benchmark*'

License

MIT. See LICENSE.

Contributors

Showing top 2 contributors by commit count.

View all contributors on GitHub →

This article is auto-generated from vitalybibikov/DynamoDBv2.Transactions via the GitHub API.Last fetched: 6/28/2026