Skip to main content

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);
ModeBehavior
DecryptionFailureMode.Throw (default)Any decryption failure throws an exception
DecryptionFailureMode.SkipFieldsRecoverable 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​