Skip to main content

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:

FeatureLocal Secondary IndexGlobal Secondary Index
Partition KeySame as base tableCan be different
Sort KeyDifferent from base tableCan be different
CreatedAt table creation onlyAnytime
ConsistencyStrong or eventualEventual only
ThroughputShares with base tableIndependent

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 partial class
  • 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:

CodeDescription
FDDB060Projection source entity not found
FDDB061Metadata inheritance failure
FDDB062Projection 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)]
Scan Requires Opt-In

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:

  1. Encryption: Data is encrypted before storing in DynamoDB
  2. 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:

OperationBehavior
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

  1. "Partial class required": Ensure your entity class is marked as partial
  2. "Missing partition key": Every entity must have exactly one [PartitionKey] property
  3. "Source generator not running": Clean and rebuild the solution
  4. "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