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;

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

  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:
    • MapperGenerator → YourEntity.g.cs (entity implementation)
    • KeysGenerator → YourEntityKeys.g.cs (key builders)
    • FieldsGenerator → YourEntityFields.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();

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 IReadOnlyEntity with 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 CodeDescription
FDDB060Projection source entity not found - the type referenced in [DynamoDbProjection(typeof(...))] doesn't exist or isn't a valid entity
FDDB061Metadata inheritance failure - projection cannot inherit metadata from source entity
FDDB062Projection 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