DynamoDb.ExpressionMapping
0.1.1
dotnet add package DynamoDb.ExpressionMapping --version 0.1.1
NuGet\Install-Package DynamoDb.ExpressionMapping -Version 0.1.1
<PackageReference Include="DynamoDb.ExpressionMapping" Version="0.1.1" />
<PackageVersion Include="DynamoDb.ExpressionMapping" Version="0.1.1" />
<PackageReference Include="DynamoDb.ExpressionMapping" />
paket add DynamoDb.ExpressionMapping --version 0.1.1
#r "nuget: DynamoDb.ExpressionMapping, 0.1.1"
#:package DynamoDb.ExpressionMapping@0.1.1
#addin nuget:?package=DynamoDb.ExpressionMapping&version=0.1.1
#tool nuget:?package=DynamoDb.ExpressionMapping&version=0.1.1
DynamoDb.ExpressionMapping
Disclaimer: This repository and its code (library and examples) should be considered experimental. The implementation has been mostly generated from specs (see the
.ralph/directory) and self-verified by an AI agent in a loop. Not recommended for production use without thorough review and due scrutiny.
A type-safe .NET library that converts C# LINQ expression trees into AWS DynamoDB expression strings (ProjectionExpression, FilterExpression, ConditionExpression, UpdateExpression, KeyConditionExpression) with direct result mapping that avoids full entity hydration.
Features
- Type-Safe Expression Building — Convert C# lambda expressions to DynamoDB expressions with compile-time checking
- Direct Result Mapping — Map
Dictionary<string, AttributeValue>directly to projected types without full entity hydration - Automatic Keyword Aliasing — 573+ DynamoDB reserved keywords automatically detected and aliased
- Expression Caching — Compiled expressions cached for performance
- Fluent AWS SDK Integration — Extension methods for all major request types
- Pluggable Type Converters — Extensible
AttributeValueconversion system - Minimal Dependencies — Works alongside AWS SDK, not as a replacement
Installation
dotnet add package DynamoDb.ExpressionMapping
Requirements: .NET 8.0+
Quick Start
using DynamoDb.ExpressionMapping;
using DynamoDb.ExpressionMapping.Expressions;
using DynamoDb.ExpressionMapping.ResultMapping;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
// Define your entity
public class Order
{
public string OrderId { get; set; }
public string CustomerId { get; set; }
public decimal Total { get; set; }
public bool IsActive { get; set; }
public string Status { get; set; } // Reserved keyword - auto-aliased
public DateTime CreatedAt { get; set; }
}
// Setup builders
var projectionBuilder = new ProjectionBuilder<Order>();
var filterBuilder = new FilterExpressionBuilder<Order>();
var resultMapper = new DirectResultMapper<Order>();
// 1. Build projection expression
var projection = projectionBuilder.BuildProjection(o => new
{
o.OrderId,
o.CustomerId,
o.Total,
o.Status
});
// 2. Build filter expression
var filter = filterBuilder.BuildFilter(o =>
o.IsActive && o.Total > 100m);
// 3. Create scan request with expressions
var scanRequest = new ScanRequest { TableName = "Orders" }
.WithProjection(projectionBuilder, o => new { o.OrderId, o.Total, o.Status })
.WithFilter(filterBuilder, o => o.IsActive && o.Total > 100m);
// 4. Execute query
var client = new AmazonDynamoDBClient();
var response = await client.ScanAsync(scanRequest);
// 5. Map results directly to DTO
var mapper = resultMapper.CreateMapper(o => new { o.OrderId, o.Total, o.Status });
var orders = response.Items.Select(mapper).ToList();
Expression Builders
Projection Expressions
Build ProjectionExpression strings from C# selectors:
var builder = new ProjectionBuilder<Order>();
// Single property
var result = builder.BuildProjection(o => o.OrderId);
// Result: "OrderId"
// Multiple properties (anonymous type)
var result = builder.BuildProjection(o => new { o.OrderId, o.CustomerId });
// Result: "OrderId, CustomerId"
// Nested properties
var result = builder.BuildProjection(o => o.Address.City);
// Result: "Address.City"
// Reserved keywords (auto-aliased)
var result = builder.BuildProjection(o => new { o.OrderId, o.Status });
// Result: "OrderId, #proj_0"
// ExpressionAttributeNames: { "#proj_0": "Status" }
Filter Expressions
Build FilterExpression and ConditionExpression strings from predicates:
var filterBuilder = new FilterExpressionBuilder<Order>();
// Comparison operators
var result = filterBuilder.BuildFilter(o => o.Total > 100m);
// Result: "Total > :filt_v0"
// Boolean logic
var result = filterBuilder.BuildFilter(o =>
o.IsActive && o.Total > 100m && o.Status == "Pending");
// String operations
var result = filterBuilder.BuildFilter(o => o.Title.StartsWith("Premium"));
// Result: "begins_with(Title, :filt_v0)"
var result = filterBuilder.BuildFilter(o => o.Description.Contains("sale"));
// Result: "contains(Description, :filt_v0)"
// Null checks
var result = filterBuilder.BuildFilter(o => o.EndDate == null);
// Result: "attribute_not_exists(EndDate)"
var result = filterBuilder.BuildFilter(o => o.EndDate != null);
// Result: "attribute_exists(EndDate)"
// IN operator
var statuses = new[] { "Pending", "Approved" };
var result = filterBuilder.BuildFilter(o => statuses.Contains(o.Status));
// Result: "#filt_0 IN (:filt_v0, :filt_v1)"
// Composable filters
var filter1 = filterBuilder.BuildFilter(o => o.IsActive);
var filter2 = filterBuilder.BuildFilter(o => o.Total > 100m);
var combined = filter1.And(filter2);
Update Expressions
Build UpdateExpression strings with fluent API:
var builder = new UpdateExpressionBuilder<Order>();
// Simple SET
var result = builder
.Set(o => o.Status, "Shipped")
.Build();
// Result: "SET Status = :upd_v0"
// Increment/Decrement
var result = builder
.Increment(o => o.ViewCount, 1)
.Decrement(o => o.Price, 10.5m)
.Build();
// Result: "SET ViewCount = ViewCount + :upd_v0, Price = Price - :upd_v1"
// Conditional SET
var result = builder
.SetIfNotExists(o => o.CreatedAt, DateTime.Now)
.Build();
// Result: "SET CreatedAt = if_not_exists(CreatedAt, :upd_v0)"
// List operations
var result = builder
.AppendToList(o => o.Tags, new List<string> { "new-tag" })
.Build();
// Result: "SET Tags = list_append(Tags, :upd_v0)"
// Multiple clauses
var result = builder
.Set(o => o.Status, "Updated")
.Increment(o => o.ViewCount, 1)
.Remove(o => o.TempFlag)
.Build();
// Result: "SET Status = :upd_v0, ViewCount = ViewCount + :upd_v1 REMOVE TempFlag"
Key Condition Expressions
Build KeyConditionExpression strings for Query operations:
var builder = new KeyConditionExpressionBuilder<Order>();
// Partition key only
var result = builder
.WithPartitionKey(e => e.PK, "USER#123")
.Build();
// Result: "PK = :key_v0"
// Partition + Sort key equality
var result = builder
.WithPartitionKey(e => e.PK, "USER#123")
.WithSortKeyEquals(e => e.SK, "ORDER#456");
// Result: "PK = :key_v0 AND SK = :key_v1"
// Partition + Sort key comparison
var result = builder
.WithPartitionKey(e => e.PK, "USER#123")
.WithSortKeyGreaterThan(e => e.SK, "ORDER#100");
// Result: "PK = :key_v0 AND SK > :key_v1"
// Partition + Sort key BETWEEN
var result = builder
.WithPartitionKey(e => e.PK, "USER#123")
.WithSortKeyBetween(e => e.SK, "ORDER#100", "ORDER#999");
// Result: "PK = :key_v0 AND SK BETWEEN :key_v1 AND :key_v2"
// Partition + Sort key begins_with
var result = builder
.WithPartitionKey(e => e.PK, "USER#123")
.WithSortKeyBeginsWith(e => e.SK, "ORDER#2024-");
// Result: "PK = :key_v0 AND begins_with(SK, :key_v1)"
Direct Result Mapping
Map DynamoDB results directly to projected types without full entity hydration:
var mapper = new DirectResultMapper<Order>();
// Single property
var orderIdMapper = mapper.CreateMapper(o => o.OrderId);
var orderId = orderIdMapper(attributeDict);
// Anonymous type (DTO)
var dtoMapper = mapper.CreateMapper(o => new
{
Id = o.OrderId,
o.CustomerId,
o.Total
});
var dto = dtoMapper(attributeDict);
// Named type
var summaryMapper = mapper.CreateMapper(o => new OrderSummary
{
OrderId = o.OrderId,
Total = o.Total,
Status = o.Status
});
var summary = summaryMapper(attributeDict);
// Nested properties
var cityMapper = mapper.CreateMapper(o => o.Address.City);
var city = cityMapper(attributeDict);
// Use with query results
var response = await client.ScanAsync(scanRequest);
var dtoMapper = mapper.CreateMapper(o => new { o.OrderId, o.Total });
var results = response.Items.Select(dtoMapper).ToList();
AWS SDK Integration
Extension methods for fluent request building:
// Query with key condition, projection, and filter
var queryRequest = new QueryRequest { TableName = "Orders" }
.WithKeyCondition(keyConditionBuilder, b => b
.WithPartitionKey(e => e.PK, "USER#123")
.WithSortKeyBeginsWith(e => e.SK, "ORDER#"))
.WithProjection(projectionBuilder, o => new { o.OrderId, o.Total, o.Status })
.WithFilter(filterBuilder, o => o.IsActive && o.Total > 100m);
// Scan with projection and filter
var scanRequest = new ScanRequest { TableName = "Orders" }
.WithProjection(projectionBuilder, o => new { o.OrderId, o.Status })
.WithFilter(filterBuilder, o => o.IsActive);
// UpdateItem with update expression and condition
var updateRequest = new UpdateItemRequest { TableName = "Orders" }
.WithUpdate(updateBuilder, b => b
.Set(e => e.Status, "Shipped")
.Increment(e => e.ViewCount, 1))
.WithCondition(conditionBuilder, o => o.Status == "Pending");
// PutItem with condition
var putRequest = new PutItemRequest { TableName = "Orders" }
.WithCondition(conditionBuilder, o => o.OrderId == null);
// DeleteItem with condition
var deleteRequest = new DeleteItemRequest { TableName = "Orders" }
.WithCondition(conditionBuilder, o => o.Status == "Draft");
// GetItem with projection
var getRequest = new GetItemRequest { TableName = "Orders" }
.WithProjection(projectionBuilder, o => new { o.OrderId, o.Total });
// BatchGetItem with projection
var batchGetRequest = new BatchGetItemRequest()
.WithProjection("Orders", projectionBuilder, o => new { o.OrderId, o.Status });
Attribute Name Mapping
Customize how C# property names map to DynamoDB attribute names:
using DynamoDb.ExpressionMapping.Attributes;
public class Product
{
public Guid Id { get; set; }
[DynamoDbAttribute("cust_id")]
public Guid CustomerId { get; set; }
[DynamoDbIgnore]
public bool IsActive { get; set; }
}
Resolution order:
- Fluent overrides (via
AttributeNameResolver) [DynamoDbAttribute]custom attribute[DynamoDBProperty](AWS SDK attribute)- Property name (convention)
Type Converters
Built-in converters for common types:
- Primitives:
string,int,long,decimal,double,float,bool - Dates:
DateTime,DateTimeOffset - Binary:
byte[],Guid - Collections:
List<T>,HashSet<T>,T[],Dictionary<string, T> - Nullable types:
int?,DateTime?, etc. - Enums (string representation)
Custom Converters
using DynamoDb.ExpressionMapping.Mapping;
public class MoneyConverter : IAttributeValueConverter<Money>
{
public AttributeValue ToAttributeValue(Money value)
{
return new AttributeValue { N = value.Amount.ToString("F2") };
}
public Money FromAttributeValue(AttributeValue value)
{
return new Money(decimal.Parse(value.N));
}
}
// Apply to property
public class Order
{
[DynamoDbConverter(typeof(MoneyConverter))]
public Money Price { get; set; }
}
// Or register globally
var registry = AttributeValueConverterRegistry.Default.Clone();
registry.Register(new MoneyConverter());
Dependency Injection
Register builders and configuration with Microsoft.Extensions.DependencyInjection:
using DynamoDb.ExpressionMapping.Extensions;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Register all builders and mappers
services.AddDynamoDbExpressionMapping(config => config
.WithNullHandling(NullHandlingMode.OmitNullValues)
.WithCustomConverterRegistry(customRegistry));
// Register per-entity configuration
services.AddDynamoDbEntity<Order>(resolver => resolver
.MapProperty(o => o.OrderId, "order_id")
.MapProperty(o => o.CustomerId, "customer_id"));
// Inject into your services
public class OrderService
{
private readonly ProjectionBuilder<Order> _projectionBuilder;
private readonly FilterExpressionBuilder<Order> _filterBuilder;
public OrderService(
ProjectionBuilder<Order> projectionBuilder,
FilterExpressionBuilder<Order> filterBuilder)
{
_projectionBuilder = projectionBuilder;
_filterBuilder = filterBuilder;
}
}
Reserved Keywords
DynamoDB has 573+ reserved keywords. This library automatically detects and aliases them:
public class Order
{
public string OrderId { get; set; }
public string Name { get; set; } // Reserved keyword
public string Status { get; set; } // Reserved keyword
public decimal Percent { get; set; } // Reserved keyword
}
var result = builder.BuildProjection(o => new { o.OrderId, o.Name, o.Status });
// Result: "OrderId, #proj_0, #proj_1"
// ExpressionAttributeNames: { "#proj_0": "Name", "#proj_1": "Status" }
Scoped alias prefixes prevent collisions:
- Projection:
#proj_, no value aliases - Filter:
#filt_,:filt_v - Condition:
#cond_,:cond_v - Update:
#upd_,:upd_v - KeyCondition:
#key_,:key_v
Performance
- Expression Caching — Compiled expression delegates cached by default
- Zero Allocations — Hot path optimized to minimize allocations
- Direct Mapping — Avoid full entity hydration for partial projections
- Compiled Delegates — Result mappers run at native speed after initial compilation
Testing
# All tests (requires Docker for DynamoDB Local)
dotnet test
# Unit tests only (no Docker required)
dotnet test --filter "Category!=Integration"
# Integration tests only
dotnet test --filter "Category=Integration"
Test Coverage:
- 565 unit tests
- 68 integration tests (using Testcontainers.DynamoDb)
- 100% specification coverage
Building
dotnet build
dotnet pack
Example: Full Pipeline
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using DynamoDb.ExpressionMapping;
using DynamoDb.ExpressionMapping.Expressions;
using DynamoDb.ExpressionMapping.ResultMapping;
// Setup
var client = new AmazonDynamoDBClient();
var projectionBuilder = new ProjectionBuilder<Order>();
var filterBuilder = new FilterExpressionBuilder<Order>();
var resultMapper = new DirectResultMapper<Order>();
// 1. Build scan request
var scanRequest = new ScanRequest { TableName = "Orders" }
.WithProjection(projectionBuilder, o => new
{
o.OrderId,
o.CustomerId,
o.Total,
o.Status
})
.WithFilter(filterBuilder, o =>
o.IsActive && o.Total > 100m);
// 2. Execute query
var response = await client.ScanAsync(scanRequest);
// 3. Map results directly to DTO
var mapper = resultMapper.CreateMapper(o => new
{
o.OrderId,
o.CustomerId,
o.Total,
o.Status
});
var orders = response.Items
.Select(mapper)
.ToList();
// 4. Use results
foreach (var order in orders)
{
Console.WriteLine($"Order {order.OrderId}: ${order.Total} - {order.Status}");
}
Contributing
Contributions welcome! Please open an issue or PR.
License
MIT License - see LICENSE file for details.
Dependencies
AWSSDK.DynamoDBv2(>= 3.7.x)Microsoft.Extensions.Logging.Abstractions(>= 8.0.0) — optionalMicrosoft.Extensions.DependencyInjection.Abstractions(>= 8.0.0) — optionalMicrosoft.Extensions.Options(>= 8.0.0) — optional
Links
| 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 was computed. 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 was computed. 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. |
-
net8.0
- AWSSDK.DynamoDBv2 (>= 3.7.511.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
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 |
|---|---|---|
| 0.1.1 | 71 | 2/15/2026 |