Skip to main content

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

LayerResponsibility
Application CodeYour business logic using the fluent API
Generated CodeType-specific mappers, table classes, and accessors created at compile time
Runtime LibraryRequest builders, expression translation, and base table operations
AWS SDKDirect 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 dictionary
  • FromDynamoDb() - Creates entity from DynamoDB item
  • GetPartitionKey() - Extracts partition key for grouping
  • MatchesEntity() - Discriminator for multi-entity tables
  • GetEntityMetadata() - 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

DecisionBenefit
Static abstract methodsEnable generic constraints while maintaining AOT compatibility
No reflectionAll mapping logic is generated at compile time
Multi-item supportEntities 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

BuilderPurpose
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)
BatchGetBuilderBuild BatchGetItem for multi-item retrieval
BatchWriteBuilderBuild BatchWriteItem for bulk writes
TransactionWriteBuilderBuild TransactWriteItems for ACID transactions
TransactionGetBuilderBuild TransactGetItems for consistent multi-item reads

Builder Integration

Builders integrate with generated code through:

  1. Entity metadata - Builders access property mappings via IEntityMetadataProvider
  2. Type-specific methods - Generated extension methods provide entity-aware operations
  3. 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:

CategoryFile PatternContents
Entity Mappers{Entity}.Mapper.g.csToDynamoDb(), FromDynamoDb(), GetPartitionKey(), MatchesEntity()
Field ConstantsNested {Entity}.Fields classCompile-time constants for DynamoDB attribute names
Key BuildersNested {Entity}.Keys classType-safe key construction methods
Table Classes{Table}Table.g.csTable class extending DynamoDbTableBase with entity accessors
Entity AccessorsNested {Entity}Accessor classEntity-specific operation methods
Extension Methods{Entity}Extensions.g.csType-specific versions of generic extension methods
Security Metadata{Entity}.SecurityMetadata.g.csSensitive 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>