Internal Architecture
This document explains how the internal components of Oproto.FluentDynamoDb work together. Understanding this architecture helps you leverage the library effectively and troubleshoot issues.
Layered Architecture
Oproto.FluentDynamoDb uses a layered architecture combining compile-time source generation with runtime expression translation:
Layer Responsibilities
| Layer | Responsibility |
|---|---|
| Application Code | Your business logic using the fluent API |
| Generated Code | Type-specific mappers, table classes, and accessors created at compile time |
| Runtime Library | Request builders, expression translation, and base table operations |
| AWS SDK | Direct communication with DynamoDB service |
Source Generator Pipeline
The source generator runs at compile time to produce type-specific code:
Generator Components
The source generator consists of four main components that work together to produce the generated code:
EntityAnalyzer
The EntityAnalyzer uses Roslyn to extract metadata from your entity classes:
- Table name from
[DynamoDbTable]attribute - Key structure from
[PartitionKey]and[SortKey]attributes - Property types and mappings from class properties
- GSI configurations from
[GlobalSecondaryIndex]attributes
MapperGenerator
Generates the {Entity}.Mapper.g.cs file containing:
ToDynamoDb()- Converts entity to DynamoDB AttributeValue dictionaryFromDynamoDb()- Creates entity from DynamoDB itemGetPartitionKey()- Extracts partition key for groupingMatchesEntity()- Discriminator for multi-entity tablesGetEntityMetadata()- Returns property mapping information
KeysGenerator
Generates the nested {Entity}.Keys class with type-safe key construction methods:
public partial class User
{
public static partial class Keys
{
// Create full composite key
public static Dictionary<string, AttributeValue> Create(
string tenantId,
string userId)
{
return new Dictionary<string, AttributeValue>(2)
{
["tenantId"] = new AttributeValue { S = tenantId },
["userId"] = new AttributeValue { S = userId }
};
}
// Create partition key only (for queries)
public static Dictionary<string, AttributeValue> PartitionKey(string tenantId)
{
return new Dictionary<string, AttributeValue>(1)
{
["tenantId"] = new AttributeValue { S = tenantId }
};
}
}
}
FieldsGenerator
Generates the nested {Entity}.Fields class with compile-time constants:
public partial class User
{
public static partial class Fields
{
public const string TenantId = "tenantId";
public const string UserId = "userId";
public const string Name = "name";
public const string Email = "email";
public const string Status = "status";
// GSI-specific field classes
public static partial class EmailIndex
{
public const string PartitionKey = "email";
public const string SortKey = "tenantId";
}
}
}
IDynamoDbEntity Interface
The IDynamoDbEntity interface is the foundation of the source generation system. Entities marked with [DynamoDbTable] implement this interface through generated code.
public interface IDynamoDbEntity : IEntityMetadataProvider
{
// Convert entity to DynamoDB AttributeValue dictionary
static abstract Dictionary<string, AttributeValue> ToDynamoDb<TSelf>(
TSelf entity,
IDynamoDbLogger? logger = null) where TSelf : IDynamoDbEntity;
// Create entity from single DynamoDB item
static abstract TSelf FromDynamoDb<TSelf>(
Dictionary<string, AttributeValue> item,
IDynamoDbLogger? logger = null) where TSelf : IDynamoDbEntity;
// Create entity from multiple DynamoDB items (multi-item entities)
static abstract TSelf FromDynamoDb<TSelf>(
IList<Dictionary<string, AttributeValue>> items,
IDynamoDbLogger? logger = null) where TSelf : IDynamoDbEntity;
// Extract partition key for grouping items
static abstract string GetPartitionKey(Dictionary<string, AttributeValue> item);
// Determine if item matches this entity type (discriminator)
static abstract bool MatchesEntity(Dictionary<string, AttributeValue> item);
}
Key Design Decisions
| Decision | Benefit |
|---|---|
| Static abstract methods | Enable generic constraints while maintaining AOT compatibility |
| No reflection | All mapping logic is generated at compile time |
| Multi-item support | Entities can span multiple DynamoDB items (e.g., orders with line items) |
Request Builder Pattern
Request builders provide the fluent API for constructing DynamoDB operations. Each operation type has a dedicated builder that integrates with generated code.
Available Builders
| Builder | Purpose |
|---|---|
QueryRequestBuilder<T> | Build Query operations with key conditions and filters |
GetItemRequestBuilder<T> | Build GetItem operations for single-item retrieval |
PutItemRequestBuilder<T> | Build PutItem operations for creating/replacing items |
UpdateItemRequestBuilder<T> | Build UpdateItem operations with SET/REMOVE/ADD expressions |
DeleteItemRequestBuilder<T> | Build DeleteItem operations with optional conditions |
ScanRequestBuilder<T> | Build Scan operations for full table scans (requires [Scannable] attribute) |
BatchGetBuilder | Build BatchGetItem for multi-item retrieval |
BatchWriteBuilder | Build BatchWriteItem for bulk writes |
TransactionWriteBuilder | Build TransactWriteItems for ACID transactions |
TransactionGetBuilder | Build TransactGetItems for consistent multi-item reads |
Builder Integration
Builders integrate with generated code through:
- Entity metadata - Builders access property mappings via
IEntityMetadataProvider - Type-specific methods - Generated extension methods provide entity-aware operations
- Expression translation - Lambda expressions are converted to DynamoDB syntax at runtime
Example: Builder Chain vs Direct Methods
Builder chain approach (full control):
var user = await table.Users.Get()
.WithKey("tenantId", tenantId)
.WithKey("userId", userId)
.WithProjection("name", "email", "status")
.WithConsistentRead(true)
.GetItemAsync();
Direct async method (simple cases):
var user = await table.Users.GetAsync(tenantId, userId);
DynamoDbTableBase
DynamoDbTableBase is the abstract base class for all generated table classes:
public abstract class DynamoDbTableBase : IDynamoDbTable
{
public IAmazonDynamoDB DynamoDbClient { get; }
public string Name { get; }
protected FluentDynamoDbOptions Options { get; }
protected IDynamoDbLogger Logger { get; }
protected IFieldEncryptor? FieldEncryptor { get; }
// Base operation methods
public QueryRequestBuilder<TEntity> Query<TEntity>() where TEntity : class;
public GetItemRequestBuilder<TEntity> Get<TEntity>() where TEntity : class;
public PutItemRequestBuilder<TEntity> Put<TEntity>() where TEntity : class;
public UpdateItemRequestBuilder<TEntity> Update<TEntity>() where TEntity : class;
public DeleteItemRequestBuilder<TEntity> Delete<TEntity>() where TEntity : class;
// Note: Scan requires [Scannable] attribute on the entity
public ScanRequestBuilder<TEntity> Scan<TEntity>() where TEntity : class;
}
Generated table classes extend this base and add:
- Entity-specific accessor properties (
table.Users,table.Orders) - Type-specific operation overloads
- Index properties for GSI access
Generated Code Categories
The source generator produces several categories of code for each entity:
| Category | File Pattern | Contents |
|---|---|---|
| Entity Mappers | {Entity}.Mapper.g.cs | ToDynamoDb(), FromDynamoDb(), GetPartitionKey(), MatchesEntity() |
| Field Constants | Nested {Entity}.Fields class | Compile-time constants for DynamoDB attribute names |
| Key Builders | Nested {Entity}.Keys class | Type-safe key construction methods |
| Table Classes | {Table}Table.g.cs | Table class extending DynamoDbTableBase with entity accessors |
| Entity Accessors | Nested {Entity}Accessor class | Entity-specific operation methods |
| Extension Methods | {Entity}Extensions.g.cs | Type-specific versions of generic extension methods |
| Security Metadata | {Entity}.SecurityMetadata.g.cs | Sensitive field detection for logging redaction |
Discovering Generated Code
Generated files are located at:
{ProjectRoot}/obj/Debug/net8.0/generated/
└── Oproto.FluentDynamoDb.SourceGenerator/
└── Oproto.FluentDynamoDb.SourceGenerator.DynamoDbSourceGenerator/
├── User.Mapper.g.cs
├── User.Fields.g.cs
├── User.Keys.g.cs
├── UsersTable.g.cs
└── ...
To emit generated files to disk for inspection:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>