Skip to main content

FluentDynamoDB v1.1.0

· 7 min read
Dan Guisinger
Founder, Oproto Inc.

FluentDynamoDB v1.1.0 is now available on NuGet.

This release focuses on developer ergonomics and safety by reducing boilerplate around key handling, improving upon computed fields and catching more mistakes at compile time. If you've ever forgotten to call Keys.Pk() before a Put, or wished your computed keys could accept typed parameters directly, this release should improve your experience.

It also adds flexibility for blob storage and encryption by enabling per-property routing to blob storage and encryption key providers.

Additionally, all compiler diagnostics codes now link back to the website for documentation on what the diagnostic code is indicating.

Automatic Key Prefix Application

The most common mistake with FluentDynamoDB v1.0 was forgetting to apply key prefixes before Put operations. In v1.1.0, Put operations automatically apply configured prefixes during serialization based on the resolved KeyInputMode.

Before (v1.0.0):

// Manual prefix construction required before Put
var order = new Order
{
Pk = Order.Keys.Pk(orderId), // "ORDER#12345"
Sk = Order.Keys.Sk(lineId), // "LINE#abc"
Total = 99.99m
};
await table.Orders.PutAsync(order);

After (v1.1.0):

// Auto mode applies prefix automatically during Put serialization
var order = new Order
{
Pk = orderId, // Automatically becomes "ORDER#12345"
Sk = lineId, // Automatically becomes "LINE#abc"
Total = 99.99m
};
await table.Orders.PutAsync(order);

Existing code using Order.Keys.Pk(value) continues to work unchanged — Auto mode detects the prefix is already present and passes through. You can also opt into explicit KeyInputMode.Value (always prepend) or KeyInputMode.Raw (never prepend) per-call or globally via FluentDynamoDbOptions.

Computed Key Typed Overloads

Entities with computed keys now generate typed convenience overloads for Get, Update, Delete, and ConditionCheck accessors. No more calling .ToString() on your enum or formatting a Guid before passing it to the accessor.

Before (v1.0.0):

// Only string parameters available — manual conversion required
var pk = Event.Keys.BuildPk(year.ToString(), month.ToString(), day.ToString());
var evt = await table.Events.GetAsync(pk);

// Enum keys required ToString()
var product = await table.Products.GetAsync(category.ToString(), productId.ToString());

After (v1.1.0):

// Typed overloads generated for non-string computed key parameters
var evt = await table.Events.GetAsync(year, month, day);

// Enum, int, Guid, DateTime, DateOnly, TimeOnly all supported
var product = await table.Products.GetAsync(category, productId);

The source generator inspects the types of your [Extracted] source properties and generates overloads matching those types directly, including proper AttributeValue construction for each type.

Computed Field Format Specifiers

Computed field format strings now support standard .NET format specifiers. Use {0:yyyy-MM-dd} for dates, {0:D4} for zero-padded integers, or {0:G} for enum name formatting — all evaluated with CultureInfo.InvariantCulture for consistency.

[SortKey]
[DynamoDbAttribute("sk")]
[Computed("EventDate", "Category", Format = "{0:yyyy-MM-dd}#{1}")]
public string Sk { get; set; } = string.Empty;
// Produces: "2024-03-15#electronics" (consistent across Put, Update, and Key builder paths)

Format specifiers declared on source properties via [DynamoDbAttribute(Format = "...")] serve as a fallback when the computed format placeholder has no explicit specifier.

Named Blob Providers

Entities with multiple blob properties can now route each property to a different storage backend. Register named providers at configuration time and reference them with the Provider property on [BlobStorage].

Before (v1.0.0):

// Single provider for all blob properties — no per-property routing
var options = new FluentDynamoDbOptions()
.WithBlobStorage(new S3BlobProvider(s3Client, "my-bucket"));

[DynamoDbTable("Documents")]
public partial class Document
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;

[BlobStorage]
[DynamoDbAttribute("content")]
public Stream Content { get; set; } = Stream.Null;

[BlobStorage]
[DynamoDbAttribute("thumbnail")]
public Stream Thumbnail { get; set; } = Stream.Null;
}
// Both Content and Thumbnail use the same S3 bucket

After (v1.1.0):

// Multiple named providers for different blob properties
var options = new FluentDynamoDbOptions()
.WithBlobStorage(new S3BlobProvider(s3Client, "default-bucket"))
.WithBlobStorage("images", new S3BlobProvider(s3Client, "images-bucket"))
.WithBlobStorage("documents", new S3BlobProvider(s3Client, "docs-bucket"));

[DynamoDbTable("Documents")]
public partial class Document
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;

[BlobStorage(Provider = "documents")]
[DynamoDbAttribute("content")]
public Stream Content { get; set; } = Stream.Null;

[BlobStorage(Provider = "images")]
[DynamoDbAttribute("thumbnail")]
public Stream Thumbnail { get; set; } = Stream.Null;
}
// Content routes to "docs-bucket", Thumbnail routes to "images-bucket"

