Source Generator Configuration
Configure the source generator for your project using attributes to control entity mapping, key generation, and code customization.
Installation
Install the package via NuGet:
dotnet add package Oproto.FluentDynamoDb
The source generator is automatically included as an analyzer and will run during compilation.
For field-level encryption support, also install:
dotnet add package Oproto.FluentDynamoDb.Encryption.Kms
Core Attributes
These attributes define how your entity maps to DynamoDB.
DynamoDbTable
Marks a class as a DynamoDB entity and specifies the table name:
[DynamoDbTable("transactions")]
public partial class Transaction
{
// Entity properties...
}
For multi-entity tables, use IsDefault to specify the default entity:
[DynamoDbTable("ecommerce", IsDefault = true)]
public partial class Order { }
[DynamoDbTable("ecommerce")]
public partial class OrderLine { }
Important: The class must be marked as partial for the source generator to extend it.
Namespace Property
Use the Namespace property to place the generated table class in a different namespace than the entity:
// Entity stays in its declared namespace
[DynamoDbTable("users", Namespace = "MyApp.Data.Tables")]
public partial class User
{
// Properties...
}
// Generated UsersTable class is in MyApp.Data.Tables namespace
This is useful when you want to organize generated code separately from your entity definitions.
PartitionKey
Marks a property as the partition key. Every entity must have exactly one partition key:
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
SortKey
Marks a property as the sort key (optional):
[SortKey]
[DynamoDbAttribute("sk")]
public string TransactionId { get; set; } = string.Empty;
DynamoDbAttribute
Maps a property to a DynamoDB attribute name:
[DynamoDbAttribute("amount")]
public decimal Amount { get; set; }
[DynamoDbAttribute("description")]
public string Description { get; set; } = string.Empty;
GsiPartitionKey
Configures a property as the partition key for a Global Secondary Index:
// Partition key for the GSI
[GsiPartitionKey("StatusIndex")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
GsiSortKey
Configures a property as the sort key for a Global Secondary Index:
// Sort key for the GSI
[GsiSortKey("StatusIndex")]
[DynamoDbAttribute("created_date")]
public DateTime CreatedDate { get; set; }
Custom Index Property Names
Use the Name property to customize the generated index property name:
// Custom name: generates table.StatusIndex
[GsiPartitionKey("status-index", Name = "StatusIndex")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
// Without Name: derives from DynamoDB index name
// "status-index" → StatusIndex (PascalCase conversion)
[GsiPartitionKey("status-index")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
The source generator creates nested classes for GSI field constants and key builders:
// Generated field constants
TransactionFields.StatusIndex.Status // "status"
TransactionFields.StatusIndex.CreatedDate // "created_date"
// Generated key builders
TransactionKeys.StatusIndex.Pk("pending")
TransactionKeys.StatusIndex.Sk(DateTime.UtcNow)
Usage in queries:
// Using custom-named index property
var response = await table.StatusIndex.Query<Transaction>()
.Where(x => x.Status == "pending")
.ToListAsync();
// Alternative: using format string
var response = await table.Query<Transaction>()
.UsingIndex("status-index")
.Where($"{TransactionFields.StatusIndex.Status} = {{0}}", "pending")
.ToListAsync();
LsiSortKey
Marks a property as the sort key for a Local Secondary Index (LSI). LSIs share the same partition key as the base table but have a different sort key:
[DynamoDbTable("orders")]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string OrderId { get; set; } = string.Empty;
// LSI for querying orders by date within a customer
[LsiSortKey("orders-by-date")]
[DynamoDbAttribute("order_date")]
public string OrderDate { get; set; } = string.Empty;
// Another LSI for querying by status
[LsiSortKey("orders-by-status")]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
}
Custom LSI Property Names
Use the Name property to customize the generated index property name:
// Custom name: generates table.OrdersByDate
[LsiSortKey("orders-by-date", Name = "OrdersByDate")]
[DynamoDbAttribute("order_date")]
public string OrderDate { get; set; } = string.Empty;
Key differences from GSI:
| Feature | Local Secondary Index | Global Secondary Index |
|---|---|---|
| Partition Key | Same as base table | Can be different |
| Sort Key | Different from base table | Can be different |
| Created | At table creation only | Anytime |
| Consistency | Strong or eventual | Eventual only |
| Throughput | Shares with base table | Independent |
Important notes:
- LSIs can only be created when the table is created (cannot be added later)
- Maximum of 5 LSIs per table
- The partition key is always the same as the base table (not specified in the attribute)
DynamoDbProjection
Defines a projection model that fetches only specific fields from an entity:
// 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;
}
// 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;
}
Requirements:
- Must be declared as
partialclass - All properties must exist on the source entity
- Property types must match the source entity types
- Property names are automatically mapped to DynamoDB attribute names
Usage:
// Query with automatic projection
var summaries = await table.Query
.Where("pk = {0}", userId)
.ToListAsync<TransactionSummary>();
// Only fetches: id, amount, status (reduces read capacity)
Projection Error Diagnostics:
| Code | Description |
|---|---|
| FDDB060 | Projection source entity not found |
| FDDB061 | Metadata inheritance failure |
| FDDB062 | Projection used in write operation context |
Type-Based Table References
For compile-time safe table class references, use typeof() instead of string names:
// Define your table class as partial
public partial class OrdersTable { }
// Reference it in entity definition
[DynamoDbTable(typeof(OrdersTable))]
public partial class Order
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string OrderId { get; set; } = string.Empty;
}
// String-based reference (existing behavior)
[DynamoDbTable("orders")]
public partial class Order { ... }
Type-based references provide:
- Compile-time validation: Referenced type must exist
- Refactoring support: Rename propagates automatically
- Diagnostic error: If referenced type is not
partial
Customization Attributes
Control how table classes and entity accessors are generated.
GenerateEntityProperty
Controls how entity accessor properties are generated on the table class:
[AttributeUsage(AttributeTargets.Class)]
public class GenerateEntityPropertyAttribute : Attribute
{
// Custom name for the accessor property
public string? Name { get; set; }
// Whether to generate the accessor property (default: true)
public bool Generate { get; set; } = true;
// Visibility modifier for the accessor property
public AccessModifier Modifier { get; set; } = AccessModifier.Public;
}
Custom accessor names:
[DynamoDbTable("ecommerce", IsDefault = true)]
[GenerateEntityProperty(Name = "CustomerOrders")]
public partial class Order { }
// Generated: table.CustomerOrders instead of table.Orders
Disable accessor generation:
[DynamoDbTable("ecommerce")]
[GenerateEntityProperty(Generate = false)]
public partial class InternalAuditLog { }
// No accessor generated - use for internal entities
Internal visibility:
[DynamoDbTable("ecommerce")]
[GenerateEntityProperty(Modifier = AccessModifier.Internal)]
public partial class OrderLine { }
// Generated: internal OrderLineAccessor OrderLines { get; }
GenerateAccessors
Controls which operation methods are generated and their visibility:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class GenerateAccessorsAttribute : Attribute
{
// Which operations to configure
public TableOperation Operations { get; set; } = TableOperation.All;
// Whether to generate the operations (default: true)
public bool Generate { get; set; } = true;
// Visibility modifier for the operations
public AccessModifier Modifier { get; set; } = AccessModifier.Public;
}
Generate only read operations:
[DynamoDbTable("ecommerce")]
[GenerateAccessors(Operations = TableOperation.Get | TableOperation.Query)]
public partial class ReadOnlyEntity { }
// Only Get() and Query() methods generated
Disable specific operations:
[DynamoDbTable("ecommerce")]
[GenerateAccessors(Operations = TableOperation.Delete, Generate = false)]
public partial class ImmutableEntity { }
// All operations except Delete() generated
Mixed visibility:
[DynamoDbTable("ecommerce")]
[GenerateAccessors(Operations = TableOperation.All, Modifier = AccessModifier.Internal)]
[GenerateAccessors(Operations = TableOperation.Get | TableOperation.Query, Modifier = AccessModifier.Public)]
public partial class Product { }
// Get() and Query() are public, other operations are internal
AccessModifier Enum
Defines visibility levels for generated code:
public enum AccessModifier
{
Public, // Accessible everywhere
Internal, // Accessible within assembly
Protected, // Accessible in derived classes
Private // Accessible only within class
}
Example - Creating a clean public API:
// Entity with internal operations
[DynamoDbTable("ecommerce")]
[GenerateAccessors(Operations = TableOperation.All, Modifier = AccessModifier.Internal)]
public partial class OrderLine { }
// Custom public methods in partial class
public partial class EcommerceTable
{
public async Task<List<OrderLine>> GetOrderLinesAsync(string customerId, string orderId)
{
// Validation
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentException("Customer ID is required");
// Use internal accessor
var response = await OrderLines.Query()
.Where($"{OrderLineFields.CustomerId} = :pk", new { pk = customerId })
.ToListAsync();
return response.Items;
}
}
TableOperation Enum
Defines DynamoDB operations as a flags enum:
[Flags]
public enum TableOperation
{
Get = 1,
Query = 2,
Scan = 4,
Put = 8,
Delete = 16,
Update = 32,
All = Get | Query | Scan | Put | Delete | Update
}
Combining operations:
// Read-only operations (Scan requires [Scannable] attribute on entity)
[GenerateAccessors(Operations = TableOperation.Get | TableOperation.Query | TableOperation.Scan)]
// Write operations only
[GenerateAccessors(Operations = TableOperation.Put | TableOperation.Delete | TableOperation.Update)]
// All operations
[GenerateAccessors(Operations = TableOperation.All)]
Even when TableOperation.Scan is included in the operations, the entity must also have the [Scannable] attribute for Scan operations to be available. This is because Scan operations are expensive and not recommended as a primary access pattern.
[DynamoDbTable("users")]
[Scannable] // Required for Scan operations
[GenerateAccessors(Operations = TableOperation.All)]
public partial class User
{
// Scan() method is now available
}
Field-Level Security Attributes
Protect sensitive data with logging redaction and encryption.
Sensitive Attribute
Marks fields to be redacted 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;
}
The source generator creates metadata for sensitive field detection:
// 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);
}
Sensitive values are replaced with [REDACTED] in logs.
Encrypted Attribute
Marks fields 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;
}
Requirements:
Install the encryption package:
dotnet add package Oproto.FluentDynamoDb.Encryption.Kms
If you use [Encrypted] without the package, the source generator emits a warning:
Warning FDDB4001: Property 'SocialSecurityNumber' has [Encrypted] attribute but
Oproto.FluentDynamoDb.Encryption.Kms package is not referenced.
Storage format:
Encrypted fields are stored as Binary (B) attribute type in DynamoDB using the AWS Encryption SDK message format.
Combining Security Attributes
Use both attributes for maximum protection:
[Encrypted] // Encrypted at rest in DynamoDB
[Sensitive] // Redacted from logs
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;
This provides:
- Encryption: Data is encrypted before storing in DynamoDB
- Logging Redaction: Field value is replaced with
[REDACTED]in logs
Behavior Attributes
Control entity behavior and enforce operational constraints.
RequireWriteTransaction
Marks an entity class as requiring write operations within a transaction. When applied, Put, Update, Delete, and BatchWrite operations throw InvalidOperationException unless performed within a TransactWrite operation.
This is useful for:
- Financial transactions requiring atomic updates
- Inventory management with consistency requirements
- Multi-entity operations that must succeed or fail together
- Audit-critical data that requires transactional guarantees
[DynamoDbTable("FinancialTransactions")]
[RequireWriteTransaction]
public partial class Transaction
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string AccountId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string TransactionId { get; set; } = string.Empty;
[DynamoDbAttribute("amount")]
public decimal Amount { get; set; }
}
Behavior:
// ❌ This throws InvalidOperationException:
await table.Transactions.Put(transaction).PutAsync();
// Error: "Entity 'Transaction' is marked with [RequireWriteTransaction] and cannot be
// modified outside of a transaction. Use DynamoDbTransactions.Write() to perform this operation."
// ❌ BatchWrite also throws:
await DynamoDbBatch.Write()
.Add(table.Transactions.Put(transaction))
.ExecuteAsync();
// ✅ TransactWrite is allowed:
await DynamoDbTransactions.Write()
.Add(table.Transactions.Put(transaction))
.ExecuteAsync();
// ✅ Multiple operations in a transaction:
await DynamoDbTransactions.Write()
.Add(table.Transactions.Put(debitTransaction))
.Add(table.Transactions.Put(creditTransaction))
.Add(table.Accounts.Update(accountId)
.Set(x => new AccountUpdateModel { Balance = newBalance }))
.ExecuteAsync();
Affected operations:
| Operation | Behavior |
|---|---|
Put().PutAsync() | Throws InvalidOperationException |
Update().UpdateAsync() | Throws InvalidOperationException |
Delete().DeleteAsync() | Throws InvalidOperationException |
DynamoDbBatch.Write().ExecuteAsync() | Throws InvalidOperationException |
DynamoDbTransactions.Write().ExecuteAsync() | ✅ Allowed |
Complete Entity Example
Here's a complete example showing all attribute types:
using Oproto.FluentDynamoDb.Attributes;
[DynamoDbTable("customers")]
[GenerateEntityProperty(Name = "CustomerRecords")]
[GenerateAccessors(Operations = TableOperation.All, Modifier = AccessModifier.Internal)]
[GenerateAccessors(Operations = TableOperation.Get | TableOperation.Query, Modifier = AccessModifier.Public)]
public partial class Customer
{
// Primary key
[PartitionKey]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string RecordType { get; set; } = string.Empty;
// Regular attributes
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
// Sensitive field - redacted from logs
[Sensitive]
[DynamoDbAttribute("email")]
public string Email { get; set; } = string.Empty;
// Encrypted and sensitive field
[Encrypted]
[Sensitive]
[DynamoDbAttribute("ssn")]
public string SocialSecurityNumber { get; set; } = string.Empty;
// GSI partition key
[GsiPartitionKey("EmailIndex")]
[DynamoDbAttribute("email_hash")]
public string EmailHash { get; set; } = string.Empty;
// GSI sort key
[GsiSortKey("EmailIndex")]
[DynamoDbAttribute("created_at")]
public DateTime CreatedAt { get; set; }
}
Troubleshooting
Common Issues
- "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 has
[DynamoDbTable]attribute
Viewing Generated Code
Generated files are available in your IDE:
- Visual Studio: Dependencies → Analyzers → Oproto.FluentDynamoDb.SourceGenerator
- Rider: External Libraries → Generated Files
Next Steps
- Architecture - Understand the internal components and design
- Overview - Return to the source generation overview