Source Generation Overview
The Oproto.FluentDynamoDb source generator automatically creates entity mapping code, field constants, key builders, and enhanced ExecuteAsync methods to reduce boilerplate and provide a more EF/LINQ-like experience.
Benefits
- Zero runtime reflection - All mapping code is generated at compile time
- Full AOT compatibility - Ready for Native AOT deployment with no runtime code generation
- Type-safe queries - Compile-time validation of field names and key structures
- Clean, readable generated code - Easy to debug and understand
- IntelliSense support - Full IDE support for generated types and methods
- Minimal memory allocations - Pre-allocated dictionaries and optimized conversions
Getting Started
1. Install the Package
dotnet add package Oproto.FluentDynamoDb
The source generator is automatically included as an analyzer and will run during compilation.
2. Define Your Entity
Create a C# class decorated with [DynamoDbTable] and mark it as partial:
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("transactions")]
public partial class Transaction
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string TransactionId { get; set; } = string.Empty;
[DynamoDbAttribute("amount")]
public decimal Amount { get; set; }
[DynamoDbAttribute("description")]
public string Description { get; set; } = string.Empty;
[GsiPartitionKey("StatusIndex")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
[GsiSortKey("StatusIndex")]
[DynamoDbAttribute("created_date")]
public DateTime CreatedDate { get; set; }
}
Important: The class must be marked as partial for the source generator to extend it.
Generated Code Types
The source generator creates three types of code for each entity:
1. Field Constants
Static string constants representing DynamoDB attribute names, providing compile-time safety when referencing fields:
// Generated: TransactionFields.cs
public static partial class TransactionFields
{
public const string TenantId = "pk";
public const string TransactionId = "sk";
public const string Amount = "amount";
public const string Description = "description";
public const string Status = "status";
public const string CreatedDate = "created_date";
public static partial class StatusIndex
{
public const string Status = "status";
public const string CreatedDate = "created_date";
}
}
2. Key Builders
Static methods for constructing partition and sort key values with type safety:
// Generated: TransactionKeys.cs
public static partial class TransactionKeys
{
public static string Pk(string tenantId) => tenantId;
public static string Sk(string transactionId) => transactionId;
public static partial class StatusIndex
{
public static string Pk(string status) => status;
public static string Sk(DateTime createdDate) => createdDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
}
}
3. Entity Mappers
The source generator makes your entity implement IDynamoDbEntity with high-performance mapping methods:
// Generated: Transaction.g.cs
public partial class Transaction : IDynamoDbEntity
{
public static Dictionary<string, AttributeValue> ToDynamoDb<TSelf>(TSelf entity)
where TSelf : IDynamoDbEntity
{
// Generated mapping logic with pre-allocated capacity
}
public static TSelf FromDynamoDb<TSelf>(Dictionary<string, AttributeValue> item)
where TSelf : IDynamoDbEntity
{
// Generated mapping logic with direct property access
}
// Additional generated methods...
}
Code Generation Pipeline
The source generator follows a structured pipeline to transform your entity definitions into optimized code:
- Syntax Analysis: The source generator identifies classes with
[DynamoDbTable]attributes - Entity Analysis:
EntityAnalyzerparses the class and creates anEntityModel - Validation: Configuration is validated and diagnostics are reported for any issues
- Code Generation: Three generators produce separate files:
MapperGenerator→YourEntity.g.cs(entity implementation)KeysGenerator→YourEntityKeys.g.cs(key builders)FieldsGenerator→YourEntityFields.g.cs(field constants)
- Compilation: Generated code is compiled with your project
Table Patterns
Single-Entity Tables
For tables with one entity type, the generated table class provides direct table-level operations:
// Generated table class for single-entity table
var transactionsTable = new TransactionsTable(dynamoDbClient, "transactions");
// Create a transaction
var transaction = new Transaction
{
TenantId = "tenant123",
TransactionId = "txn456",
Amount = 100.50m,
Description = "Payment",
Status = "pending",
CreatedDate = DateTime.UtcNow
};
// Put item using table-level operation
await transactionsTable.Put(transaction)
.PutAsync();
// Get item with strongly-typed response
var response = await transactionsTable.Get()
.WithKey(TransactionFields.TenantId, TransactionKeys.Pk("tenant123"))
.WithKey(TransactionFields.TransactionId, TransactionKeys.Sk("txn456"))
.GetItemAsync();
if (response.Item != null)
{
Console.WriteLine($"Found transaction: {response.Item.Description}");
}
Multi-Entity Tables
For tables with multiple entity types (single-table design), the generated table class provides entity-specific accessors:
// Multiple entities sharing the same table
[DynamoDbTable("ecommerce", IsDefault = true)]
public partial class Order { }
[DynamoDbTable("ecommerce")]
public partial class OrderLine { }
// Generated table class with entity accessors
var ecommerceTable = new EcommerceTable(dynamoDbClient, "ecommerce");
// Access operations via entity accessors
await ecommerceTable.Orders.Put(order)
.PutAsync();
await ecommerceTable.OrderLines.Put(orderLine)
.PutAsync();
// Query specific entity type
var orders = await ecommerceTable.Orders.Query()
.Where($"{OrderFields.CustomerId} = {{0}}", OrderKeys.Pk("customer123"))
.ToListAsync();
// Table-level operations use the default entity (Order)
var defaultOrder = await ecommerceTable.Get()
.WithKey(OrderFields.OrderId, OrderKeys.Pk("order123"))
.GetItemAsync();
Projection Models
Projection models enable automatic generation and application of DynamoDB projection expressions. They reduce boilerplate, prevent common mistakes, and optimize query costs by fetching only required data.
Defining Projections
Use the [DynamoDbProjection] attribute to define a projection model:
// Full entity
[DynamoDbTable("transactions")]
public partial class Transaction
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Id { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
public string Description { get; set; } = string.Empty;
public Dictionary<string, string> Metadata { get; set; } = new();
}
// Projection model - only the fields you need
[DynamoDbProjection(typeof(Transaction))]
public partial class TransactionSummary
{
public string Id { get; set; } = string.Empty;
public decimal Amount { get; set; }
public string Status { get; set; } = string.Empty;
}
Projection Interface Hierarchy
The source generator creates an interface hierarchy that enables projections to work seamlessly with query builders:
IEntityMetadataProvider
↓
IReadOnlyEntity<TSelf> ← Projections implement this
↓
IDynamoDbEntity<TSelf> ← Full entities implement this
- IReadOnlyEntity: Base interface for read-only operations (
FromDynamoDb,GetPartitionKey) - IDynamoDbEntity: Extends
IReadOnlyEntitywith write operations (ToDynamoDb,MatchesEntity)
This design allows projections to be used with QueryRequestBuilder<T> while maintaining their read-only nature.
Querying with Projections
// Projection is automatically applied based on the result type
var summaries = await table.Query
.Where("pk = {0}", userId)
.ToListAsync<TransactionSummary>();
// Only fetches: id, amount, status (not description or metadata)
// Reduces data transfer and read capacity consumption
Index with Default Projection Type
Define indexes with a default projection type for simplified queries:
// Index with default projection type
public DynamoDbIndex<TransactionSummary> StatusIndex =>
new DynamoDbIndex<TransactionSummary>(this, "status-index", TransactionSummary.ProjectionExpression);
// Non-generic Query() uses the default projection type automatically
var results = await table.StatusIndex.Query()
.Where(x => x.Status == "pending")
.ToListAsync();
// Override to use full entity when needed
var fullResults = await table.StatusIndex.Query<Transaction>()
.Where(x => x.Status == "pending")
.ToListAsync();
Projection Error Handling
The source generator provides compile-time validation for projection configurations:
| Diagnostic Code | Description |
|---|---|
| FDDB060 | Projection source entity not found - the type referenced in [DynamoDbProjection(typeof(...))] doesn't exist or isn't a valid entity |
| FDDB061 | Metadata inheritance failure - projection cannot inherit metadata from source entity |
| FDDB062 | Projection interface violation - projection is used in a write operation context |
// ❌ FDDB060: Source entity not found
[DynamoDbProjection(typeof(NonExistentEntity))]
public partial class InvalidProjection { }
// ❌ FDDB062: Projections cannot be used for writes
await table.Put(projectionInstance).PutAsync(); // Compile error
Performance Optimizations
The generated code is optimized for maximum performance:
- Pre-allocated dictionaries: Dictionary capacity is calculated at compile time to avoid resizing
- Aggressive inlining: Methods marked with
[MethodImpl(MethodImplOptions.AggressiveInlining)] - Direct property access: No reflection overhead at runtime
- Efficient type conversions: Optimized conversion logic for common types
Next Steps
- Configuration - Learn about all available attributes and customization options
- Architecture - Understand the internal components and design