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.
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;
GlobalSecondaryIndex
Configures a property as part of a Global Secondary Index:
// Partition key for the GSI
[GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)]
[DynamoDbAttribute("status")]
public string Status { get; set; } = string.Empty;
// Sort key for the GSI
[GlobalSecondaryIndex("StatusIndex", IsSortKey = true)]
[DynamoDbAttribute("created_date")]
public DateTime CreatedDate { get; set; }
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:
var response = await table.Query<Transaction>()
.UsingIndex("StatusIndex")
.Where($"{TransactionFields.StatusIndex.Status} = {{0}}", "pending")
.ToListAsync();
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
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
[GlobalSecondaryIndex("EmailIndex", IsPartitionKey = true)]
[DynamoDbAttribute("email_hash")]
public string EmailHash { get; set; } = string.Empty;
// GSI sort key
[GlobalSecondaryIndex("EmailIndex", IsSortKey = true)]
[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