Properties without a Provider specified continue to use the default provider, so existing code is unaffected.

Per-Property Encryption Key Aliases

The [Encrypted] attribute now supports a KeyAlias property, enabling different encrypted fields on the same entity to use different KMS keys based on data classification.

[Encrypted(KeyAlias = "pii")]
[DynamoDbAttribute("ssn")]
public string Ssn { get; set; } = string.Empty;

[Encrypted(KeyAlias = "financial")]
[DynamoDbAttribute("accountNumber")]
public string AccountNumber { get; set; } = string.Empty;

The alias is threaded through FieldEncryptionContext to your IKmsKeyResolver implementation. The DefaultKmsKeyResolver now accepts an aliasKeyMap parameter for static alias-to-ARN mappings.

Async KMS Key Resolver (Breaking Change)

Breaking Change

The IKmsKeyResolver interface has changed from synchronous to asynchronous. All implementations must be updated.

Before:

public interface IKmsKeyResolver
{
string ResolveKeyId(string? contextId);
}

After:

public interface IKmsKeyResolver
{
Task<string> ResolveKeyIdAsync(
string? contextId,
string? keyAlias = null,
CancellationToken cancellationToken = default);
}

This was the only blocking call in an otherwise fully-async encryption pipeline. The new signature enables true async key resolution (database lookups, secrets managers, external APIs), per-property key selection via keyAlias, and cooperative cancellation. See the encryption documentation for details.

Auto-Derived Discriminator Patterns

Multi-entity tables no longer require manually specifying DiscriminatorPattern on [DynamoDbTable]. The source generator now derives discriminator patterns automatically from key prefix and computed format configurations.

// Generator auto-derives discriminator from key format — no manual specification needed
[DynamoDbTable("orders")]
public partial class Order
{
[PartitionKey(Prefix = "CUSTOMER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;

[SortKey(Prefix = "ORDER")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
// Auto-derived: DiscriminatorPattern = "ORDER#*" (on "sk" attribute)
}

New compile-time diagnostics (FDDB100–FDDB104) catch prefix/format conflicts, discriminator/key-format contradictions, overlapping patterns between entities, redundant explicit discriminators, and compound key discrimination where entities share the same sort key prefix but differ by partition key. Overlapping patterns are resolved automatically using specificity scoring with generated exclusion guards, and same-score overlaps are resolved via compound key checks when cross-key patterns differ.

Schema Versioning

A new assembly-level attribute lets you declare which generated code shape your project targets:

[assembly: FluentDynamoDbSchemaVersion(1, 0)]

This decouples the generated code evolution from the NuGet package version. When a future release changes the generated code shape, you can migrate at your own pace by bumping your declared version when ready. Seven new diagnostics (FDDB110–FDDB116) guide you through version management.

Update Model Compile-Time Safety

Key properties and computed fields are now excluded from generated update models at compile time. Attempting to set a key property in an update expression produces a compile error instead of a runtime exception.

Source-property-based updates for computed fields also work automatically — updating the source properties triggers recomputation of the computed key value:

// Setting source properties automatically recomputes the GSI key
await table.Products.Update(pk, sk)
.Set(x => new ProductUpdateModel { Status = "Active", Region = "US-East" })
.UpdateAsync();
// Generates: SET #status = :p0, #region = :p1, #gsi1pk = :p2
// where :p2 = "Active#US-East" (recomputed from source properties)

Constant Key Detection

Key properties that return a fixed compile-time value are now detected automatically by the source generator. Use expression-body (=>) or read-only auto-property syntax to declare constant keys:

[DynamoDbTable("Customers")]
public partial class Customer
{
[PartitionKey(Prefix = "CUSTOMER")]
[DynamoDbAttribute("pk")]
public string CustomerId { get; set; } = string.Empty;

[SortKey]
[DynamoDbAttribute("sk")]
public string Sk => "PROFILE"; // Constant key — auto-detected
}

When detected, the generator simplifies the Keys class (omits the constant from method parameters), omits the constant from Get/Delete/Update convenience methods, auto-derives a discriminator pattern, and handles serialization correctly for properties without setters. Four new diagnostics (FDDB120–FDDB123) catch invalid constant key configurations at compile time.

Expanded Diagnostics

This release adds 15 new compile-time diagnostics covering constant keys (FDDB120–FDDB126), discriminator pattern conflicts (FDDB100–FDDB104), and schema versioning (FDDB110–FDDB116). All diagnostic codes now include helpLinkUri that links directly to detailed documentation at fluentdynamodb.dev/diagnostics.

Installing v1.1.0

dotnet add package Oproto.FluentDynamoDb --version 1.1.0

Or update an existing project:

dotnet add package Oproto.FluentDynamoDb

Feedback

Questions or feedback? Find us on GitHub or Reddit.