DynamicTable
Schema-less access to any DynamoDB table without requiring entity class definitions.
Overview
While typed entities provide compile-time safety and IntelliSense, there are scenarios where you need to work with DynamoDB tables without predefined schemas:
- Schema Exploration: Inspecting tables with unknown or evolving schemas
- Migration Tools: Moving data between tables with different structures
- Admin Utilities: Building tools that work with any table
- Truly Schema-less Data: Tables where items have varying attributes
DynamicTable and DynamicEntity provide this flexibility while maintaining the fluent API patterns you're familiar with.
Basic Usage
Creating a DynamicTable
using Oproto.FluentDynamoDb.Storage;
using Oproto.FluentDynamoDb.Entities;
// Basic DynamicTable - requires AttributeValue keys for all operations
var table = new DynamicTable(client, "my-table");
// With key configuration - enables typed key methods
var keyOptions = new DynamicTableKeyOptions
{
PartitionKeyName = "pk",
PartitionKeyType = ScalarAttributeType.S,
SortKeyName = "sk",
SortKeyType = ScalarAttributeType.S
};
var table = new DynamicTable(client, "my-table", keyOptions);
// With FluentDynamoDbOptions
var options = new FluentDynamoDbOptions()
.WithLogger(logger);
var table = new DynamicTable(client, "my-table", keyOptions, options);
Reading Items
// With typed keys (requires key configuration)
var item = await table.GetAsync("USER#123", "PROFILE");
// Access attributes via DynamicFields
if (item != null)
{
var name = item.DynamicFields.GetString("name");
var age = item.DynamicFields.GetInt("age");
var isActive = item.DynamicFields.GetBool("is_active");
var balance = item.DynamicFields.GetDecimal("balance");
}
// With AttributeValue keys (always available)
var pk = new AttributeValue { S = "USER#123" };
var sk = new AttributeValue { S = "PROFILE" };
var item = await table.GetAsync(pk, sk);
Writing Items
// Create a DynamicEntity
var entity = new DynamicEntity();
entity.DynamicFields.SetString("pk", "USER#456");
entity.DynamicFields.SetString("sk", "PROFILE");
entity.DynamicFields.SetString("name", "Jane Doe");
entity.DynamicFields.SetInt("age", 30);
entity.DynamicFields.SetBool("is_active", true);
entity.DynamicFields.SetDecimal("balance", 1234.56m);
// Put the item
await table.PutAsync(entity);
Querying
// Query with lambda expressions
var items = await table.Query()
.Where(x => x.DynamicFields["pk"] == "USER#123")
.ToListAsync();
// Query with sort key condition
var items = await table.Query()
.Where(x => x.DynamicFields["pk"] == "TENANT#A"
&& x.DynamicFields["sk"].BeginsWith("ORDER#"))
.ToListAsync();
// Query with filter
var items = await table.Query()
.Where(x => x.DynamicFields["pk"] == "TENANT#A")
.WithFilter(x => x.DynamicFields["status"] == "active")
.ToListAsync();
Scanning
// Scan all items
var allItems = await table.Scan().ToListAsync();
// Scan with filter
var activeItems = await table.Scan()
.WithFilter(x => x.DynamicFields["status"] == "active")
.ToListAsync();
Updating Items
// Update with typed keys
await table.Update("USER#123", "PROFILE")
.Set("name", new AttributeValue { S = "Jane Smith" })
.Set("updated_at", new AttributeValue { S = DateTime.UtcNow.ToString("O") })
.UpdateAsync();
// Update with AttributeValue keys
var pk = new AttributeValue { S = "USER#123" };
var sk = new AttributeValue { S = "PROFILE" };
await table.Update(pk, sk)
.Set("name", new AttributeValue { S = "Jane Smith" })
.UpdateAsync();
Deleting Items
// Delete with typed keys
await table.DeleteAsync("USER#123", "PROFILE");
// Delete with AttributeValue keys
var pk = new AttributeValue { S = "USER#123" };
var sk = new AttributeValue { S = "PROFILE" };
await table.DeleteAsync(pk, sk);
Key Configuration
DynamicTableKeyOptions
Configure key schema to enable typed key methods:
public class DynamicTableKeyOptions
{
// Partition key configuration
public string PartitionKeyName { get; set; } = "pk";
public ScalarAttributeType PartitionKeyType { get; set; } = ScalarAttributeType.S;
// Sort key configuration (optional)
public string? SortKeyName { get; set; }
public ScalarAttributeType? SortKeyType { get; set; }
}
String Keys (Most Common)
var keyOptions = new DynamicTableKeyOptions
{
PartitionKeyName = "pk",
PartitionKeyType = ScalarAttributeType.S,
SortKeyName = "sk",
SortKeyType = ScalarAttributeType.S
};
var table = new DynamicTable(client, "my-table", keyOptions);
// Now you can use string key methods
var item = await table.GetAsync("partition-value", "sort-value");
await table.DeleteAsync("partition-value", "sort-value");
Numeric Keys
var keyOptions = new DynamicTableKeyOptions
{
PartitionKeyName = "id",
PartitionKeyType = ScalarAttributeType.N,
SortKeyName = "version",
SortKeyType = ScalarAttributeType.N
};
var table = new DynamicTable(client, "my-table", keyOptions);
// Use numeric key methods
var item = await table.GetAsync(12345L, 1L);
await table.DeleteAsync(12345L, 1L);
Partition Key Only (No Sort Key)
var keyOptions = new DynamicTableKeyOptions
{
PartitionKeyName = "id",
PartitionKeyType = ScalarAttributeType.S
// SortKeyName and SortKeyType left null
};
var table = new DynamicTable(client, "my-table", keyOptions);
// Use single-key methods
var item = await table.GetAsync("item-123");
await table.DeleteAsync("item-123");
DynamicEntity
Working with DynamicFields
DynamicEntity stores all attributes in a DynamicFieldCollection:
var entity = new DynamicEntity();
// Typed setters
entity.DynamicFields.SetString("name", "John Doe");
entity.DynamicFields.SetInt("age", 25);
entity.DynamicFields.SetLong("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
entity.DynamicFields.SetDouble("score", 98.5);
entity.DynamicFields.SetDecimal("price", 19.99m);
entity.DynamicFields.SetBool("active", true);
entity.DynamicFields.SetDateTime("created", DateTime.UtcNow);
entity.DynamicFields.SetBytes("data", new byte[] { 1, 2, 3 });
// Typed getters
var name = entity.DynamicFields.GetString("name");
var age = entity.DynamicFields.GetInt("age");
var timestamp = entity.DynamicFields.GetLong("timestamp");
var score = entity.DynamicFields.GetDouble("score");
var price = entity.DynamicFields.GetDecimal("price");
var active = entity.DynamicFields.GetBool("active");
var created = entity.DynamicFields.GetDateTime("created");
var data = entity.DynamicFields.GetBytes("data");
// Check if field exists
if (entity.DynamicFields.ContainsKey("optional_field"))
{
var value = entity.DynamicFields.GetString("optional_field");
}
// Get field type
var fieldType = entity.DynamicFields.GetFieldType("name"); // DynamicFieldType.String
Expression Support
DynamicEntity supports lambda expressions in queries and filters:
// Equality
.Where(x => x.DynamicFields["pk"] == "value")
// Comparison operators
.WithFilter(x => x.DynamicFields["age"] > 18)
.WithFilter(x => x.DynamicFields["score"] >= 90)
.WithFilter(x => x.DynamicFields["price"] < 100)
.WithFilter(x => x.DynamicFields["count"] <= 10)
.WithFilter(x => x.DynamicFields["status"] != "deleted")
// String operations
.Where(x => x.DynamicFields["sk"].BeginsWith("ORDER#"))
.WithFilter(x => x.DynamicFields["name"].Contains("John"))
// Existence checks
.WithFilter(x => x.DynamicFields.Exists("optional_field"))
.WithFilter(x => x.DynamicFields.NotExists("deleted_at"))
Use Cases
Schema Exploration
// Explore an unknown table
var table = new DynamicTable(client, "unknown-table");
// Scan a sample of items
var sample = await table.Scan()
.Take(10)
.ToListAsync();
// Discover the schema
foreach (var item in sample)
{
Console.WriteLine("Item attributes:");
foreach (var key in item.DynamicFields.Keys)
{
var type = item.DynamicFields.GetFieldType(key);
Console.WriteLine($" {key}: {type}");
}
}
Data Migration
// Copy data between tables with transformation
var sourceTable = new DynamicTable(sourceClient, "old-table");
var targetTable = new DynamicTable(targetClient, "new-table", targetKeyOptions);
await foreach (var item in sourceTable.Scan().ToAsyncEnumerable())
{
// Transform the item
var newItem = new DynamicEntity();
newItem.DynamicFields.SetString("pk", item.DynamicFields.GetString("old_pk"));
newItem.DynamicFields.SetString("sk", item.DynamicFields.GetString("old_sk"));
// Copy other fields
foreach (var key in item.DynamicFields.Keys)
{
if (key != "old_pk" && key != "old_sk")
{
newItem.DynamicFields[key] = item.DynamicFields[key];
}
}
await targetTable.PutAsync(newItem);
}
Admin Utilities
// Generic table browser
public async Task<List<DynamicEntity>> BrowseTable(
IAmazonDynamoDB client,
string tableName,
string? filterExpression = null)
{
var table = new DynamicTable(client, tableName);
var scan = table.Scan();
return await scan.Take(100).ToListAsync();
}
Comparison: DynamicTable vs Typed Entities
| Feature | Typed Entities | DynamicTable |
|---|---|---|
| Compile-time safety | ✅ Yes | ❌ No |
| IntelliSense for fields | ✅ Yes | ❌ No |
| Schema definition required | ✅ Yes | ❌ No |
| Works with unknown schemas | ❌ No | ✅ Yes |
| Performance | ✅ Optimal | ✅ Optimal |
| Lambda expression support | ✅ Full | ✅ Via DynamicFields indexer |
| Key validation | ✅ Automatic | ⚠️ Manual (via KeyOptions) |
When to Use DynamicTable
- Exploring tables with unknown schemas
- Building migration or admin tools
- Working with truly schema-less data
- Prototyping before defining entity classes
When to Use Typed Entities
- Production application code
- When schema is known and stable
- When compile-time safety is important
- When IntelliSense improves developer productivity
Error Handling
Key Configuration Errors
// Using typed key methods without key configuration throws
var table = new DynamicTable(client, "my-table"); // No key options
try
{
await table.GetAsync("pk-value"); // Throws InvalidOperationException
}
catch (InvalidOperationException ex)
{
// "Key options must be configured to use typed key methods."
}
Sort Key Errors
// Providing sort key when not configured throws
var keyOptions = new DynamicTableKeyOptions
{
PartitionKeyName = "pk",
PartitionKeyType = ScalarAttributeType.S
// No sort key configured
};
var table = new DynamicTable(client, "my-table", keyOptions);
try
{
await table.GetAsync("pk-value", "sk-value"); // Throws InvalidOperationException
}
catch (InvalidOperationException ex)
{
// "Sort key was provided but DynamicTableKeyOptions does not define a sort key."
}
Type Mismatch Errors
// Getting wrong type throws DynamicFieldTypeException
var entity = new DynamicEntity();
entity.DynamicFields.SetString("name", "John");
try
{
var age = entity.DynamicFields.GetInt("name"); // Throws DynamicFieldTypeException
}
catch (DynamicFieldTypeException ex)
{
// "Field 'name' is of type String, not Number."
}
See Also
- Dynamic Fields - Using dynamic fields with typed entities
- PartiQL - SQL-like queries with DynamicEntity