Introducing FluentDynamoDB
I'm Dan Guisinger, founder of Oproto, a small business financial platform built on a fully serverless, event-driven architecture. As we developed our microservices, we ran head-first into a familiar challenge for .NET developers:
DynamoDB is powerful, but the AWS SDK is painfully verbose, low-level, and easy to get wrong.
We needed something that made DynamoDB feel native to .NET — strongly typed, expressive, source-generated, and optimized for production workloads, not just demos.
That need eventually grew into FluentDynamoDB.
🚧 Project Status Update – Temporary Code Freeze FluentDynamoDB is currently in a temporary code freeze due to our Kiroween 2025 hackathon submission. During this period, the public repository will not receive updates, and new features will be released after judging concludes in early 2026. We're still actively engaged with the community, and we're preparing a February release that will include important fixes, improvements, and new capabilities.
What is FluentDynamoDB?
FluentDynamoDB is a source-generated, strongly-typed, DynamoDB ORM for .NET 8+, built to provide a modern development experience without sacrificing DynamoDB’s power or best practices.
At its core, the philosophy is simple:
- You define your entities.
- The source generator produces everything else.
That includes:
- Table classes
- Key builders
- Accessor methods
- Update models
- Expression translation
- Automatic hydration
- Composite attribute generation/splitting
- Strongly-typed index projections
- Stream processors
- AOT-friendly serialization
- Query, Scan, Batch, and Transaction operations in three modes (Lambda, String, Manual)
No runtime reflection, no dynamic IL generation — just pure Roslyn-generated C# tailored to your entities.
For deeper technical details on the library's capabilities, check out our documentation.
Why We Built It
FluentDynamoDB was originally developed to support Oproto’s large-scale, multi-tenant, serverless architecture. As we built out dozens of microservices, we ran into DynamoDB challenges that the AWS SDK didn’t solve well.
We needed a DynamoDB library that offered a modern, developer-friendly API with strongly-typed keys, lambdas for expressions, and clean C# accessors — not long, sprawling attribute maps or request objects.
We needed full AOT support to reduce cold-start times on AWS Lambda, so FluentDynamoDB has no runtime reflection or dynamic code generation.
We needed true multi-entity, single-table modeling. We wanted to go from query to nested, complex models. We accomplished this with automatic hydration of root + child entities, composite attributes, and rich indexing support.
We needed DynamoDB best practices enforced by design, for example - All scans disabled unless an entity is marked [Scannable]
We needed explicit control over access patterns. FluentDynamoDB supports strongly-typed LSI/GSI projections
We needed Expression translation generated at compile time for AoT compatibility. UpdateExpressions, KeyConditionExpressions, and conditional expressions are built directly from c# lambda expressions.
We needed production features we needed day one:
- Encryption fields
- S3-backed blob properties
- stuctured logging
- redaction
- stream processors.
FluentDynamoDB embraces DynamoDB’s strengths instead of abstracting them away like a relational ORM — because DynamoDB isn’t relational, and we didn’t want to pretend it is.
Example: Defining an Entity
Here’s the kind of clean code you write when defining an entity. Everything else, the keys, accessors, and table API are generated at build time.
[DynamoDbEntity]
[DynamoDbTable("Orders", IsDefault = true)]
[GenerateEntityProperty(Name = "Orders")]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
[DynamoDbAttribute("orderId")]
public string OrderId { get; set; } = string.Empty;
...
}
Once defined, the generator creates:
- OrdersTable
- OrderUpdate model
- Entity accessors
- Expression translation code
- Composite attribute mapping
No boilerplate required.
Example: Querying with Lambda Expressions
FluentDynamoDB lets you express DynamoDB access patterns directly and safely:
var recent = await db.Orders
.Where(o => o.Pk == $"{customerId}#orders" && o.Sk.StartsWith("{orderNumber}#"))
.ToListAsync();
The generator:
- Translates the lambda into a KeyConditionExpression
- Builds ExpressionAttributeNames and Values
- Constructs the full QueryRequest
- Hydrates the resulting Order objects
Side-by-side Comparison: Transactional Write (SDK vs Fluent API)
Lets compare the AWS SDK approach to writing a transaction to how we do it with FluentDynamoDB.
AWS SDK
var request = new TransactWriteItemsRequest
{
TransactItems = new List<TransactWriteItem>
{
// Put: Add a new order line
new TransactWriteItem
{
Put = new Put
{
TableName = TableName,
Item = new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = newLine.Pk },
["sk"] = new AttributeValue { S = newLine.Sk },
["lineId"] = new AttributeValue { S = newLine.LineId },
["productId"] = new AttributeValue { S = newLine.ProductId },
["productName"] = new AttributeValue { S = newLine.ProductName },
["quantity"] = new AttributeValue { N = newLine.Quantity.ToString() },
["unitPrice"] = new AttributeValue { N = newLine.UnitPrice.ToString() }
},
ConditionExpression = "attribute_not_exists(pk)"
}
},
// Update: Update the order total
new TransactWriteItem
{
Update = new Update
{
TableName = TableName,
Key = new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = $"ORDER#{orderId}" },
["sk"] = new AttributeValue { S = "META" }
},
UpdateExpression = "SET #total = :total",
ExpressionAttributeNames = new Dictionary<string, string>
{
["#total"] = "totalAmount"
},
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":total"] = new AttributeValue { N = newTotal.ToString() }
},
ConditionExpression = "attribute_exists(pk)"
}
},
// Delete: Remove an old order line
new TransactWriteItem
{
Delete = new Delete
{
TableName = TableName,
Key = new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = $"ORDER#{orderId}" },
["sk"] = new AttributeValue { S = $"LINE#{deleteLineId}" }
},
ConditionExpression = "attribute_exists(pk)"
}
}
}
};
return await client.TransactWriteItemsAsync(request);
There are many reasons to dislike this approach:
- Way too verbose
- Manual attribute maps
- Manual expressions
- No type safety
- Error-prone
FluentDynamoDB: Clean, Lambda-First Approach
await DynamoDbTransactions.Write
.Add(table.OrderLines.Put(newLine)
.Where(x => x.Pk.AttributeNotExists()))
.Add(table.Orders.Update(Order.CreatePk(orderId), Order.CreateSk())
.Set(x => new OrderUpdateModel { TotalAmount = newTotal })
.Where(x => x.Pk.AttributeExists()))
.Add(table.OrderLines.Delete(OrderLine.CreatePk(orderId), OrderLine.CreateSk(deleteLineId))
.Where(x => x.Pk.AttributeExists()))
.ExecuteAsync();
The benefits are clear:
- Strongly typed
- Compiler-validated
- Hydrated models
- Lambda-based conditional expressions
- Zero expression strings
- No manual attribute maps
- Generator builds the entire transaction structure
This alone turns 70 lines of error-prone request objects into a few expressive, safe lines of C#.
What's Next
Thank you for your interest in FluentDynamoDB. We're working toward a 1.0 release in February after our Kiroween hackathon code freeze is lifted, and we can't wait to see what you build with it.
If you're interested in following the project or using FluentDynamoDB in your own systems, check out the documentation and reach out with feedback.
