DynamoDB Dynamic Fields in C#
Work with unmapped DynamoDB attributes using FluentDynamoDB's dynamic fields feature. Dynamic fields allow entities to capture attributes not explicitly defined as properties, making them ideal for multi-tenant applications where different tenants need different custom fields without modifying the entity schema.
Common use cases:
- Multi-tenant applications with tenant-specific custom attributes
- Flexible schemas where attributes vary by item type
- Capturing attributes from external systems or migrations
- A/B testing with feature-specific metadata
Entity Definition
Enable dynamic fields by adding the [EnableDynamicFields] attribute:
[DynamoDbTable("products")]
[EnableDynamicFields]
public partial class Product
{
[PartitionKey(Prefix = "TENANT")]
[DynamoDbAttribute("pk")]
public string TenantId { get; set; } = string.Empty;
[SortKey(Prefix = "PRODUCT")]
[DynamoDbAttribute("sk")]
public string ProductId { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
[DynamoDbAttribute("price")]
public decimal Price { get; set; }
// Source generator automatically adds:
// public DynamicFieldCollection DynamicFields { get; set; } = new();
}
- Lambda/Fluent
- String Formatted
- Manual Builder
Create with Dynamic Fields
var product = new Product
{
TenantId = "tenant-123",
ProductId = "prod-456",
Name = "T-Shirt",
Price = 29.99m
};
// Add tenant-specific custom fields
product.DynamicFields.SetString("color", "Blue");
product.DynamicFields.SetInt("size_us", 10);
product.DynamicFields.SetBool("in_stock", true);
product.DynamicFields.SetDateTime("last_restocked", DateTime.UtcNow);
product.DynamicFields.SetStringList("tags", new List<string> { "sale", "featured" });
await table.Products.PutAsync(product);
Read Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Typed getters (return null if field doesn't exist)
var color = product.DynamicFields.GetString("color");
var size = product.DynamicFields.GetInt("size_us");
var inStock = product.DynamicFields.GetBool("in_stock");
var restocked = product.DynamicFields.GetDateTime("last_restocked");
var tags = product.DynamicFields.GetStringList("tags");
// TryGet pattern for safe access
if (product.DynamicFields.TryGetDecimal("sale_price", out var salePrice))
{
Console.WriteLine($"On sale for: {salePrice:C}");
}
Update Dynamic Fields
// Load entity and modify dynamic fields
var product = await table.Products.GetAsync(pk, sk);
product.DynamicFields.SetDecimal("sale_price", 24.99m);
product.DynamicFields.SetDateTime("sale_ends", DateTime.UtcNow.AddDays(7));
product.DynamicFields.Remove("temporary_note");
// Update with only the changed fields (efficient)
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
// Or create changes without loading entity
var changes = new DynamicFieldCollection();
changes.SetDecimal("sale_price", 19.99m);
changes.Remove("old_field");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel { DynamicFields = changes })
.UpdateAsync();
Query with Dynamic Field Filters
// Filter by dynamic field value
var blueProducts = await table.Products.Query()
.Where(x => x.TenantId == tenantPk)
.WithFilter(x => x.DynamicFields["color"] == "Blue")
.ToListAsync();
// Numeric comparisons
var heavyProducts = await table.Products.Scan()
.WithFilter(x => x.DynamicFields["weight_grams"] > 500)
.ToListAsync();
// Check field existence
var productsWithWarranty = await table.Products.Scan()
.WithFilter(x => x.DynamicFields.Exists("warranty_months"))
.ToListAsync();
// Combine with regular filters
var results = await table.Products.Query()
.Where(x => x.TenantId == tenantPk)
.WithFilter(x => x.Price < 100 && x.DynamicFields["color"] == "Blue")
.ToListAsync();
Delete Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Remove specific fields
product.DynamicFields.Remove("temporary_note");
product.DynamicFields.Remove("deprecated_field");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
Create with Dynamic Fields
var product = new Product
{
TenantId = "tenant-123",
ProductId = "prod-456",
Name = "T-Shirt",
Price = 29.99m
};
// Add tenant-specific custom fields
product.DynamicFields.SetString("color", "Blue");
product.DynamicFields.SetInt("size_us", 10);
product.DynamicFields.SetBool("in_stock", true);
product.DynamicFields.SetDateTime("last_restocked", DateTime.UtcNow);
product.DynamicFields.SetStringList("tags", new List<string> { "sale", "featured" });
await table.Products.PutAsync(product);
Read Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Typed getters (return null if field doesn't exist)
var color = product.DynamicFields.GetString("color");
var size = product.DynamicFields.GetInt("size_us");
var inStock = product.DynamicFields.GetBool("in_stock");
var restocked = product.DynamicFields.GetDateTime("last_restocked");
var tags = product.DynamicFields.GetStringList("tags");
// TryGet pattern for safe access
if (product.DynamicFields.TryGetDecimal("sale_price", out var salePrice))
{
Console.WriteLine($"On sale for: {salePrice:C}");
}
Update Dynamic Fields
// Using DynamicFieldCollection (preferred)
var product = await table.Products.GetAsync(pk, sk);
product.DynamicFields.SetDecimal("sale_price", 24.99m);
product.DynamicFields.SetDateTime("sale_ends", DateTime.UtcNow.AddDays(7));
product.DynamicFields.Remove("temporary_note");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
// Or using string expressions for explicit control
await table.Products.Update(pk, sk)
.Set($"SET {Product.Fields.DynamicFields}.sale_price = {{0}}", 24.99m)
.UpdateAsync();
Query with Dynamic Field Filters
// Filter by dynamic field value using string format
var blueProducts = await table.Products.Query()
.Where($"{Product.Fields.TenantId} = {{0}}", tenantPk)
.WithFilter($"{Product.Fields.DynamicFields}.color = {{0}}", "Blue")
.ToListAsync();
// Numeric comparisons
var heavyProducts = await table.Products.Scan()
.WithFilter($"{Product.Fields.DynamicFields}.weight_grams > {{0}}", 500)
.ToListAsync();
// Check field existence
var productsWithWarranty = await table.Products.Scan()
.WithFilter($"attribute_exists({Product.Fields.DynamicFields}.warranty_months)")
.ToListAsync();
// Combine with regular filters
var results = await table.Products.Query()
.Where($"{Product.Fields.TenantId} = {{0}}", tenantPk)
.WithFilter($"{Product.Fields.Price} < {{0}} AND {Product.Fields.DynamicFields}.color = {{1}}", 100, "Blue")
.ToListAsync();
Delete Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Remove specific fields
product.DynamicFields.Remove("temporary_note");
product.DynamicFields.Remove("deprecated_field");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
// Or using string expression
await table.Products.Update(pk, sk)
.Remove($"{Product.Fields.DynamicFields}.temporary_note")
.UpdateAsync();
Create with Dynamic Fields
var product = new Product
{
TenantId = "tenant-123",
ProductId = "prod-456",
Name = "T-Shirt",
Price = 29.99m
};
// Add tenant-specific custom fields
product.DynamicFields.SetString("color", "Blue");
product.DynamicFields.SetInt("size_us", 10);
product.DynamicFields.SetBool("in_stock", true);
product.DynamicFields.SetDateTime("last_restocked", DateTime.UtcNow);
product.DynamicFields.SetStringList("tags", new List<string> { "sale", "featured" });
await table.Products.PutAsync(product);
Read Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Typed getters (return null if field doesn't exist)
var color = product.DynamicFields.GetString("color");
var size = product.DynamicFields.GetInt("size_us");
var inStock = product.DynamicFields.GetBool("in_stock");
var restocked = product.DynamicFields.GetDateTime("last_restocked");
var tags = product.DynamicFields.GetStringList("tags");
// TryGet pattern for safe access
if (product.DynamicFields.TryGetDecimal("sale_price", out var salePrice))
{
Console.WriteLine($"On sale for: {salePrice:C}");
}
Update Dynamic Fields
// Using DynamicFieldCollection (preferred)
var product = await table.Products.GetAsync(pk, sk);
product.DynamicFields.SetDecimal("sale_price", 24.99m);
product.DynamicFields.SetDateTime("sale_ends", DateTime.UtcNow.AddDays(7));
product.DynamicFields.Remove("temporary_note");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
// Or using manual expressions for explicit control
await table.Products.Update(pk, sk)
.Set("SET #salePrice = :salePrice, #saleEnds = :saleEnds")
.WithAttribute("#salePrice", "sale_price")
.WithAttribute("#saleEnds", "sale_ends")
.WithValue(":salePrice", new AttributeValue { N = "24.99" })
.WithValue(":saleEnds", new AttributeValue { S = DateTime.UtcNow.AddDays(7).ToString("O") })
.UpdateAsync();
Query with Dynamic Field Filters
// Filter by dynamic field value using manual builder
var blueProducts = await table.Products.Query()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", tenantPk)
.WithFilter("#color = :color")
.WithAttribute("#color", "color")
.WithValue(":color", "Blue")
.ToListAsync();
// Numeric comparisons
var heavyProducts = await table.Products.Scan()
.WithFilter("#weight > :minWeight")
.WithAttribute("#weight", "weight_grams")
.WithValue(":minWeight", 500)
.ToListAsync();
// Check field existence
var productsWithWarranty = await table.Products.Scan()
.WithFilter("attribute_exists(#warranty)")
.WithAttribute("#warranty", "warranty_months")
.ToListAsync();
// Combine with regular filters
var results = await table.Products.Query()
.Where("#pk = :pk")
.WithAttribute("#pk", "pk")
.WithValue(":pk", tenantPk)
.WithFilter("#price < :maxPrice AND #color = :color")
.WithAttribute("#price", "price")
.WithAttribute("#color", "color")
.WithValue(":maxPrice", 100)
.WithValue(":color", "Blue")
.ToListAsync();
Delete Dynamic Fields
var product = await table.Products.GetAsync(pk, sk);
// Remove specific fields
product.DynamicFields.Remove("temporary_note");
product.DynamicFields.Remove("deprecated_field");
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
// Or using manual expression
await table.Products.Update(pk, sk)
.Remove("#tempNote, #deprecated")
.WithAttribute("#tempNote", "temporary_note")
.WithAttribute("#deprecated", "deprecated_field")
.UpdateAsync();
Supported Field Types
| Type | Getter | Setter |
|---|---|---|
| String | GetString() | SetString() |
| Int/Long | GetInt(), GetLong() | SetInt(), SetLong() |
| Decimal/Double | GetDecimal(), GetDouble() | SetDecimal(), SetDouble() |
| Boolean | GetBool() | SetBool() |
| DateTime | GetDateTime(), GetDateTimeOffset() | SetDateTime(), SetDateTimeOffset() |
| Lists | GetStringList(), GetIntList() | SetStringList(), SetIntList() |
| Sets | GetStringSet(), GetNumberSet() | SetStringSet(), SetNumberSet() |
| Binary | GetBytes() | SetBytes() |
Change Tracking
The DynamicFieldCollection automatically tracks changes when an entity is loaded from DynamoDB:
var product = await table.Products.GetAsync(pk, sk);
// Modify fields - changes are tracked
product.DynamicFields.SetString("color", "Red");
product.DynamicFields.Remove("old_field");
// Check if there are changes
if (product.DynamicFields.HasChanges)
{
// Update with only changed fields
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel
{
DynamicFields = product.DynamicFields.ChangesOnly()
})
.UpdateAsync();
}
Learn More
- Dynamic Fields Guide - Complete dynamic fields reference with change tracking, type detection, and security considerations
- Entity Definition - Define entities with attributes and keys
- Basic Operations - Single-item CRUD operations
- Query & Scan - Querying and filtering data