The Oproto.FluentDynamoDb advanced type system extends the source generator to support DynamoDB's native collection types (Maps, Sets, Lists), time-to-live (TTL) fields, JSON blob serialization, and external blob storage. All features maintain AOT compatibility and the library's zero-reflection design philosophy.
- Native Collection Types
- Time-To-Live (TTL) Fields
- JSON Blob Serialization
- External Blob Storage
- Empty Collection Handling
- Format String Support
- AOT Compatibility
- Migration Guide
DynamoDB Maps (M) allow you to store nested key-value structures. The library supports three types of map properties:
Store simple string-to-string mappings:
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("pk")]
public string Id { get; set; }
[DynamoDbAttribute("metadata")]
public Dictionary<string, string> Metadata { get; set; }
}
// Usage
var product = new Product
{
Id = "prod-123",
Metadata = new Dictionary<string, string>
{
["color"] = "blue",
["size"] = "large",
["material"] = "cotton"
}
};
await table.Products.PutAsync(product);For more complex mappings with mixed types:
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("attributes")]
public Dictionary<string, AttributeValue> Attributes { get; set; }
}
// Usage
var product = new Product
{
Id = "prod-123",
Attributes = new Dictionary<string, AttributeValue>
{
["price"] = new AttributeValue { N = "29.99" },
["inStock"] = new AttributeValue { BOOL = true },
["tags"] = new AttributeValue { SS = new List<string> { "new", "sale" } }
}
};Store complex nested objects as maps:
// Define the nested type - MUST be marked with [DynamoDbEntity]
[DynamoDbEntity]
public partial class ProductAttributes
{
[DynamoDbAttribute("color")]
public string Color { get; set; }
[DynamoDbAttribute("size")]
public int? Size { get; set; }
[DynamoDbAttribute("dimensions")]
public Dictionary<string, decimal> Dimensions { get; set; }
}
// Use in parent entity
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("pk")]
public string Id { get; set; }
[DynamoDbAttribute("attributes")]
[DynamoDbMap]
public ProductAttributes Attributes { get; set; }
}
// Usage
var product = new Product
{
Id = "prod-123",
Attributes = new ProductAttributes
{
Color = "blue",
Size = 42,
Dimensions = new Dictionary<string, decimal>
{
["length"] = 10.5m,
["width"] = 8.0m,
["height"] = 3.2m
}
}
};Important: When using [DynamoDbMap] on a custom type:
- The nested type MUST be marked with
[DynamoDbEntity]to generate mapping code - This ensures AOT compatibility by using compile-time generated methods instead of reflection
- Nested types can themselves contain maps, creating deep hierarchies
DynamoDB Sets ensure uniqueness and support efficient set operations. The library supports three set types:
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("tags")]
public HashSet<string> Tags { get; set; }
}
// Usage
var product = new Product
{
Id = "prod-123",
Tags = new HashSet<string> { "electronics", "sale", "featured" }
};
// Query with set operations
await table.Query<Product>()
.Where("contains(tags, {0})", "sale")
.ToListAsync();[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("category_ids")]
public HashSet<int> CategoryIds { get; set; }
[DynamoDbAttribute("prices")]
public HashSet<decimal> Prices { get; set; }
}
// Usage
var product = new Product
{
Id = "prod-123",
CategoryIds = new HashSet<int> { 1, 5, 12 },
Prices = new HashSet<decimal> { 19.99m, 24.99m, 29.99m }
};[DynamoDbTable("files")]
public partial class FileMetadata
{
[DynamoDbAttribute("checksums")]
public HashSet<byte[]> Checksums { get; set; }
}
// Usage
var file = new FileMetadata
{
Id = "file-123",
Checksums = new HashSet<byte[]>
{
SHA256.HashData(data1),
SHA256.HashData(data2)
}
};DynamoDB Lists (L) maintain element order and support heterogeneous types:
[DynamoDbTable("orders")]
public partial class Order
{
[DynamoDbAttribute("item_ids")]
public List<string> ItemIds { get; set; }
[DynamoDbAttribute("prices")]
public List<decimal> Prices { get; set; }
[DynamoDbAttribute("quantities")]
public List<int> Quantities { get; set; }
}
// Usage
var order = new Order
{
Id = "order-123",
ItemIds = new List<string> { "item-1", "item-2", "item-3" },
Prices = new List<decimal> { 19.99m, 24.99m, 9.99m },
Quantities = new List<int> { 2, 1, 3 }
};
// Lists maintain order
var loaded = await table.Get<Order>()
.WithKey("pk", "order-123")
.GetItemAsync();
// ItemIds[0] is guaranteed to be "item-1"The library supports both DateTime and DateTimeOffset types for timestamp properties.
Standard DateTime properties are serialized to ISO 8601 format:
[DynamoDbTable("events")]
public partial class Event
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string EventId { get; set; } = string.Empty;
[DynamoDbAttribute("created_at")]
public DateTime CreatedAt { get; set; }
[DynamoDbAttribute("scheduled_at")]
public DateTime? ScheduledAt { get; set; }
}
// Usage
var event = new Event
{
EventId = "evt-123",
CreatedAt = DateTime.UtcNow,
ScheduledAt = DateTime.UtcNow.AddDays(7)
};
await table.Events.PutAsync(event);
// CreatedAt stored as: "2024-03-15T10:30:00.0000000Z"DateTimeOffset preserves timezone offset information:
[DynamoDbTable("appointments")]
public partial class Appointment
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string AppointmentId { get; set; } = string.Empty;
// DateTimeOffset preserves timezone offset
[DynamoDbAttribute("scheduled_time")]
public DateTimeOffset ScheduledTime { get; set; }
[DynamoDbAttribute("reminder_time")]
public DateTimeOffset? ReminderTime { get; set; }
}
// Usage
var appointment = new Appointment
{
AppointmentId = "apt-123",
ScheduledTime = new DateTimeOffset(2024, 3, 15, 14, 30, 0, TimeSpan.FromHours(-5)),
ReminderTime = DateTimeOffset.Now.AddHours(-1)
};
await table.Appointments.PutAsync(appointment);
// ScheduledTime stored as: "2024-03-15T14:30:00.0000000-05:00"When to Use DateTimeOffset:
- When timezone information must be preserved
- For user-facing times that should display in the original timezone
- When working with events across multiple timezones
- For audit trails where the original timezone context matters
DateTimeOffset Round-Trip:
// Original value with specific offset
var original = new DateTimeOffset(2024, 3, 15, 14, 30, 0, TimeSpan.FromHours(-5));
appointment.ScheduledTime = original;
await table.Appointments.PutAsync(appointment);
var loaded = await table.Appointments.GetAsync("apt-123");
// Round-trip preserves the value (within millisecond precision)
Assert.Equal(original, loaded.ScheduledTime);TTL fields enable automatic item expiration in DynamoDB. Mark a DateTime or DateTimeOffset property with [TimeToLive]:
[DynamoDbTable("sessions")]
public partial class Session
{
[PartitionKey]
[DynamoDbAttribute("session_id")]
public string SessionId { get; set; } = string.Empty;
[DynamoDbAttribute("ttl")]
[TimeToLive]
public DateTime? ExpiresAt { get; set; }
}
// Usage - Set expiration 7 days from now
var session = new Session
{
SessionId = "sess-123",
ExpiresAt = DateTime.UtcNow.AddDays(7)
};
await table.Sessions.PutAsync(session);DateTimeOffset is also supported for TTL fields:
[DynamoDbTable("tokens")]
public partial class AuthToken
{
[PartitionKey]
[DynamoDbAttribute("token_id")]
public string TokenId { get; set; } = string.Empty;
[DynamoDbAttribute("issued_at")]
public DateTimeOffset IssuedAt { get; set; }
// DateTimeOffset TTL - converted to Unix epoch seconds
[DynamoDbAttribute("ttl")]
[TimeToLive]
public DateTimeOffset? ExpiresAt { get; set; }
}
// Usage
var token = new AuthToken
{
TokenId = "tok-123",
IssuedAt = DateTimeOffset.UtcNow,
ExpiresAt = DateTimeOffset.UtcNow.AddHours(24)
};
await table.Tokens.PutAsync(token);
// ExpiresAt stored as Unix epoch seconds: 1710590400TTL Conversion:
- DateTimeOffset values are converted to Unix epoch seconds using
ToUnixTimeSeconds() - When reading, the value is reconstructed using
DateTimeOffset.FromUnixTimeSeconds() - Precision is limited to seconds (milliseconds are truncated)
Important Notes:
- Only ONE TTL field is allowed per entity
- TTL values are stored as Unix epoch seconds (number of seconds since January 1, 1970 UTC)
- DynamoDB typically deletes expired items within 48 hours
- You must enable TTL on the table in AWS Console or via API
- Use UTC times to avoid timezone issues
- DateTimeOffset TTL values must be after Unix epoch (1970-01-01)
# AWS CLI
aws dynamodb update-time-to-live \
--table-name sessions \
--time-to-live-specification "Enabled=true, AttributeName=ttl"Store complex objects as JSON strings in DynamoDB attributes using [JsonBlob]. JSON serialization is configured at runtime via FluentDynamoDbOptions, giving you full control over serialization settings.
JSON serialization requires installing a serializer package and configuring it via FluentDynamoDbOptions:
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.SystemTextJson; // or NewtonsoftJson
// Configure options with your preferred JSON serializer
var options = new FluentDynamoDbOptions()
.WithSystemTextJson(); // or .WithNewtonsoftJson()
// Create your table with the configured options
var table = new DocumentTable(dynamoDbClient, "documents", options);// 1. Install package
// dotnet add package Oproto.FluentDynamoDb.SystemTextJson
// 2. Define entity with [JsonBlob] property
[DynamoDbTable("documents")]
public partial class Document
{
[PartitionKey]
[DynamoDbAttribute("doc_id")]
public string DocumentId { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = "DOC";
[DynamoDbAttribute("content")]
[JsonBlob]
public DocumentContent Content { get; set; } = new();
}
public class DocumentContent
{
public string Title { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public Dictionary<string, string> Metadata { get; set; } = new();
public List<string> Tags { get; set; } = new();
}
// 3. Configure FluentDynamoDbOptions with System.Text.Json
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.SystemTextJson;
var options = new FluentDynamoDbOptions()
.WithSystemTextJson();
var table = new DocumentTable(dynamoDbClient, "documents", options);
// 4. Use the entity
var document = new Document
{
DocumentId = "doc-123",
Content = new DocumentContent
{
Title = "My Document",
Body = "Document content here...",
Metadata = new Dictionary<string, string>
{
["author"] = "John Doe",
["version"] = "1.0"
},
Tags = new List<string> { "important", "draft" }
}
};
await table.Documents.Put(document).PutAsync();Customize serialization behavior by passing JsonSerializerOptions:
using System.Text.Json;
using System.Text.Json.Serialization;
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
var options = new FluentDynamoDbOptions()
.WithSystemTextJson(jsonOptions);For Native AOT and trimmed applications, use a source-generated JsonSerializerContext:
using System.Text.Json.Serialization;
// 1. Define a JsonSerializerContext for your types
[JsonSerializable(typeof(DocumentContent))]
internal partial class DocumentJsonContext : JsonSerializerContext { }
// 2. Configure FluentDynamoDbOptions with the context
var options = new FluentDynamoDbOptions()
.WithSystemTextJson(DocumentJsonContext.Default);
var table = new DocumentTable(dynamoDbClient, "documents", options);// 1. Install package
// dotnet add package Oproto.FluentDynamoDb.NewtonsoftJson
// 2. Configure FluentDynamoDbOptions with Newtonsoft.Json
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.NewtonsoftJson;
var options = new FluentDynamoDbOptions()
.WithNewtonsoftJson();
var table = new DocumentTable(dynamoDbClient, "documents", options);
// 3. Use same entity definition as aboveCustomize serialization behavior by passing JsonSerializerSettings:
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Include,
Formatting = Formatting.None
};
var options = new FluentDynamoDbOptions()
.WithNewtonsoftJson(settings);The default Newtonsoft.Json settings include:
TypeNameHandling.None- No type metadata (security best practice)NullValueHandling.Ignore- Omit null values to reduce storageDateFormatHandling.IsoDateFormat- ISO 8601 dates for consistencyReferenceLoopHandling.Ignore- Handle circular references gracefully
Note: Newtonsoft.Json uses runtime reflection and has limited AOT support. Use System.Text.Json for full AOT compatibility.
If you use [JsonBlob] properties without configuring a JSON serializer, you'll get a clear runtime exception:
InvalidOperationException: Property 'Content' has [JsonBlob] attribute but no JSON serializer is configured.
Call .WithSystemTextJson() or .WithNewtonsoftJson() on FluentDynamoDbOptions.
Store large data externally (e.g., S3) with only a reference in DynamoDB using [BlobStorage] and BlobData<T>:
Note: The
[BlobReference]attribute is deprecated. Use[BlobStorage]withBlobData<T>wrapper type instead.
The BlobData<T> wrapper encapsulates blob storage behavior:
| Property/Method | Description |
|---|---|
Value |
Gets the loaded data. Throws InvalidOperationException if not loaded. |
ReferenceKey |
Gets the storage key, or null if not yet stored. |
IsLoaded |
Returns true when data has been loaded from storage. |
HasPendingData |
Returns true when instance has new data to be stored. |
Create(T value) |
Static factory to create instance with data to be stored. |
LoadAsync() |
Loads data from storage. Idempotent - returns immediately if already loaded. |
// 1. Install package
// dotnet add package Oproto.FluentDynamoDb.BlobStorage.S3
// 2. Define entity with [BlobStorage] and BlobData<T>
using Oproto.FluentDynamoDb.Attributes;
using Oproto.FluentDynamoDb.Providers.BlobStorage;
[DynamoDbTable("files")]
public partial class FileMetadata
{
[PartitionKey]
[DynamoDbAttribute("file_id")]
public string FileId { get; set; } = string.Empty;
// Eager loading (default) - data loaded during deserialization
[BlobStorage]
[DynamoDbAttribute("data")]
public BlobData<byte[]> Data { get; set; } = default!;
// Lazy loading - data loaded only when LoadAsync() is called
[BlobStorage(LazyLoad = true)]
[DynamoDbAttribute("thumbnail")]
public BlobData<byte[]>? Thumbnail { get; set; }
}
// 3. Configure blob storage with strategy
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.BlobStorage.S3;
var s3Client = new AmazonS3Client();
var blobProvider = new S3BlobProvider(s3Client, "my-files-bucket", "uploads");
// Default configuration uses BestEffortCleanupStrategy
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider);
var table = new FileTable(dynamoDbClient, "files", options);
// 4. Save entity with blob using BlobData<T>.Create()
var file = new FileMetadata
{
FileId = "file-123",
Data = BlobData<byte[]>.Create(File.ReadAllBytes("large-file.pdf")),
Thumbnail = BlobData<byte[]>.Create(thumbnailBytes)
};
await table.Files.PutAsync(file);
// 5. Load entity - eager loaded data is already available
var loaded = await table.Files.GetAsync("file-123");
var data = loaded.Data.Value; // Already loaded (eager)
// For lazy loaded properties, call LoadAsync() first
if (loaded.Thumbnail != null && !loaded.Thumbnail.IsLoaded)
{
await loaded.Thumbnail.LoadAsync();
var thumbnail = loaded.Thumbnail.Value;
}Configure how failures between blob storage and DynamoDB operations are handled:
Attempts to clean up orphaned blobs when DynamoDB operations fail:
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider); // Uses BestEffortCleanupStrategy by default
// Or explicitly:
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider)
.WithBlobStorageStrategy(new BestEffortCleanupStrategy(blobProvider, logger));Behavior:
- Uploads blobs to S3 before DynamoDB write
- If DynamoDB write fails, attempts to delete uploaded blobs (best effort)
- Cleanup failures are logged but don't throw exceptions
- On successful DynamoDB delete, deletes associated blobs from S3
Simple strategy for non-critical data where orphaned blobs are acceptable:
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider)
.WithBlobStorageStrategy(new NoCleanupStrategy(blobProvider));Behavior:
- Uploads blobs to S3 before DynamoDB write
- No cleanup on DynamoDB write failure (orphaned blobs may remain)
- No blob deletion on DynamoDB delete
Serialize complex objects to JSON before uploading to S3:
[DynamoDbTable("documents")]
public partial class LargeDocument
{
[PartitionKey]
[DynamoDbAttribute("doc_id")]
public string DocumentId { get; set; } = string.Empty;
// JSON serialize, then upload to S3
[BlobStorage]
[JsonBlob]
[DynamoDbAttribute("content")]
public BlobData<ComplexContent> Content { get; set; } = default!;
}
// Processing order:
// Save: Object → JSON serialize → Upload to S3 → Store reference in DynamoDB
// Load: Read reference from DynamoDB → Download from S3 → JSON deserialize → ObjectEncrypt data before uploading to S3:
[DynamoDbTable("secrets")]
public partial class SecretDocument
{
[PartitionKey]
[DynamoDbAttribute("id")]
public string Id { get; set; } = string.Empty;
// Encrypt before upload
[BlobStorage]
[Encrypted]
[DynamoDbAttribute("secret")]
public BlobData<byte[]> SecretData { get; set; } = default!;
}
// Requires encryption configuration:
var options = new FluentDynamoDbOptions()
.WithBlobStorage(blobProvider)
.WithEncryption(fieldEncryptor);Redact blob reference keys and values in logs:
[BlobStorage]
[Sensitive]
[DynamoDbAttribute("pii")]
public BlobData<byte[]> PersonalData { get; set; } = default!;// JSON serialize → Encrypt → Upload to S3
[BlobStorage]
[JsonBlob]
[Encrypted]
[DynamoDbAttribute("encrypted_content")]
public BlobData<SensitiveContent> EncryptedContent { get; set; } = default!;| Scenario | Exception |
|---|---|
Access Value before LoadAsync() |
InvalidOperationException |
LoadAsync() without provider configured |
InvalidOperationException |
| S3 upload/download failure | BlobStorageException |
[Encrypted] without encryptor |
EncryptionRequiredException |
[BlobStorage] without provider |
InvalidOperationException |
// Before (deprecated):
[BlobReference(BlobProvider.S3)]
[DynamoDbAttribute("data")]
public byte[] Data { get; set; }
// After:
[BlobStorage]
[DynamoDbAttribute("data")]
public BlobData<byte[]> Data { get; set; } = default!;
// Creating data:
// Before: entity.Data = bytes;
// After: entity.Data = BlobData<byte[]>.Create(bytes);
// Accessing data:
// Before: var bytes = entity.Data;
// After: var bytes = entity.Data.Value;DynamoDB does not support empty Maps, Sets, or Lists. The library automatically handles this:
var product = new Product
{
Id = "prod-123",
Tags = new HashSet<string>() // Empty set
};
await table.Products.PutAsync(product);
// The 'tags' attribute is automatically omitted from the DynamoDB itemvar emptyTags = new HashSet<string>();
// This will throw ArgumentException with clear message
await table.Query<Product>()
.Where("tags = {0}", emptyTags)
.ToListAsync();
// Error: "Cannot use empty collection in format string.
// DynamoDB does not support empty Maps, Sets, or Lists."// Check before using in expressions
if (tags != null && tags.Count > 0)
{
await table.Update<Product>()
.WithKey("pk", productId)
.Set("SET tags = {0}", tags)
.UpdateAsync();
}
else
{
// Use REMOVE to delete the attribute
await table.Update<Product>()
.WithKey("pk", productId)
.Remove("REMOVE tags")
.UpdateAsync();
}Advanced types work seamlessly with the library's format string system:
var metadata = new Dictionary<string, string>
{
["color"] = "blue",
["size"] = "large"
};
var tags = new HashSet<string> { "sale", "featured" };
// Use directly in format strings
await table.Update<Product>()
.WithKey("pk", "prod-123")
.Set("SET metadata = {0}, tags = {1}", metadata, tags)
.UpdateAsync();
// Query with collections
await table.Query<Product>()
.Where("tags = {0}", tags)
.ToListAsync();var expiresAt = DateTime.UtcNow.AddDays(30);
await table.Update<Session>()
.WithKey("pk", "sess-123")
.Set("SET expires_at = {0}", expiresAt)
.UpdateAsync();// ADD elements to a set
var newTags = new HashSet<string> { "clearance" };
await table.Update<Product>()
.WithKey("pk", "prod-123")
.Set("ADD tags {0}", newTags)
.UpdateAsync();
// DELETE elements from a set
var removeTags = new HashSet<string> { "old-tag" };
await table.Update<Product>()
.WithKey("pk", "prod-123")
.Set("DELETE tags {0}", removeTags)
.UpdateAsync();| Feature | System.Text.Json | Newtonsoft.Json | Notes |
|---|---|---|---|
| Maps | ✅ Full AOT | ✅ Full AOT | No serialization needed |
| Sets | ✅ Full AOT | ✅ Full AOT | No serialization needed |
| Lists | ✅ Full AOT | ✅ Full AOT | No serialization needed |
| TTL | ✅ Full AOT | ✅ Full AOT | Simple numeric conversion |
| JSON Blobs | ✅ Full AOT | STJ uses source generation | |
| Blob Storage | ✅ Full AOT | ✅ Full AOT | No serialization needed |
For full AOT compatibility, create a JsonSerializerContext and pass it to WithSystemTextJson():
using System.Text.Json.Serialization;
using Oproto.FluentDynamoDb;
using Oproto.FluentDynamoDb.SystemTextJson;
// 1. Define your entity
[DynamoDbTable("documents")]
public partial class Document
{
[PartitionKey]
[DynamoDbAttribute("doc_id")]
public string DocumentId { get; set; } = string.Empty;
[JsonBlob]
[DynamoDbAttribute("content")]
public DocumentContent Content { get; set; } = new();
}
// 2. Create a JsonSerializerContext for your types
[JsonSerializable(typeof(DocumentContent))]
internal partial class DocumentJsonContext : JsonSerializerContext { }
// 3. Configure FluentDynamoDbOptions with the context
var options = new FluentDynamoDbOptions()
.WithSystemTextJson(DocumentJsonContext.Default);
var table = new DocumentTable(dynamoDbClient, "documents", options);This approach:
- Uses compile-time source generation for serialization
- Produces no trim warnings
- Has zero runtime reflection overhead
Newtonsoft.Json uses runtime reflection which has limited AOT support:
// Uses runtime reflection - may cause trim warnings
var options = new FluentDynamoDbOptions()
.WithNewtonsoftJson();Recommendation: Use System.Text.Json with a JsonSerializerContext for projects targeting Native AOT.
<ItemGroup>
<PackageReference Include="Oproto.FluentDynamoDb.Attributes" Version="0.3.0" />
</ItemGroup>// Before
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("pk")]
public string Id { get; set; }
[DynamoDbAttribute("name")]
public string Name { get; set; }
}
// After - Add advanced types
[DynamoDbTable("products")]
public partial class Product
{
[DynamoDbAttribute("pk")]
public string Id { get; set; }
[DynamoDbAttribute("name")]
public string Name { get; set; }
// Add collections
[DynamoDbAttribute("tags")]
public HashSet<string> Tags { get; set; }
[DynamoDbAttribute("metadata")]
public Dictionary<string, string> Metadata { get; set; }
// Add TTL
[DynamoDbAttribute("ttl")]
[TimeToLive]
public DateTime? ExpiresAt { get; set; }
}Existing items without the new attributes will load with null values:
var product = await table.Get<Product>()
.WithKey("pk", "old-product")
.GetItemAsync();
// product.Tags will be null for old items
// Initialize if needed
product.Tags ??= new HashSet<string>();
product.Tags.Add("migrated");
await table.Products.PutAsync(product);// Before - Manual AttributeValue creation
var item = new Dictionary<string, AttributeValue>
{
["pk"] = new AttributeValue { S = "prod-123" },
["tags"] = new AttributeValue { SS = new List<string> { "tag1", "tag2" } },
["metadata"] = new AttributeValue
{
M = new Dictionary<string, AttributeValue>
{
["color"] = new AttributeValue { S = "blue" }
}
}
};
await dynamoDbClient.PutItemAsync(new PutItemRequest
{
TableName = "products",
Item = item
});
// After - Use entity with source generator
var product = new Product
{
Id = "prod-123",
Tags = new HashSet<string> { "tag1", "tag2" },
Metadata = new Dictionary<string, string> { ["color"] = "blue" }
};
await table.Products.PutAsync(product);The source generator validates advanced type usage at compile-time:
// DYNDB101: Invalid TTL type
[TimeToLive]
public string ExpiresAt { get; set; } // Error: Must be DateTime or DateTimeOffset
// DYNDB102: Missing JSON serializer package
[JsonBlob]
public ComplexObject Data { get; set; } // Warning: Add SystemTextJson or NewtonsoftJson package
// DYNDB105: Multiple TTL fields
[TimeToLive]
public DateTime? ExpiresAt { get; set; }
[TimeToLive]
public DateTime? DeletedAt { get; set; } // Error: Only one TTL field allowedIf you use [JsonBlob] properties without configuring a JSON serializer via FluentDynamoDbOptions, you'll get a clear runtime exception:
// This will throw InvalidOperationException
var options = new FluentDynamoDbOptions(); // No JSON serializer configured!
var table = new DocumentTable(dynamoDbClient, "documents", options);
await table.Documents.Put(document).PutAsync();
// InvalidOperationException: Property 'Content' has [JsonBlob] attribute but no JSON serializer is configured.
// Call .WithSystemTextJson() or .WithNewtonsoftJson() on FluentDynamoDbOptions.Solution: Configure a JSON serializer:
var options = new FluentDynamoDbOptions()
.WithSystemTextJson(); // or .WithNewtonsoftJson()try
{
await table.Products.PutAsync(product);
}
catch (DynamoDbMappingException ex)
{
// Detailed error with property name and context
Console.WriteLine($"Failed to map property: {ex.PropertyName}");
Console.WriteLine($"Entity type: {ex.EntityType}");
Console.WriteLine($"Error: {ex.Message}");
}