Skip to main content

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;

[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.

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:

  1. Syntax Analysis: The source generator identifies classes with [DynamoDbTable] attributes
  2. Entity Analysis: EntityAnalyzer parses the class and creates an EntityModel
  3. Validation: Configuration is validated and diagnostics are reported for any issues
  4. Code Generation: Three generators produce separate files:
    • MapperGeneratorYourEntity.g.cs (entity implementation)
    • KeysGeneratorYourEntityKeys.g.cs (key builders)
    • FieldsGeneratorYourEntityFields.g.cs (field constants)
  5. 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();

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