| title | Source Generator Guide | ||||
|---|---|---|---|---|---|
| category | guide | ||||
| order | 2 | ||||
| keywords |
|
||||
| related |
|
Documentation > Source Generator Guide
The Oproto.FluentDynamoDb source generator automatically creates entity mapping code, field constants, key builders, and type-safe async methods to reduce boilerplate and provide a more EF/LINQ-like experience.
Quick Links: Quick Start | First Entity | Entity Definition
dotnet add package Oproto.FluentDynamoDbThe source generator is automatically included as an analyzer and will run during compilation.
For the simplest case (one entity per table), see Single-Entity Tables.
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;
[GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
[GlobalSecondaryIndex("StatusIndex", IsSortKey = true)]
[DynamoDbAttribute("created_date")]
public DateTime CreatedDate { get; set; }
}Important: The class must be marked as partial for the source generator to extend it.
The source generator creates several types of code for each entity:
// 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";
}
}// 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");
}
}The source generator makes your entity implement IDynamoDbEntity with 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
}
public static TSelf FromDynamoDb<TSelf>(Dictionary<string, AttributeValue> item) where TSelf : IDynamoDbEntity
{
// Generated mapping logic
}
// Additional generated methods...
}The source generator creates table classes that provide type-safe access to DynamoDB operations. The generated code structure depends on whether you have a single entity or multiple entities per table.
For tables with one entity, the generated table class provides table-level operations:
// Generated table class for single-entity table
// Table name is configurable at runtime for different environments
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 transaction = await transactionsTable.Get()
.WithKey(TransactionFields.TenantId, TransactionKeys.Pk("tenant123"))
.WithKey(TransactionFields.TransactionId, TransactionKeys.Sk("txn456"))
.GetItemAsync();
if (transaction != null)
{
Console.WriteLine($"Found transaction: {transaction.Description}");
}See Single-Entity Tables for complete documentation.
For tables with multiple entities (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();See Multi-Entity Tables for complete documentation.
For manual usage without source generation, create a class that inherits from DynamoDbTableBase:
public class TransactionsTableManual : DynamoDbTableBase
{
public TransactionsTableManual(IAmazonDynamoDB client, string tableName)
: base(client, tableName) { }
}
var table = new TransactionsTableManual(dynamoDbClient, "transactions");
// Put item using generated mapping
await table.Put()
.WithItem(transaction)
.PutAsync();
// Get item with strongly-typed response
var response = await table.Get<Transaction>()
.WithKey(TransactionFields.TenantId, TransactionKeys.Pk("tenant123"))
.WithKey(TransactionFields.TransactionId, TransactionKeys.Sk("txn456"))
.GetItemAsync();// Query with strongly-typed results
var transactions = await table.Query<Transaction>()
.Where($"{TransactionFields.TenantId} = :pk", new { pk = TransactionKeys.Pk("tenant123") })
.ToListAsync();
foreach (var transaction in transactions)
{
Console.WriteLine($"Transaction: {transaction.Description} - {transaction.Amount}");
}// Query GSI using generated field constants and key builders
var pendingTransactions = await table.Query<Transaction>()
.UsingIndex("StatusIndex")
.Where($"{Transaction.Fields.Status} = {{0}}", "pending")
.ToListAsync();For entities that span multiple DynamoDB items:
[DynamoDbTable("transactions")]
public partial class TransactionWithEntries
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string TransactionId { get; set; } = string.Empty;
// This collection will be mapped to separate DynamoDB items
public List<LedgerEntry> LedgerEntries { get; set; } = new();
}
// Query automatically groups items by partition key
var transactionsWithEntries = await table.Query<TransactionWithEntries>()
.Where($"{TransactionWithEntriesFields.TenantId} = :pk",
new { pk = TransactionWithEntriesKeys.Pk("tenant123") })
.ToListAsync();
// Each TransactionWithEntries contains all related LedgerEntry itemsDefine related entities that are automatically populated based on query results:
[DynamoDbTable("transactions")]
public partial class TransactionWithRelated
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string TransactionId { get; set; } = string.Empty;
// Related entities populated based on sort key patterns
[RelatedEntity(SortKeyPattern = "audit#*")]
public List<AuditEntry>? AuditEntries { get; set; }
[RelatedEntity(SortKeyPattern = "summary")]
public TransactionSummary? Summary { get; set; }
}All generated methods support scoped clients for tenant isolation:
public class TransactionService
{
private readonly DynamoDbTableBase _table;
private readonly IStsTokenService _stsService;
public async Task<Transaction?> GetTransactionAsync(string tenantId, string transactionId, ClaimsPrincipal user)
{
// Generate tenant-scoped client
var scopedClient = await _stsService.CreateClientForTenant(tenantId, user.Claims);
// Use scoped client for the operation
var transaction = await _table.Get<Transaction>()
.WithClient(scopedClient)
.WithKey(TransactionFields.TenantId, TransactionKeys.Pk(tenantId))
.WithKey(TransactionFields.TransactionId, TransactionKeys.Sk(transactionId))
.GetItemAsync();
return transaction;
}
}Install the optional FluentResults package for Result return patterns:
dotnet add package Oproto.FluentDynamoDb.FluentResultsusing Oproto.FluentDynamoDb.FluentResults;
// Returns Result<Transaction?> instead of throwing exceptions
var result = await table.Get<Transaction>()
.WithKey(TransactionFields.TenantId, TransactionKeys.Pk("tenant123"))
.WithKey(TransactionFields.TransactionId, TransactionKeys.Sk("txn456"))
.GetItemAsync();
if (result.IsSuccess)
{
var transaction = result.Value;
// Handle success
}
else
{
// Handle failure without exceptions
Console.WriteLine($"Error: {result.Errors.First().Message}");
}The DynamoDB Source Generator consists of four main components that work together to analyze your entity classes and generate optimized mapping code:
Purpose: Analyzes class declarations to extract DynamoDB entity information.
Location: Oproto.FluentDynamoDb.SourceGenerator/Analysis/EntityAnalyzer.cs
Responsibilities:
- Parses class declarations with
[DynamoDbTable]attributes - Extracts property information including keys, attributes, and relationships
- Validates entity configuration (partition key requirements, conflicting patterns)
- Reports diagnostic errors and warnings for configuration issues
- Produces
EntityModeldata structures for code generation
Key Features:
- Detects partition and sort keys
- Identifies Global Secondary Index configurations
- Analyzes related entity relationships
- Validates computed and extracted key patterns
- Ensures classes are marked as
partial
Purpose: Generates entity mapping code for converting between C# objects and DynamoDB AttributeValue dictionaries.
Location: Oproto.FluentDynamoDb.SourceGenerator/Generators/MapperGenerator.cs
Responsibilities:
- Generates
ToDynamoDb<TSelf>()method for entity-to-DynamoDB conversion - Generates
FromDynamoDb<TSelf>()methods (single-item and multi-item overloads) - Generates
GetPartitionKey()method for extracting partition keys - Generates
MatchesEntity()method for entity type discrimination - Generates
GetEntityMetadata()method for future LINQ support
Performance Optimizations:
- 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
Generated Code Structure:
public partial class YourEntity
{
// High-performance conversion with pre-allocated capacity
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<string, AttributeValue> ToDynamoDb<TSelf>(TSelf entity)
{
// Pre-allocate with exact capacity (no resizing needed)
var item = new Dictionary<string, AttributeValue>(propertyCount);
// Direct property access (no reflection)
item["pk"] = new AttributeValue { S = typedEntity.PartitionKey };
// ... more mappings
return item;
}
// Additional generated methods...
}Purpose: Generates static key builder methods for DynamoDB entities.
Location: Oproto.FluentDynamoDb.SourceGenerator/Generators/KeysGenerator.cs
Responsibilities:
- Generates partition key and sort key builder methods
- Handles composite keys with multiple components, prefixes, and separators
- Generates separate key builders for each Global Secondary Index
- Creates extraction helper methods for composite keys
- Ensures type safety for all key builder parameters
Generated Code Structure:
public static partial class YourEntityKeys
{
// Main table keys
public static string Pk(string tenantId, string customerId)
=> $"{tenantId}#{customerId}";
public static string Sk(DateTime date)
=> date.ToString("yyyy-MM-dd");
// GSI keys
public static partial class StatusIndex
{
public static string Pk(string status) => $"STATUS#{status}";
}
// Extraction helpers
public static (string TenantId, string CustomerId) ExtractPkComponents(string pk)
{
var parts = pk.Split('#');
return (parts[0], parts[1]);
}
}Purpose: Generates static field name constant classes for DynamoDB entities.
Location: Oproto.FluentDynamoDb.SourceGenerator/Generators/FieldsGenerator.cs
Responsibilities:
- Generates string constants for all DynamoDB attribute names
- Creates nested classes for Global Secondary Index fields
- Provides compile-time safety when referencing attribute names
- Handles reserved word mapping and special cases
Generated Code Structure:
public static partial class YourEntityFields
{
// Main table fields
public const string PartitionKey = "pk";
public const string SortKey = "sk";
public const string Amount = "amount";
public const string Status = "status";
// GSI fields
public static partial class StatusIndex
{
public const string Status = "status";
public const string CreatedDate = "created_date";
}
}- 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
- 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
Single Responsibility: Each generator has a focused purpose and generates one type of code.
Performance First: Generated code is optimized for minimal allocations and maximum throughput:
- Pre-allocated collections with exact capacity
- Aggressive inlining for hot paths
- Direct property access (no reflection)
- Efficient string operations
AOT Compatibility: All code generation produces AOT-safe code:
- No runtime reflection
- All types resolved at compile time
- Trimmer-safe implementations
Maintainability: Clear separation of concerns makes the codebase easy to understand and extend:
EntityAnalyzerhandles all parsing and validation- Each generator focuses on one output type
- No circular dependencies between components
The source generator supports field-level security through the [Sensitive] and [Encrypted] attributes.
Mark fields with [Sensitive] to exclude their values from log output:
[DynamoDbTable("users")]
public partial class User
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string UserId { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
[Sensitive] // Redacted from logs
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
[Sensitive] // Redacted from logs
[DynamoDbAttribute("phone")]
public string PhoneNumber { get; set; } = string.Empty;
}Generated Code:
The source generator creates a static set of sensitive field names:
// Generated: UserMetadata.g.cs
public static partial class UserMetadata
{
private static readonly HashSet<string> SensitiveFields = new()
{
"email",
"phone"
};
public static bool IsSensitiveField(string fieldName)
=> SensitiveFields.Contains(fieldName);
}This metadata is used by the logging infrastructure to replace sensitive values with [REDACTED].
Mark fields with [Encrypted] for encryption at rest using AWS KMS:
[DynamoDbTable("customers")]
public partial class CustomerData
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[Encrypted] // Encrypted at rest
[Sensitive] // Also redacted from logs
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;
[Encrypted(CacheTtlSeconds = 600)] // Custom cache TTL
[Sensitive]
[DynamoDbAttribute("cc")]
public string CreditCardNumber { get; set; } = string.Empty;
}Generated Code:
The source generator modifies the ToDynamoDb and FromDynamoDb methods to include encryption/decryption calls:
// Generated: CustomerData.g.cs
public static Dictionary<string, AttributeValue> ToDynamoDb<TSelf>(TSelf entity)
{
var typedEntity = (CustomerData)(object)entity;
var item = new Dictionary<string, AttributeValue>(propertyCount);
// Regular field
item["pk"] = new AttributeValue { S = typedEntity.CustomerId };
// Encrypted field - calls IFieldEncryptor if available
if (_fieldEncryptor != null)
{
var plaintext = Encoding.UTF8.GetBytes(typedEntity.SocialSecurityNumber);
var context = new FieldEncryptionContext
{
ContextId = _encryptionContext,
CacheTtlSeconds = 300 // From attribute
};
var ciphertext = await _fieldEncryptor.EncryptAsync(
plaintext,
"ssn",
context,
cancellationToken);
item["ssn"] = new AttributeValue { B = new MemoryStream(ciphertext) };
}
else
{
// Fallback to plaintext if no encryptor configured
item["ssn"] = new AttributeValue { S = typedEntity.SocialSecurityNumber };
}
return item;
}
public static TSelf FromDynamoDb<TSelf>(Dictionary<string, AttributeValue> item)
{
var entity = new CustomerData();
// Regular field
if (item.TryGetValue("pk", out var pkValue))
entity.CustomerId = pkValue.S;
// Encrypted field - calls IFieldEncryptor if available
if (item.TryGetValue("ssn", out var ssnValue))
{
if (_fieldEncryptor != null && ssnValue.B != null)
{
var context = new FieldEncryptionContext
{
ContextId = _encryptionContext,
CacheTtlSeconds = 300
};
var plaintext = await _fieldEncryptor.DecryptAsync(
ssnValue.B.ToArray(),
"ssn",
context,
cancellationToken);
entity.SocialSecurityNumber = Encoding.UTF8.GetString(plaintext);
}
else if (ssnValue.S != null)
{
// Fallback for plaintext data
entity.SocialSecurityNumber = ssnValue.S;
}
}
return (TSelf)(object)entity;
}If you use [Encrypted] without the Oproto.FluentDynamoDb.Encryption.Kms package, the source generator emits a warning:
Warning FDDB4001: Property 'SocialSecurityNumber' has [Encrypted] attribute but Oproto.FluentDynamoDb.Encryption.Kms package is not referenced.
Add the package to enable field encryption: dotnet add package Oproto.FluentDynamoDb.Encryption.Kms
Solution:
dotnet add package Oproto.FluentDynamoDb.Encryption.KmsYou can combine [Sensitive] and [Encrypted] for maximum protection:
[Encrypted] // Encrypted at rest in DynamoDB
[Sensitive] // Redacted from logs
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;The source generator applies both features:
- Encryption: Data is encrypted before storing in DynamoDB
- Logging Redaction: Field value is replaced with
[REDACTED]in logs
Encrypted fields are stored as Binary (B) attribute type in DynamoDB using the AWS Encryption SDK message format:
{
"pk": { "S": "customer-123" },
"ssn": { "B": "<AWS Encryption SDK binary message>" }
}The AWS Encryption SDK format includes:
- Algorithm suite identifier
- Encrypted data key(s)
- Initialization vector (IV)
- Encrypted content
- Authentication tag
- Digital signature (for key commitment)
- Field-Level Security Guide - Complete security guide
- Encryption.Kms Package - Encryption package documentation
- Attribute Reference - Complete attribute documentation
- .NET 6.0 and later
- .NET Framework is not supported (source generators require modern .NET)
The generated code is fully AOT-compatible:
- No reflection at runtime
- All type information resolved at compile time
- Trimmer-safe and ready for Native AOT deployment
- C# 11.0 or later (for static abstract interface members)
- Source generators require the Roslyn compiler
- "Partial class required": Ensure your entity class is marked as
partial - "Missing partition key": Every entity must have exactly one
[PartitionKey]property - "Source generator not running": Clean and rebuild the solution
- "Generated code not found": Check that the entity is in a
partialclass and has[DynamoDbTable]
Generated files are available in your IDE:
- Visual Studio: Dependencies → Analyzers → Oproto.FluentDynamoDb.SourceGenerator
- Rider: External Libraries → Generated Files
- Generated mapping code is optimized for performance
- No reflection overhead at runtime
- Minimal memory allocations during mapping
- Incremental source generation for fast builds
- Add
[DynamoDbTable]and property attributes to existing entities - Mark classes as
partial - Replace manual mapping code with generated type-safe async methods (
GetItemAsync(),PutAsync(),ToListAsync(), etc.) - Update field references to use generated constants
- Replace manual key construction with generated key builders
The fluent API remains fully compatible, so you can migrate incrementally.
See Adoption Guide for detailed migration strategies.
- Quick Start - Get started in 5 minutes
- First Entity - Deep dive into entity definition
- Entity Definition - Complete entity definition guide
- Basic Operations - CRUD operations
- Expression Formatting - Format string reference
- Adoption Guide - Migration strategies
- Troubleshooting - Common issues and solutions