Skip to main content

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();
}

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();

Supported Field Types

TypeGetterSetter
StringGetString()SetString()
Int/LongGetInt(), GetLong()SetInt(), SetLong()
Decimal/DoubleGetDecimal(), GetDouble()SetDecimal(), SetDouble()
BooleanGetBool()SetBool()
DateTimeGetDateTime(), GetDateTimeOffset()SetDateTime(), SetDateTimeOffset()
ListsGetStringList(), GetIntList()SetStringList(), SetIntList()
SetsGetStringSet(), GetNumberSet()SetStringSet(), SetNumberSet()
BinaryGetBytes()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