Configuration
Configure FluentDynamoDb for your application using FluentDynamoDbOptions.
FluentDynamoDbOptions Overview
FluentDynamoDbOptions is the central configuration object for FluentDynamoDb. It uses an immutable builder pattern where each configuration method returns a new instance with the updated settings.
// Basic usage - no optional features
var client = new AmazonDynamoDBClient();
var table = new UsersTable(client, "users");
// With configuration options
var options = new FluentDynamoDbOptions()
.WithLogger(logger);
var table = new UsersTable(client, "users", options);
Default Options
When you don't need any optional features, create a table without options:
var client = new AmazonDynamoDBClient();
var table = new UsersTable(client, "users");
Or explicitly pass default options:
var options = new FluentDynamoDbOptions();
var table = new UsersTable(client, "users", options);
Logging Configuration
Use WithLogger() to enable logging for DynamoDB operations.
With Microsoft.Extensions.Logging
Install the logging extensions package:
dotnet add package Oproto.FluentDynamoDb.Logging.Extensions
Configure logging using the ToDynamoDbLogger() extension method:
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.Logging.Extensions;
// From ILogger
var logger = loggerFactory.CreateLogger<UsersTable>();
var options = new FluentDynamoDbOptions()
.WithLogger(logger.ToDynamoDbLogger());
var table = new UsersTable(client, "users", options);
// From ILoggerFactory
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger<UsersTable>());
var table = new UsersTable(client, "users", options);
With Custom Logger
Implement IDynamoDbLogger for custom logging:
using Oproto.FluentDynamoDb.Logging;
public class MyCustomLogger : IDynamoDbLogger
{
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
public void LogTrace(int eventId, string message, params object[] args) { }
public void LogDebug(int eventId, string message, params object[] args) { }
public void LogInformation(int eventId, string message, params object[] args)
{
Console.WriteLine($"[INFO] [{eventId}] {string.Format(message, args)}");
}
public void LogWarning(int eventId, string message, params object[] args)
{
Console.WriteLine($"[WARN] [{eventId}] {string.Format(message, args)}");
}
public void LogError(int eventId, string message, params object[] args)
{
Console.WriteLine($"[ERROR] [{eventId}] {string.Format(message, args)}");
}
public void LogError(int eventId, Exception exception, string message, params object[] args)
{
Console.WriteLine($"[ERROR] [{eventId}] {string.Format(message, args)}: {exception}");
}
public void LogCritical(int eventId, Exception exception, string message, params object[] args)
{
Console.WriteLine($"[CRITICAL] [{eventId}] {string.Format(message, args)}: {exception}");
}
}
var options = new FluentDynamoDbOptions()
.WithLogger(new MyCustomLogger());
var table = new UsersTable(client, "users", options);
Optional Features
Geospatial Support
Enable geospatial features (GeoHash, S2, H3) with AddGeospatial().
dotnet add package Oproto.FluentDynamoDb.Geospatial
using Oproto.FluentDynamoDb;
var options = new FluentDynamoDbOptions()
.AddGeospatial();
var table = new LocationsTable(client, "locations", options);
Blob Storage
Enable large object storage in S3 with WithBlobStorage().
dotnet add package Oproto.FluentDynamoDb.BlobStorage.S3
using Oproto.FluentDynamoDb.BlobStorage.S3;
var s3Client = new AmazonS3Client();
var blobProvider = new S3BlobProvider(s3Client, "my-bucket");
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider);
var table = new DocumentsTable(client, "documents", options);
Encryption
Enable field-level encryption with AWS KMS using WithEncryption().
dotnet add package Oproto.FluentDynamoDb.Encryption.Kms
using Oproto.FluentDynamoDb.Encryption.Kms;
var keyResolver = new DefaultKmsKeyResolver("arn:aws:kms:us-east-1:123456789012:key/my-key");
var encryptor = new AwsEncryptionSdkFieldEncryptor(keyResolver);
var options = new FluentDynamoDbOptions()
.WithEncryption(encryptor);
var table = new SecretsTable(client, "secrets", options);
Decryption Failure Modes
By default, any decryption failure throws an exception. Use WithDecryptionFailureMode() to configure graceful degradation for scenarios like STS downscoping where a service has reduced KMS permissions:
using Oproto.FluentDynamoDb.Providers.Encryption;
// Skip encrypted fields when decryption fails (access denied, no encryptor)
var options = new FluentDynamoDbOptions()
.WithEncryption(encryptor)
.WithDecryptionFailureMode(DecryptionFailureMode.SkipFields);
var table = new SecretsTable(client, "secrets", options);
| Mode | Behavior |
|---|---|
DecryptionFailureMode.Throw (default) | Any decryption failure throws an exception |
DecryptionFailureMode.SkipFields | Recoverable failures leave the property at CLR default and log a warning; integrity failures always throw |
Write operations are unaffected — they always throw on failure regardless of this setting.
For details, see Field-Level Encryption.
Default Request Options
Configure default settings that apply to all request builders. These defaults reduce boilerplate when you want consistent behavior across operations.
Consistent Reads
Enable consistent reads by default for all Get and Query operations:
var options = new FluentDynamoDbOptions()
.UseConsistentRead(true);
var table = new UsersTable(client, "users", options);
// All Get and Query operations now use consistent reads by default
var user = await table.Users.Get(userId).GetItemAsync();
var users = await table.Users.Query()
.Where(x => x.TenantId == tenantId)
.ToListAsync();
Return Consumed Capacity
Track consumed capacity for all operations:
var options = new FluentDynamoDbOptions()
.ReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);
var table = new UsersTable(client, "users", options);
// All operations return consumed capacity
var response = await table.Users.Query()
.Where(x => x.TenantId == tenantId)
.ToDynamoDbResponseAsync();
Console.WriteLine($"Consumed: {response.ConsumedCapacity.CapacityUnits} RCUs");
Return Item Collection Metrics
Track item collection metrics for write operations (useful for tables with LSIs):
var options = new FluentDynamoDbOptions()
.ReturnItemCollectionMetrics(ReturnItemCollectionMetrics.SIZE);
var table = new UsersTable(client, "users", options);
// Write operations return item collection metrics
var response = await table.Users.Put(user).ToDynamoDbResponseAsync();
if (response.ItemCollectionMetrics != null)
{
Console.WriteLine($"Collection size: {response.ItemCollectionMetrics.SizeEstimateRangeGB}");
}
Return Values
Configure default return values for Put, Update, and Delete operations:
var options = new FluentDynamoDbOptions()
.ReturnValues(ReturnValue.ALL_NEW);
var table = new UsersTable(client, "users", options);
// Update operations return the new values by default
var response = await table.Users.Update(userId)
.Set(x => new UserUpdateModel { Name = "Jane Doe" })
.ToDynamoDbResponseAsync();
// response.Attributes contains the updated item
var updatedUser = User.FromDynamoDb<User>(response.Attributes);
Overriding Defaults
Explicit builder method calls always override default options. This allows you to set sensible defaults while still having full control over individual operations.
Override Consistent Read:
// Default is consistent read
var options = new FluentDynamoDbOptions()
.UseConsistentRead(true);
var table = new UsersTable(client, "users", options);
// This specific query uses eventually consistent read (overrides default)
var users = await table.Users.Query()
.Where(x => x.TenantId == tenantId)
.UseConsistentRead(false) // Explicit override
.ToListAsync();
// This query uses the default (consistent read)
var otherUsers = await table.Users.Query()
.Where(x => x.TenantId == otherTenantId)
.ToListAsync();
Override Return Consumed Capacity:
// Default tracks total capacity
var options = new FluentDynamoDbOptions()
.ReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL);
var table = new UsersTable(client, "users", options);
// This operation gets detailed capacity breakdown (overrides default)
var response = await table.Users.Query()
.Where(x => x.TenantId == tenantId)
.ReturnConsumedCapacity(ReturnConsumedCapacity.INDEXES) // Explicit override
.ToDynamoDbResponseAsync();
// This operation skips capacity tracking entirely
var simpleResponse = await table.Users.Get(userId)
.ReturnConsumedCapacity(ReturnConsumedCapacity.NONE) // Explicit override
.ToDynamoDbResponseAsync();
Override Return Values:
// Default returns all new values
var options = new FluentDynamoDbOptions()
.ReturnValues(ReturnValue.ALL_NEW);
var table = new UsersTable(client, "users", options);
// This update only returns the old values (overrides default)
var response = await table.Users.Update(userId)
.Set(x => new UserUpdateModel { Name = "Jane Doe" })
.ReturnValues(ReturnValue.ALL_OLD) // Explicit override
.ToDynamoDbResponseAsync();
// This delete returns nothing (overrides default)
await table.Users.Delete(userId)
.ReturnValues(ReturnValue.NONE) // Explicit override
.ExecuteAsync();
Combining Default Options
Chain multiple default options together:
var options = new FluentDynamoDbOptions()
.UseConsistentRead(true)
.ReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL)
.ReturnValues(ReturnValue.ALL_NEW);
var table = new UsersTable(client, "users", options);
Combining Configuration Options
Chain configuration methods to enable multiple features:
var options = new FluentDynamoDbOptions()
.WithLogger(loggerFactory.ToDynamoDbLogger<MyTable>())
.AddGeospatial()
.WithBlobStorage(blobProvider)
.WithEncryption(encryptor);
var table = new MyTable(client, "my-table", options);
The order of method calls doesn't matter. Each method returns a new instance with all previous settings preserved.
Disabling Logging (Zero Overhead)
By default, FluentDynamoDb uses NoOpLogger.Instance which provides near-zero overhead when logging is not needed. The NoOpLogger.IsEnabled() method always returns false, causing all logging calls to be skipped with minimal overhead.
// Default behavior - no logging, uses NoOpLogger.Instance internally
var table = new UsersTable(client, "users");
// Explicit NoOpLogger (equivalent to default)
var options = new FluentDynamoDbOptions()
.WithLogger(NoOpLogger.Instance);
var table = new UsersTable(client, "users", options);
This is the recommended approach for production environments where logging is not needed, as it provides:
- Zero allocation overhead - No log message strings are constructed
- Minimal CPU overhead -
IsEnabled()check returns immediately - No dependencies - Works without any logging framework
Test Isolation
Each table instance has its own configuration, which provides excellent test isolation:
[Fact]
public async Task Test_WithMockLogger()
{
var mockLogger = new MockDynamoDbLogger();
var options = new FluentDynamoDbOptions()
.WithLogger(mockLogger);
var table = new UsersTable(client, "test-users", options);
// Test operations...
Assert.True(mockLogger.LoggedMessages.Any());
}
[Fact]
public async Task Test_WithoutLogging()
{
// No logging configured - uses NoOpLogger by default
var table = new UsersTable(client, "test-users");
// Test operations...
}
Parallel Test Support
Because configuration is instance-based rather than static, tests can run in parallel without interference:
// These tests can run in parallel safely
[Fact]
public async Task Test1()
{
var options = new FluentDynamoDbOptions()
.WithLogger(logger1);
var table = new UsersTable(client, "table1", options);
// ...
}
[Fact]
public async Task Test2()
{
var options = new FluentDynamoDbOptions()
.WithLogger(logger2);
var table = new UsersTable(client, "table2", options);
// ...
}
Next Steps
- Core Concepts - Understand entities, tables, and operations
- Querying - Learn advanced query patterns
- Source Generation - How the source generator works