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.

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)]
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

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

  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