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; }
}

Schema Versioning​

Declare the schema version your project targets using the assembly-level FluentDynamoDbSchemaVersion attribute. This tells the source generator which version of the generated schema contract your code expects.

[assembly: FluentDynamoDbSchemaVersion(1, 0)]

Place this attribute in any file in your project (commonly AssemblyInfo.cs or GlobalUsings.cs). The two parameters are the major and minor version numbers.

Versioning Semantics​

ComponentMeaningExample Change
MajorBreaking schema changes — generated code signatures or behavior may differ from prior major versionsRenamed generated method, removed a generated property
MinorAdditive changes — new generated members or features that do not break existing codeNew helper method added to the generated Keys class

Incrementing the major version signals that upgrading may require code changes in consumers. Incrementing the minor version signals new functionality that is backward-compatible.

Diagnostics​

The source generator emits the following diagnostics related to schema versioning:

CodeSeverityDescription
FDDB110WarningMissing schema version attribute — no FluentDynamoDbSchemaVersion declared in the assembly
FDDB111ErrorDeclared version is below the minimum supported version
FDDB112ErrorDeclared version is above the current generator version
FDDB113InfoOlder-but-supported version detected — an upgrade is available
FDDB114ErrorMajor version is less than 1
FDDB115ErrorMinor version is less than 0
FDDB116ErrorMultiple schema version attributes detected in the same assembly

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