All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
[RelatedEntity]Generic Mapping Not Deserializing Items WhenEntityTypeIs Omitted - Fixed the source generator emitting a TODO stub (new ElementType()) instead of callingFromDynamoDbwhen a[RelatedEntity]attribute does not explicitly specifyEntityType. The framework now infers the entity type from the property's generic type parameter (e.g.,UserSubscriptionfromList<UserSubscription>) and generates proper deserialization calls. This affectedToCompositeEntityAsyncreturning collections with correct item counts but all-default property values. The fix applies to sync collection mapping, async collection mapping, and single entity mapping paths. SpecifyingEntityType = typeof(T)explicitly continues to work as before. -
Complex-Type Collection Properties Not Deserialized in Multi-Item
FromDynamoDbPath - Fixed the source generator emitting a TODO stub for complex-type collection elements (e.g.,List<Address>whereAddressis a[DynamoDbEntity]) during multi-item entity reconstruction. The generated code now properly deserializes Map AttributeValues viaFromDynamoDband handles List-of-Maps as a fallback, with try/catch error handling for graceful degradation. This only affected composite entities retrieved viaToCompositeEntityAsyncthat also had complex-type collection properties alongside[RelatedEntity]relationships.
-
Extracted Property Type Conversion - Fixed the source generator producing uncompilable code for
[Extracted]properties with non-string types (enums, int, long, decimal, etc.). TheMapperGenerator.GenerateExtractedKeyLogicmethod unconditionally assigned the rawstringresult ofSplit()without type conversion, causing CS0029 compile errors. TheKeysGenerator.GetExtractionExpressionmethod relied on a name-basedIsEnumTypeheuristic that only matched type names containing "Status", "Type", "Kind", or "State" — enums with other names (e.g.,SnsSubscriptionTopic) silently generated broken code. The fix adds aPropertyModel.IsEnumflag set from Roslyn semantic analysis (ITypeSymbol.TypeKind == TypeKind.Enum) and applies type-aware conversion (Enum.Parse<T>(),int.Parse(), etc.) in both code generation paths. The brokenIsEnumTypeheuristic methods in bothKeysGeneratorandMapperGeneratorhave been removed. -
DYNDB033 False Positive on Computed↔Extracted Bidirectional Mapping - Fixed the source generator's
EntityAnalyzerincorrectly reporting DYNDB033 "Circular dependency detected between computed properties" when[Computed]and[Extracted]attributes form a valid bidirectional mapping pattern on the same property pair. The[Computed]attribute operates on the write path (composing source properties into a key) while[Extracted]operates on the read path (decomposing the key back into components) — these cannot form a circular dependency. The fix removes the over-broad cross-check fromValidateExtractedProperty, relying on the existing DFS-basedValidateComputedKeyCircularDependencieswhich correctly catches genuine Computed→Computed cycles. -
Non-String Key Accessor Compilation Errors - Fixed the source generator producing uncompilable code for entities with non-string key types (enum, int, long, Guid, DateTime, DateOnly, TimeOnly, nullable value types) that have no prefix and are not computed. The generated accessor methods (Get, Update, Delete, ConditionCheck) and table-level key overloads now emit
.SetKey(k => { ... })with inlineAttributeValueconstruction instead of.WithKey()when the key parameter is a non-string type. This ensures the generated code compiles correctly by constructing theAttributeValuewith the appropriate DynamoDB type (Nfor numerics,Sfor strings/enums/Guids/dates) and respectingFormat,DateTimeKind, and nullable.Valueaccessor patterns. Entities with string keys, prefixed keys, or computed keys continue to use.WithKey()unchanged. -
Tautological Exclusion Guard Detection - Fixed the source generator's
PatternOverlapAnalyzersilently generating contradictoryMatchesEntitycode when a computed exclusion guard is identical to the entity's own positive match criterion. This causedMatchesEntityto always returnfalsefor affected entities (e.g., a Contains-strategy entity*#ROLE#*overlapping with a Complex-strategy entityUSER#*#ROLE#*). The fix adds compile-time detection via a newDISC006diagnostic error instead of generating unreachable code. Valid hierarchies (e.g.,USER#*withUSER#*#ROLE#*) are unaffected. -
NuGet Package Including Examples Code - Fixed the
Oproto.FluentDynamoDb/Examples/folder being compiled into the library DLL and shipped in the NuGet package. Removed the folder entirely. -
ToCompositeEntityAsyncFails to Populate[RelatedEntity]Collections on Encrypted Entities - Fixed the generated multi-itemFromDynamoDbAsync(IList<...> items, ...)overload discarding all items after index 0, causingToCompositeEntityAsyncto return entities with empty related collections when the parent entity has[Encrypted]or[BlobReference]properties. The fix:- Implements full composite assembly logic in the generated async multi-item method — primary entity identification via regex exclusion of
[RelatedEntity]sort key patterns, async deserialization of matching related items viaawait ChildEntity.FromDynamoDbAsync(...), and collection population - Adds
FromDynamoDbAsync<TSelf>(IList<...> items, ...)as a static abstract member onIDynamoDbEntity, enablingToCompositeEntityAsyncto call the async multi-item path directly without hydrator routing - Generates a single-item
FromDynamoDbAsync(Dictionary<...> item, ...)delegating method for non-encrypted entities, so parent composite assembly can uniformly callChildEntity.FromDynamoDbAsync(item, ...)regardless of the child's encryption status - Routes
ToCompositeEntityAsyncthrough the async path unconditionally, eliminating the behavioral split between encrypted and non-encrypted parent entities - Child entities with
[Encrypted]properties are now correctly decrypted during composite assembly regardless of the parent's encryption status
- Implements full composite assembly logic in the generated async multi-item method — primary entity identification via regex exclusion of
- Obsolete In-Project Examples - Removed the
Oproto.FluentDynamoDb/Examples/folder containing outdated manual entity/table implementations that predated the source generator. The canonical examples now live in theexamples/solution folder and use the current source-generator patterns. - ManualTableImplementationTests - Removed unit tests that depended on the deleted example classes. The underlying builder functionality is still covered by other tests.
- Stale RealworldExample exclusion - Cleaned up a
<Compile Remove="RealworldExample/**" />entry in the csproj for a folder that no longer exists.
- Most-Specific Pattern Matching for Overlapping Discriminators - The source generator now automatically disambiguates overlapping discriminator patterns using compile-time specificity analysis. When multiple entities on the same table have patterns that could match the same value (e.g.,
INVOICE#*andINVOICE#*#LINE#*), the generator:- Computes a specificity score for each pattern by counting non-empty literal segments after splitting on
* - Generates exclusion guards in the less-specific entity's
MatchesEntitymethod to prevent it from claiming items that match a more-specific entity - Ensures
ExactMatchdiscriminators always take precedence over wildcard patterns - Emits
DISC004(Error) when two overlapping patterns have the same specificity score (ambiguous — developer must resolve) - Emits
DISC005(Info) when overlapping patterns are automatically resolved by specificity ordering - Eliminates the need for a dedicated
entity_typeattribute in hierarchical sort key designs where parent/child entities share a common key prefix
- Computes a specificity score for each pattern by counting non-empty literal segments after splitting on
- Discriminator Pattern Overlap Analysis - Refined the
PatternOverlapAnalyzerto perform structural analysis of literal segments for Contains and Complex patterns, eliminating false-positive DISC004 errors for common multi-entity table designs:- Contains patterns (
*literal*) now use substring analysis instead of conservatively assuming overlap. Two Contains patterns only overlap if one literal is a substring of the other (e.g.,*#DEDUCTION#*and*#GARNISHMENT#*are correctly identified as non-overlapping) - Complex patterns (multi-wildcard, e.g.,
EMPLOYEE#*#DEDUCTION#*) now use structural segment comparison. Same-structure patterns are non-overlapping if any corresponding segment pair is distinguishing (neither is a substring of the other) - Cross-strategy Complex patterns (Complex vs StartsWith/EndsWith/Contains) check whether all segments of the simpler pattern appear in the more complex pattern's text
- Conservative behavior is preserved: when analysis is inconclusive (e.g., degenerate all-wildcard patterns), overlap is assumed to avoid false negatives
- Common payroll/HR pattern (
EMPLOYEE#*#PAYRATE#*,EMPLOYEE#*#DEDUCTION#*,EMPLOYEE#*#GARNISHMENT#*) now compiles cleanly without spurious DISC004 errors
- Contains patterns (
-
nameof()and Compile-Time Constant Resolution in Attributes - FixedEntityAnalyzersilently ignoringnameof()expressions andconstvariables used as positional arguments in[Computed]and[Extracted]attributes. BothExtractComputedKeyAttributes()andExtractExtractedKeyAttributes()only matchedLiteralExpressionSyntax, causingnameof(Property)(anInvocationExpressionSyntax) and const references (anIdentifierNameSyntax) to be skipped — resulting in emptySourceProperties, emptySourceProperty, or defaultIndex = 0. The fix adds asemanticModel.GetConstantValue()fallback after the existing literal check, resolving any compile-time constant expression to its value while preserving the fast-path for string and integer literals. -
Enum Type Validation in Source Generator - Fixed
EntityAnalyzer.IsSupportedPropertyType()incorrectly rejecting user-defined enum types with diagnostic DYNDB009 ("type not supported for DynamoDB mapping"). The downstreamMapperGeneratoralready handled enum serialization correctly via.ToString()/Enum.Parse<T>(), but the validator's hardcoded type allowlist had no enum detection logic. The fix uses the Roslyn semantic model (TypeKind.Enum) to identify enum types — including nullable enums (Status?) — before the DYNDB009 check, allowing them to flow through to the mapper. Collections of enums (List<Status>,HashSet<Status>) were already accepted by the collection type check. -
MatchesEntity Silent Item Drops - Fixed the source-generated
MatchesEntitymethod silently dropping legitimate items from Query, Scan, and Get results. The method used attribute-presence checks on ALL non-nullable properties as a heuristic for entity type discrimination, causing false negatives when any non-nullable property was missing from a DynamoDB item — even when the item legitimately belonged to that entity type. This affected empty collections (not persisted by DynamoDB), schema evolution (new properties added after items were written), and sparse writes (items written with partial attributes). The fix replaces the overly strict heuristic with a three-tier approach:- Tier 1 (discriminator configured): Uses only the discriminator check (
DiscriminatorProperty/DiscriminatorValue/DiscriminatorPattern) to determine entity type membership — no data attribute checks - Tier 2 (single-entity table): Uses only key attribute presence (pk/sk) since no type discrimination is needed
- Tier 3 (multi-entity table without discriminator): Uses only key attribute presence, accepting the tradeoff of possible wrong-type hydration vs silent data loss
- Also fixes the legacy
EntityDiscriminatorattribute name: the old code hard-coded"EntityType"but the actual DynamoDB attribute (as mapped byDiscriminatorAnalyzer) is"entity_type" - Migration note: Multi-entity tables that previously relied on the over-filtering behavior (non-key attribute checks acting as implicit type discrimination) should add explicit discriminator configuration via
DiscriminatorProperty/DiscriminatorValueto maintain correct entity type filtering
- Tier 1 (discriminator configured): Uses only the discriminator check (
- Missing GSI/LSI on Table Creation - Fixed two bugs that prevented Global Secondary Indexes and Local Secondary Indexes from being provisioned during programmatic table creation:
IntegrationTestBase.CreateTableAsync<TEntity>()manually constructed aCreateTableRequestwithout readingmetadata.Indexes, so entities with GSIs/LSIs got tables without any secondary indexes. Now delegates toTableCreator.CreateAsync()which already handles indexes correctly.- The source-generated multi-entity
CreateTableAsynconly used the default entity's metadata, omitting indexes from non-default entities. Now aggregates indexes from all entities sharing the table into a merged metadata before callingTableCreator.
- Bare Boolean Expression Translation - Fixed
ExpressionTranslatorproducing invalid DynamoDB syntax for bare boolean property access in filter and condition expressions.!x.IsDeletedgeneratedNOT (#attr0)andx.IsActivegenerated just#attr0— neither is a valid DynamoDB condition. They now correctly translate to#attr0 = :p0with the appropriate BOOL value (false for negation, true for affirmative). Affects top-level booleans, booleans in compound AND/OR expressions, and nested boolean properties (e.g.,x.Settings.IsEnabled).
- Encrypted Composite Entity Read Operations - Fixed
NotSupportedExceptiononToCompositeEntityAsyncandToCompositeEntityListAsync(Query/Scan) for entities with[Encrypted]properties. These methods called the sync multi-itemFromDynamoDbstub directly instead of using the registered hydrator.
-
Automatic Hydrator Registration - Generated table constructors now auto-register hydrators for entities with
[Encrypted]or blob storage properties. Previously, users had to manually callDefaultEntityHydratorRegistry.Instance.Register{Entity}Hydrator()before any Put/Get operations would work on encrypted entities. This is now handled automatically when the table is instantiated. -
Encrypted Entity Read Operations - Fixed
NotSupportedExceptiononGetItemAsync,ToListAsync(Query), andToListAsync(Scan) for entities with[Encrypted]properties. These methods called the syncFromDynamoDbstub directly instead of using the registered hydrator for async deserialization. They now check the hydrator registry and useHydrateAsyncwhen a hydrator is available. -
Options Not Passed to Scan Builder - The generated
Scan()method on entity accessors and single-entity tables created aScanRequestBuilderwithout passingFluentDynamoDbOptions. This meant the hydrator had no access to the configuredIFieldEncryptorduring deserialization, causing "no IFieldEncryptor is configured" errors on encrypted entities. All generated builder instantiations now pass options through. -
Options Not Passed to ConditionCheck Builder - The generated
ConditionCheck<T>()method was not passingFluentDynamoDbOptionsto theConditionCheckBuilder. Fixed for consistency — ensures logger and future options are available. -
Misleading FromDynamoDb Stub Error Messages - The generated
FromDynamoDbstub methods always reported "has blob reference properties" regardless of whether the entity used encryption-only or blob storage. Error messages now correctly identify the entity's actual requirements (encrypted, blob, or both).
- AWS Encryption SDK Updated to 5.0.0 - Updated
AWS.Cryptography.EncryptionSDKfrom[4.0.0,5.0.0)to[5.0.0,6.0.0)andAWSSDK.KeyManagementServiceminimum from4.0.0to4.0.9.4. The 4.x Encryption SDK's internalComAmazonawsKms 1.0.0package was built against AWSSDK v3 and is incompatible with AWSSDK v4. Version 5.0.0 usesComAmazonawsKms 2.0.0which targets AWSSDK v4. Also adaptedCreateAwsKmsKeyringcalls to provide the now-requiredKmsClientparameter with region extracted from the key ARN.
-
Encrypted Entity Put Operations - Fixed
NotSupportedExceptionthrown when usingPut(entity)on entities with[Encrypted]properties. Three code paths were eagerly calling the synchronousToDynamoDb()stub instead of deferring serialization to async execution time via the hydrator pipeline:- Generated accessor
Put(entity)method (e.g.,table.Users.Put(user)) - Generated table-level
Put(entity)shortcut (e.g.,table.Put(user)) EntityExecuteAsyncExtensions.WithItem<T>()(used by generictable.PutAsync<TEntity>(entity))
All three now correctly delegate to
PutItemRequestBuilder.WithItem(TEntity)which detects encrypted entities and defers serialization toPutAsync()resolution via the hydrator registry. - Generated accessor
-
String CompareTo Support in Lambda Expressions - Added support for
string.CompareTo()method in lambda expressions for string range comparisons- Enables intuitive string comparison syntax:
x => x.SortKey.CompareTo("2024-01-01") >= 0 - Translates to DynamoDB comparison operators:
#attr >= :p0 - Supports all comparison operators:
>,>=,<,<=,==,!= - Works in both key conditions and filter expressions
Usage:
// String range query on sort key var orders = await table.Orders.Query() .Where(x => x.CustomerId == customerId && x.OrderDate.CompareTo("2024-01-01") >= 0) .ToListAsync(); // Combined range (alternative to Between) var orders = await table.Orders.Query() .Where(x => x.CustomerId == customerId && x.OrderDate.CompareTo("2024-01-01") >= 0 && x.OrderDate.CompareTo("2024-12-31") <= 0) .ToListAsync();
- Enables intuitive string comparison syntax:
-
DateOnly and TimeOnly Serialization - Native serialization support for .NET 6+
DateOnlyandTimeOnlytypes- DateOnly Serialization: Automatically serializes to ISO 8601 date format (
yyyy-MM-dd) as DynamoDB string attribute - TimeOnly Serialization: Automatically serializes to ISO 8601 time format (
HH:mm:ss.fffffff) as DynamoDB string attribute - UpdateExpressionTranslator Support: Both types work in lambda-based update and filter expressions
- Collection Support:
List<DateOnly>andList<TimeOnly>are fully supported - Custom Format Strings: Use
[DynamoDbAttribute(Format = "...")]to specify custom formats - Nullable Support:
DateOnly?andTimeOnly?handle null values correctly - Round-Trip Consistency: Serializing then deserializing produces equivalent values
- Requirements: 1.1-1.5, 2.1-2.5, 3.1-3.2, 4.1-4.4, 5.1-5.4
Usage:
[DynamoDbTable("Events")] public partial class Event { [PartitionKey] [DynamoDbAttribute("pk")] public string Pk { get; set; } = string.Empty; // Default ISO 8601 format: "2024-12-28" [DynamoDbAttribute("eventDate")] public DateOnly EventDate { get; set; } // Custom format: "12/28/2024" [DynamoDbAttribute("displayDate", Format = "MM/dd/yyyy")] public DateOnly DisplayDate { get; set; } // Default ISO 8601 format: "14:30:45.1234567" [DynamoDbAttribute("startTime")] public TimeOnly StartTime { get; set; } // Custom format: "2:30 PM" [DynamoDbAttribute("displayTime", Format = "h:mm tt")] public TimeOnly DisplayTime { get; set; } // Collections are supported [DynamoDbAttribute("availableDates")] public List<DateOnly> AvailableDates { get; set; } = new(); } // Use in update expressions await table.Events.Update(eventId) .Set(x => new EventUpdateModel { EventDate = new DateOnly(2024, 12, 31) }) .UpdateAsync(); // Use in filter expressions var events = await table.Events.Query(x => x.Pk == pk) .WithFilter(x => x.EventDate > new DateOnly(2024, 1, 1)) .ToListAsync();
- DateOnly Serialization: Automatically serializes to ISO 8601 date format (
-
IfNotExists with Arithmetic - Support for combining
IfNotExists()with arithmetic operations in update expressions- Enables counter patterns with non-zero default values:
x.Count.IfNotExists(100) + 1 - Generates DynamoDB expression:
SET #count = if_not_exists(#count, :default) + :increment - Supports both addition and subtraction operators
- Works with all numeric types (int, long, decimal, etc.)
Usage:
// Initialize counter to 100 if missing, then increment await table.Users.Update(userId) .Set(x => new UserUpdateModel { Count = x.Count.IfNotExists(100) + 1 }) .UpdateAsync(); // Decrement with default await table.Users.Update(userId) .Set(x => new UserUpdateModel { Balance = x.Balance.IfNotExists(1000m) - 50m }) .UpdateAsync();
- Enables counter patterns with non-zero default values:
-
Key Condition Shortcuts - Simplified conditional patterns for Put, Update, and Delete operations
- New
KeyConditionenum withNone,MustExist, andMustNotExistvalues - New
IfExists()andIfNotExists()builder methods onPutItemRequestBuilder,UpdateItemRequestBuilder, andDeleteItemRequestBuilder - New
WithKeyCondition(KeyCondition)builder method for explicit enum-based configuration - Automatically generates
attribute_exists()orattribute_not_exists()conditions for key attributes - Supports both simple (PK only) and composite (PK + SK) key entities
- Combines with existing
Where()clauses using AND - Optional
KeyConditionparameter on generated convenience methods (PutAsync,Update,DeleteAsync) - Works within transactions and batch operations
- Requirements: 1.1-1.4, 2.1-2.5, 3.1-3.3, 4.1-4.3, 5.1-5.4, 6.1-6.4, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.3
Usage:
// Create only (fail if exists) await table.Users.Put(user).IfNotExists().PutAsync(); await table.Users.PutAsync(user, KeyCondition.MustNotExist); // Update existing only (prevent upsert) await table.Users.Update(pk, sk, KeyCondition.MustExist) .Set(x => new UserUpdateModel { Name = newName }) .UpdateAsync(); // Delete only if exists await table.Users.Delete(pk).IfExists().DeleteAsync(); await table.Users.DeleteAsync(pk, KeyCondition.MustExist); // Combine with additional conditions await table.Users.Update(pk, sk) .IfExists() .Set(x => new UserUpdateModel { Status = "active" }) .Where(x => x.Status == "pending") .UpdateAsync();
- New
-
Automatic Index Projections - Enhanced GSI/LSI generation with automatic projection type support
- Automatic Entity Projections for Single-Entity Tables: Indexes in single-entity tables automatically use the entity type as the default projection, enabling non-generic
Query()methods- Single-entity tables generate
DynamoDbIndex<TEntity>instead ofDynamoDbIndex - Non-generic
Query()methods return the entity type directly - Multi-entity tables preserve existing behavior with
DynamoDbIndex
- Single-entity tables generate
- ProjectionType Property on Index Attributes: New
ProjectionTypeproperty on[GlobalSecondaryIndex]and[LocalSecondaryIndex]attributesProjectionType.All(default): All attributes projectedProjectionType.KeysOnly: Auto-generates keys-only projection recordProjectionType.Include: Use with[UseProjection]for custom projection- Metadata used by schema validation and table creation
- Auto-Generated Keys Only Projection Records: When
ProjectionType = KeysOnlyis specified, a read-only projection record is automatically generated- Record named
{IndexPropertyName}KeysProjection - Contains GSI/LSI keys AND base table keys
- Implements
IReadOnlyEntity<TSelf>interface - Includes
FromDynamoDbmethod andProjectionExpressionproperty GetPartitionKey()/GetSortKey()return base table keys for entity lookup
- Record named
- New Diagnostic Warnings:
FDDB070: Warning whenProjectionType = Includebut noProjectedPropertiesdefinedFDDB072: Warning whenProjectionType = KeysOnlycombined with[UseProjection](UseProjection takes precedence)
- Requirements: 1.1-1.4, 2.1-2.5, 3.1-3.9, 4.1-4.5, 5.1-5.4, 6.1-6.3
Usage:
// Single-entity table: automatic entity projection [DynamoDbTable("orders")] public partial class Order { [PartitionKey] [DynamoDbAttribute("pk")] public string Pk { get; set; } = string.Empty; [SortKey] [DynamoDbAttribute("sk")] public string Sk { get; set; } = string.Empty; [GlobalSecondaryIndex("StatusIndex", IsPartitionKey = true)] [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty; } // Generated: DynamoDbIndex<Order> StatusIndex var orders = await table.StatusIndex.Query(x => x.Status == "pending").ToListAsync(); // Keys Only projection: auto-generates projection record [GlobalSecondaryIndex("CategoryIndex", IsPartitionKey = true, ProjectionType = ProjectionType.KeysOnly)] [DynamoDbAttribute("category")] public string Category { get; set; } = string.Empty; // Query returns CategoryIndexKeysProjection, use to batch-get full entities var keys = await table.CategoryIndex.Query(x => x.Category == "electronics").ToListAsync(); var orders = await DynamoDbBatch.Get.Add(keys.Select(k => table.Orders.Get(k.Pk, k.Sk))).ExecuteAsync();
- Automatic Entity Projections for Single-Entity Tables: Indexes in single-entity tables automatically use the entity type as the default projection, enabling non-generic
-
List Index API Consistency and Dynamic Index Support - Unified API for list index operations with dynamic index evaluation
- New
SetAtExtension Method: Update list elements at specific indices using consistent extension method patternx.Tags.SetAt(0, "updated")→SET #tags[0] = :v0- Works with nested lists:
x.Metadata.Keywords.SetAt(0, "value") - Supports chaining multiple SetAt calls:
x.Tags.SetAt(0, "a").SetAt(1, "b")→SET #tags[0] = :v0, #tags[1] = :v1
- New
RemoveAtExtension Method: Remove list elements at specific indices using consistent extension method patternx.Tags.RemoveAt(2)→REMOVE #tags[2]- Works with nested lists:
x.Metadata.Keywords.RemoveAt(1)
- Dynamic Index Support: Use variables, method calls, and property access as list indices (evaluated at translation time)
- Variable index:
int idx = GetIndex(); .Set(x => x.Tags.SetAt(idx, "value")) - Method call index:
.Set(x => x.Tags.SetAt(GetTargetIndex(), "value")) - Property access index:
.Set(x => x.Tags.SetAt(config.Index, "value")) - Works in filter expressions:
int i = 0; .WithFilter(x => x.Tags[i] == "featured") - Works in condition expressions:
.Where(x => x.Tags[index] == "expected")
- Variable index:
- Index Validation: Negative indices throw
ArgumentOutOfRangeExceptionat translation time - Entity Parameter Rejection: Indices referencing the entity parameter throw
UnsupportedExpressionException - Requirements: 1.1-1.5, 2.1-2.5, 3.1-3.6
Usage:
using Oproto.FluentDynamoDb.Expressions; // SetAt with constant index await table.Items.Update(itemId) .Set(x => x.Tags.SetAt(0, "updated")) .UpdateAsync(); // SetAt with dynamic index int index = GetIndexToUpdate(); await table.Items.Update(itemId) .Set(x => x.Tags.SetAt(index, "updated")) .UpdateAsync(); // Chained SetAt operations await table.Items.Update(itemId) .Set(x => x.Tags.SetAt(0, "first").SetAt(1, "second")) .UpdateAsync(); // RemoveAt await table.Items.Update(itemId) .Set(x => x.Tags.RemoveAt(2)) .UpdateAsync(); // Dynamic index in filter int idx = 0; var items = await table.Items.Query(x => x.Category == category) .WithFilter(x => x.Tags[idx] == "featured") .ToListAsync();
- New
-
Nested Map and List Lambda Expression Support - Full lambda expression support for nested object properties, list indexing, and collection operations
- Nested Property Access in Filters/Conditions: Query nested map properties using lambda expressions in
.WithFilter()and.Where()on Put/Update/Delete operations- Single-level:
.WithFilter(x => x.Address.City == "Seattle") - Multi-level:
.WithFilter(x => x.ShippingAddress.Country.Code == "US") - Works with all comparison operators and logical operators
- Single-level:
- List Index Access in Filters/Conditions: Filter on specific list elements by index
- Direct index:
.WithFilter(x => x.Tags[0] == "featured") - Nested list:
.WithFilter(x => x.Metadata.Keywords[0] == "sale") - Object in list:
.WithFilter(x => x.LineItems[0].ProductId == productId)
- Direct index:
- Nested Property Updates: Partially update nested objects without replacing the entire map
- Single property:
.Set(x => new CustomerUpdateModel { ShippingAddress = new AddressUpdateModel { City = "Portland" } }) - Multiple properties in single expression
- Multi-level nesting support
- Combined with top-level updates
- Single property:
- List Operations: New extension methods for list manipulation in update expressions
Append<T>(item)- Add element to end of listPrepend<T>(item)- Add element to beginning of listAppendRange<T>(items)- Add multiple elements to endPrependRange<T>(items)- Add multiple elements to beginningSet(x => x.List[index], value)- Update element at specific indexRemove(x => x.List[index])- Remove element at specific index- Works with nested lists:
.Set(x => x.Metadata.Keywords.Append("sale"))
- Set Operations: New builder methods for DynamoDB set manipulation
Add(x => x.SetProperty, value)- Add element to setAdd(x => x.SetProperty, values[])- Add multiple elementsDelete(x => x.SetProperty, value)- Remove element from setDelete(x => x.SetProperty, values[])- Remove multiple elements- Supports
HashSet<string>,HashSet<int>, and other set types
- Source Generator Enhancement: Automatically generates nested
*UpdateModeltypes for entities with[DynamoDbMap]properties - Requirements: 1.1-1.6, 2.1-2.4, 3.1-3.5, 4.1-4.6, 5.1-5.5, 6.1-6.4
Usage:
using Oproto.FluentDynamoDb.Expressions; // Filter on nested property var customers = await table.Customers.Query(x => x.CustomerId == tenantId) .WithFilter(x => x.ShippingAddress.City == "Seattle") .ToListAsync(); // Update nested property await table.Customers.Update(customerId) .Set(x => new CustomerUpdateModel { ShippingAddress = new AddressUpdateModel { City = "Portland" } }) .UpdateAsync(); // List operations await table.Items.Update(itemId) .Set(x => x.Tags.Append("new-tag")) .UpdateAsync(); // Set operations await table.Items.Update(itemId) .Add(x => x.Categories, "electronics") .UpdateAsync();
- Nested Property Access in Filters/Conditions: Query nested map properties using lambda expressions in
-
BREAKING: Index attribute API redesigned —
[GlobalSecondaryIndex]and[LocalSecondaryIndex]have been removed and replaced with three self-describing attributes:[GsiPartitionKey],[GsiSortKey], and[LsiSortKey]. The key role (partition key vs sort key) and index type (GSI vs LSI) are now encoded directly in the attribute name, eliminating the error-proneIsPartitionKey = true/IsSortKey = trueboolean flags.[GlobalSecondaryIndex("index-name", IsPartitionKey = true)]→[GsiPartitionKey("index-name")][GlobalSecondaryIndex("index-name", IsSortKey = true)]→[GsiSortKey("index-name")][LocalSecondaryIndex("index-name")]→[LsiSortKey("index-name")]- All three attributes support optional
NameandProjectionTypeproperties [GsiPartitionKey]additionally supportsDiscriminatorProperty,DiscriminatorValue, andDiscriminatorPattern- When both
[GsiPartitionKey]and[GsiSortKey]specifyNameorProjectionTypefor the same index, the[GsiPartitionKey]values take precedence - New diagnostic codes DYNDB120–DYNDB127 for index attribute validation:
- DYNDB120: GSI sort key without partition key
- DYNDB121: Duplicate GSI partition keys for same index
- DYNDB122: Duplicate GSI sort keys for same index
- DYNDB123: Duplicate LSI sort keys for same index
- DYNDB124: Empty/whitespace index name on
[GsiPartitionKey] - DYNDB125: Empty/whitespace index name on
[GsiSortKey] - DYNDB126: Empty/whitespace index name on
[LsiSortKey] - DYNDB127: Same index name used as both GSI and LSI
Migration — GSI with partition key only:
// Before [GlobalSecondaryIndex("status-index", IsPartitionKey = true)] [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty; // After [GsiPartitionKey("status-index")] [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty;
Migration — GSI with partition key and sort key:
// Before [GlobalSecondaryIndex("status-index", IsPartitionKey = true)] [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty; [GlobalSecondaryIndex("status-index", IsSortKey = true)] [DynamoDbAttribute("createdAt")] public DateTime CreatedAt { get; set; } // After [GsiPartitionKey("status-index")] [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty; [GsiSortKey("status-index")] [DynamoDbAttribute("createdAt")] public DateTime CreatedAt { get; set; }
Migration — LSI sort key:
// Before [LocalSecondaryIndex("lsi1")] [DynamoDbAttribute("updatedAt")] public DateTime UpdatedAt { get; set; } // After [LsiSortKey("lsi1")] [DynamoDbAttribute("updatedAt")] public DateTime UpdatedAt { get; set; }
-
ExpressionCache bounded to 1024 entries - The expression translation cache now has a configurable maximum size (default: 1024) to prevent unbounded memory growth in long-running applications. When the limit is reached, the cache is cleared and repopulates naturally. A new
ExpressionCache(int maxSize)constructor overload andMaxSizeproperty are available for customization. -
DYNDB023 string property heuristic removed - The source generator no longer emits DYNDB023 performance warnings for
stringproperties named "Description", "Content", or "Body". The heuristic produced too many false positives. Warnings forbyte[], complex collections, and nested complex objects remain. -
[Queryable]attribute removed - The deprecated[Queryable]attribute has been removed as promised in the v0.9 deprecation notice. Query capabilities are now exclusively derived from[PartitionKey]and[SortKey]attributes. TheDynamoDbOperationenum remains available for use in generatedPropertyMetadata. The DYNDB113 diagnostic warning is also removed since the attribute no longer exists. -
System.Text.Json minimum version raised to 8.0.5 - The lower bound of the
System.Text.Jsondependency range was raised from[8.0.0,11.0.0)to[8.0.5,11.0.0)to exclude versions with known high-severity vulnerabilities (GHSA-8g4q-xg66-9fp4, GHSA-hh2w-p6rv-4g7w). Consumers using System.Text.Json 8.0.0 through 8.0.4 will need to update. -
ConfigureAwait(false) applied to all library async calls - All
awaitcalls across all library projects and source-generated code now use.ConfigureAwait(false). This prevents potential deadlocks when the library is consumed from environments with aSynchronizationContext(WPF, WinForms, Blazor WASM). No API changes; this is a behavioral improvement.
-
Source generator hydrator nullability mismatch (CS8767) - The generated
IAsyncEntityHydrator<T>implementations now emitIBlobStorageProvider?(nullable) for theblobProviderparameter, matching the interface declaration. This eliminates CS8767 warnings in consumer projects using encryption-only entities without blob storage. -
Local Method Evaluation in Update Expressions - Fixed
UnsupportedExpressionExceptionwhen using local method calls like.ToString(),.ToUpper(),.Trim()in update expressions- Previously, method calls that don't reference the entity parameter (e.g.,
TransactionStatus.Active.ToString(),myVar.Trim().ToUpper()) would throw an exception - Now correctly evaluates these method calls at translation time and treats them as simple value assignments
- Supports enum
.ToString(), numeric.ToString(),Guid.ToString(), and chained string methods - Consistent with existing behavior in filter/query expressions (
ExpressionTranslator) - Requirements: US-1, US-2, US-3, US-4, US-5
Before (broken):
// This would throw UnsupportedExpressionException await table.Users.Update(userId) .Set(x => new UserUpdateModel { Status = TransactionStatus.Active.ToString() }) .UpdateAsync();
After (fixed):
// Now works correctly - generates SET #status = :p0 where :p0 = "Active" await table.Users.Update(userId) .Set(x => new UserUpdateModel { Status = TransactionStatus.Active.ToString() }) .UpdateAsync(); // Also works with captured variables and chained methods var name = " John Doe "; await table.Users.Update(userId) .Set(x => new UserUpdateModel { Name = name.Trim().ToUpper() }) .UpdateAsync(); // Generates SET #name = :p0 where :p0 = "JOHN DOE"
- Previously, method calls that don't reference the entity parameter (e.g.,
-
Hydration Architecture Consolidation - Fixed composite entity assembly bug where
[RelatedEntity]collections fail to populate when child entities have[DynamoDbMap]properties- Root Cause Fix: Removed
MatchesEntity()check fromGenerateRelatedEntityCollectionMapping- related entity mapping now relies solely on sort key pattern matching - Consolidated Hydration Paths: Extracted shared property deserialization logic into
GeneratePropertyDeserializationhelper method, ensuring consistent behavior across single-item, multi-item, and async hydration paths - Recursive Composite Entity Assembly: Child entities with their own
[RelatedEntity]properties are now recursively assembled, supporting arbitrary nesting depth - Graceful Error Handling: Related entity deserialization failures are logged as warnings and skipped rather than throwing exceptions
- New Log Event IDs: Added
RelatedEntityDeserializationFailed(1001) andNoPrimaryEntityFound(1002) for improved diagnostics - Backward Compatible: Existing patterns with
[JsonBlob], entities without[DynamoDbMap], and existing[RelatedEntity]configurations continue to work unchanged - Requirements: 1.1-1.4, 2.1-2.5, 3.1-3.4, 4.1-4.2, 5.1-5.4, 6.1-6.4, 7.1-7.5, 8.1-8.4
Before (broken):
// Child entity with DynamoDbMap property [DynamoDbTable("invoices")] public partial class InvoiceLine { [DynamoDbMap] [DynamoDbAttribute("metadata")] public LineMetadata Metadata { get; set; } = new(); } // Parent with RelatedEntity collection [DynamoDbTable("invoices", IsDefault = true)] public partial class Invoice { [RelatedEntity("INVOICE#*#LINE#*", EntityType = typeof(InvoiceLine))] public List<InvoiceLine> Lines { get; set; } = new(); } // ToCompositeEntityAsync would return empty Lines collection var invoice = await table.Invoices.Query() .Where(x => x.Pk == pk) .ToCompositeEntityAsync<Invoice>(); // invoice.Lines was empty!
After (fixed):
// Same entity definitions now work correctly var invoice = await table.Invoices.Query() .Where(x => x.Pk == pk) .ToCompositeEntityAsync<Invoice>(); // invoice.Lines is correctly populated with all matching child entities // Each InvoiceLine.Metadata is properly deserialized
- Root Cause Fix: Removed
-
DynamoDbMap Multi-Item Deserialization - Fixed source generator to correctly deserialize
[DynamoDbMap]properties in composite entity (multi-item) scenarios- Previously, the
GeneratePrimaryEntityIdentificationmethod in the source generator was missing theComplexType.IsMapcheck, causing incorrect deserialization of nested map types - The generated multi-item
FromDynamoDbcode now correctly uses the nested type'sFromDynamoDbmethod instead of attempting to useEnum.Parse - Affects entities with
[DynamoDbMap]properties that also use[RelatedEntity]for composite entity patterns - Requirements: 2.1, 2.2, 2.3, 2.4, 2.5
- Previously, the
-
RelatedEntity Collection Warning Suppression - Fixed false DYNDB023 performance warnings for
[RelatedEntity]collections- Properties marked with
[RelatedEntity]attribute no longer trigger DYNDB023 performance warnings - Added
IsRelatedEntityflag toPropertyModelto track RelatedEntity properties - The
EntityAnalyzer.CheckPropertyPerformancemethod now skips performance checks for RelatedEntity properties - DYNDB023 warnings continue to be reported for complex collection types without
[RelatedEntity]attribute - Requirements: 4.1, 4.3, 4.4
- Properties marked with
-
Central Package Management - Implemented NuGet Central Package Management for easier version management
- All package versions are now defined in
Directory.Packages.props - Individual
.csprojfiles no longer specify version attributes onPackageReferenceelements - Updated package version ranges to support .NET 10.x:
System.Text.Json:[8.0.0,11.0.0)Microsoft.Extensions.Logging.Abstractions:[8.0.0,11.0.0)
- Requirements: 1.1, 1.2, 1.3, 1.4, 5.1, 5.2, 5.4, 5.5
- All package versions are now defined in
-
Complex Conditional OR Pattern in Filter Expressions - Fixed an issue where complex nested OR patterns with multiple conditional clauses would incorrectly apply filters when they should be skipped
- Pattern like
skipFlag || (condA && filter1) || (condB && filter2)now correctly skips all filters whenskipFlagis true - Previously, when combining OR patterns with AND patterns (e.g.,
skipAll || (hasValue && x.Attr.AttributeExists()) || (!hasValue && x.Attr.AttributeNotExists())), one of the entity filters would incorrectly be applied even whenskipAllwas true - Root cause: Empty string was used to represent "skip" for both OR and AND patterns, but these have different semantics when combined in complex expressions
- Fix introduces distinct sentinel values for OR-skip vs AND-skip that are handled correctly when combining expressions
- Pattern like
-
Nullable Property NULL Handling - Fixed an issue where nullable properties (e.g.,
DateTime?,int?,decimal?) would throw a conversion error when reading back records wherenullwas stored- DynamoDB represents null values as
{ NULL: true }which is a valid attribute - Previously, the generated
FromDynamoDbcode would find the attribute but fail when trying to parse the non-existent value - Error message was: "Failed to convert DynamoDB attribute 'PropertyName' to Type. Attribute type: Null"
- Now properly checks for
NULL == truebefore attempting to parse nullable properties - Affects all nullable value types:
DateTime?,int?,long?,decimal?,double?,float?,bool?,Guid?, etc.
- DynamoDB represents null values as
-
Multi-Entity Index Consolidation - Indexes defined on any entity in a multi-entity table are now consolidated and available on the generated table class
- Indexes from all entities sharing a table are collected and merged
- Multiple entities defining the same index with identical configuration generate a single index property
- Indexes defined on non-default entities are now properly generated
- New diagnostics for conflicting index configurations:
FDDB053: Conflicting partition keys on same index nameFDDB054: Conflicting sort keys on same index nameFDDB055: Conflicting index types (GSI vs LSI) on same index name
- Index documentation includes all referencing entities
- Full backward compatibility with existing single-entity and multi-entity tables
- Requirements: 1.1-1.3, 2.1-2.4, 3.1-3.4, 4.1-4.3, 5.1-5.3, 6.1-6.3
Usage:
// Multi-entity table with indexes on different entities [DynamoDbTable("shared-table", IsDefault = true)] public partial class Order { [GlobalSecondaryIndex("status-index", IsPartitionKey = true)] [DynamoDbAttribute("status")] public string Status { get; set; } } [DynamoDbTable("shared-table")] public partial class Customer { [GlobalSecondaryIndex("email-index", IsPartitionKey = true)] [DynamoDbAttribute("email")] public string Email { get; set; } } // Both indexes are available on the generated table class var ordersByStatus = await table.StatusIndex.Query<Order>() .Where(x => x.Status == "pending") .ToListAsync(); var customersByEmail = await table.EmailIndex.Query<Customer>() .Where(x => x.Email == "user@example.com") .ToListAsync();
-
Conditional Filter Expressions - Support for natural
||and&&patterns with local boolean conditions in filter expressions(localCondition || x.Property == value)- skip filter when local condition is true(localCondition && x.Property == value)- include filter only when local condition is true- Works with method calls like
string.IsNullOrWhiteSpace(), negations (!flag), and compound expressions - Local conditions are evaluated at translation time, not sent to DynamoDB
- OR between two entity conditions throws
UnsupportedExpressionException(DynamoDB limitation) - Requirements: 1.1-1.3, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.3, 7.1-7.3
Usage:
// Optional filter based on parameter presence var orders = await table.Orders.Query(x => x.CustomerId == customerId) .WithFilter(x => string.IsNullOrWhiteSpace(status) || x.Status == status) .ToListAsync(); // Feature flag controlled filter var items = await table.Items.Query(x => x.Key == key) .WithFilter(x => enableDateFilter && x.Date > minDate) .ToListAsync(); // Multiple optional filters var results = await table.Orders.Query(x => x.CustomerId == customerId) .WithFilter(x => (skipStatusFilter || x.Status == status) && (skipDateFilter || x.Date > minDate)) .ToListAsync();
-
Custom Index Property Naming - New
Nameproperty on[GlobalSecondaryIndex]and[LocalSecondaryIndex]attributes for customizing generated index property names- Specify
Name = "StatusIndex"to generatetable.StatusIndexinstead of derived name from DynamoDB index name - When not specified, property name is derived from DynamoDB index name using PascalCase conversion (e.g.,
"status-index"→StatusIndex) - Multi-entity tables: when only one entity specifies a
Name, it applies to all entities sharing that index - Requirements: 1.1, 1.2, 1.3, 1.5
Usage:
// Custom index property name [GlobalSecondaryIndex("status-index", Name = "StatusIndex", IsPartitionKey = true)] [DynamoDbAttribute("status")] public string Status { get; set; } // Generated: table.StatusIndex.Query<T>() var orders = await table.StatusIndex.Query<Order>() .Where(x => x.Status == "pending") .ToListAsync();
- Specify
-
Type-Based Table Class References - New constructor overload on
[DynamoDbTable]acceptingTypefor compile-time safe table class references- Use
[DynamoDbTable(typeof(MyTableClass))]instead of string-based table names - Provides refactoring support and compile-time validation
- Referenced type must be declared as
partialclass - String-based
[DynamoDbTable("name")]continues to work unchanged - Requirements: 4.1, 4.2, 4.4, 4.5
Usage:
// Define your table class as partial public partial class OrdersTable { } // Reference it in entity definition [DynamoDbTable(typeof(OrdersTable))] public partial class Order { // Entity properties... }
- Use
-
Enhanced Typed Index Classes - Generated index classes now inherit from
DynamoDbIndexand provide consistent Query builder methods- Index classes are generated as
partialfor extensibility - Generic
Query<T>()methods with lambda, format string, and key+filter overloads - Non-generic
Query()methods when projection type is defined - Requirements: 2.2, 2.3, 2.4, 2.5, 2.6, 3.1, 3.2
Usage:
// Generic query methods var orders = await table.StatusIndex.Query<Order>() .Where(x => x.Status == "pending") .ToListAsync(); // With key and filter expressions var filtered = await table.StatusIndex.Query<Order>( x => x.Status == "active", x => x.Amount > 100) .ToListAsync(); // Non-generic when projection type defined var projected = await table.StatusIndex.Query() .Where(x => x.Status == "pending") .ToListAsync();
- Index classes are generated as
-
New Diagnostic Codes - Compile-time validation for index and table configurations
FDDB050(Error): Conflicting indexNamevalues across entities sharing a tableFDDB051(Error): Type-based table reference must be a partial classFDDB052(Warning): IndexNamespecified on multiple entities (informational)- Requirements: 1.4, 4.3, 5.2
-
Projection Interface Enhancement - Projections now implement
IReadOnlyEntity<TSelf>for seamlessQueryRequestBuildercompatibility- New
IReadOnlyEntity<TSelf>interface as base for both projections and full entities IDynamoDbEntity<TSelf>now inherits fromIReadOnlyEntity<TSelf>(backward compatible)- Generated projections implement both
IProjectionModel<TSelf>andIReadOnlyEntity<TSelf> - Projections inherit metadata from source entity (table name, partition key, sort key)
QueryRequestBuilder<T>constraint updated towhere T : class, IReadOnlyEntity<T>- Existing entity types continue to work without modification due to interface inheritance
- Projections work with standard
ToListAsync()- projection expression is automatically applied - Non-generic
Query()methods onDynamoDbIndex<TDefault>for projection-typed indexes - New diagnostic codes for projection configuration errors:
FDDB060(Error): Projection source entity not foundFDDB061(Error): Metadata inheritance failureFDDB062(Error): Projection interface violation (projection used in write context)
- Requirements: 1.1-1.5, 2.1-2.5, 3.1-3.5, 4.1-4.5, 5.1-5.5, 6.1-6.5, 7.1-7.5, 8.1-8.5
Interface Hierarchy:
IEntityMetadataProvider │ ▼ IReadOnlyEntity<TSelf> ◄── Projections implement this │ ▼ IDynamoDbEntity<TSelf> ◄── Full entities implement thisUsage:
// Define a projection for an entity [DynamoDbProjection(typeof(Order))] public partial class OrderSummary { [DynamoDbAttribute("orderId")] public string OrderId { get; set; } = string.Empty; [DynamoDbAttribute("status")] public string Status { get; set; } = string.Empty; [DynamoDbAttribute("totalAmount")] public decimal TotalAmount { get; set; } } // Define an index with default projection type public DynamoDbIndex<OrderSummary> StatusIndex => new DynamoDbIndex<OrderSummary>(this, "status-index", OrderSummary.ProjectionExpression); // Query the index - non-generic Query() uses OrderSummary automatically var results = await table.StatusIndex.Query().Where(x => x.Status == "pending").ToListAsync(); var results = await table.StatusIndex.Query("status = {0}", "pending").ToListAsync(); // Projections are read-only - write operations fail at compile time // ❌ await table.Put(orderSummary).PutAsync(); // Won't compile // ✅ await table.Put(order).PutAsync(); // Use source entity
- New
-
Dynamic Fields Enhancements - Extended
DynamicFieldCollectionwith prefix-based accessors, typed Map operations, and bulk Set/Remove operations for efficient handling of sparse attribute patterns- Prefix-Based Field Discovery:
GetFieldNamesByPrefix(prefix)returns all field names matching a prefix pattern (e.g.,"c_"for children) - Prefix-Based Field Retrieval:
GetByPrefix(prefix)returnsDictionary<string, AttributeValue>with full keysGetByPrefixWithStrippedKeys(prefix)returns dictionary with prefix stripped from keys
- Prefix-Based Field Removal:
RemoveByPrefix(prefix)removes all matching fields and returns count removed - Typed Map Getter:
GetMap<T>(fieldName)deserializes Map fields to[DynamoDbEntity]types usingIReadOnlyEntity.FromDynamoDb<T>() - Typed Map Setter:
SetMap<T>(fieldName, entity)serializes[DynamoDbEntity]types to Map fields usingIDynamoDbEntity.ToDynamoDb() - Prefix-Based Typed Map Retrieval:
GetMapsByPrefix<T>(prefix)returnsDictionary<string, T>of typed entities with full keysGetMapsByPrefixWithStrippedKeys<T>(prefix)returns dictionary with prefix stripped from keys
- Bulk Set Operations:
SetMany(fields)sets multipleAttributeValuefields at onceSetManyWithPrefix(prefix, fields)prepends prefix to each key before settingSetMapsWithPrefix<T>(prefix, entities)serializes and sets multiple typed entities with prefix
- Bulk Remove Operations:
RemoveMany(fieldNames)removes multiple fields and returns count removed - Change Tracking Integration: All operations integrate with existing change tracking for update expression support
- AOT Compatible: Uses static abstract interface methods, no reflection
- Requirements: 1.1-1.4, 2.1-2.5, 3.1-3.4, 4.1-4.5, 5.1-5.5, 6.1-6.6, 7.1-7.4, 8.1-8.5, 9.1-9.4, 10.1-10.4
Usage - Sparse Attribute Pattern (BalanceTreeNode):
// Entity with dynamic fields for children (c_{id}) and transactions (t_{id}) [DynamoDbTable("BalanceTree")] [EnableDynamicFields] public partial class TreeNode { [PartitionKey] [DynamoDbAttribute("pk")] public string Pk { get; set; } = string.Empty; [SortKey] [DynamoDbAttribute("sk")] public string Sk { get; set; } = string.Empty; [DynamoDbAttribute("v")] public int Version { get; set; } } // Nested entity for child references [DynamoDbEntity] public partial class ChildReference { [DynamoDbAttribute("subtotal")] public decimal Subtotal { get; set; } } // Read all children as typed entities var node = await table.TreeNodes.Get(pk, sk).GetItemAsync(); var children = node.DynamicFields.GetMapsByPrefixWithStrippedKeys<ChildReference>("c_"); foreach (var (childId, child) in children) Console.WriteLine($"Child {childId}: {child.Subtotal}"); // Add multiple children at once node.DynamicFields.SetMapsWithPrefix("c_", new Dictionary<string, ChildReference> { ["child1"] = new ChildReference { Subtotal = 500m }, ["child2"] = new ChildReference { Subtotal = 300m } }); // Remove all old children node.DynamicFields.RemoveByPrefix("old_"); // Save with optimistic locking await table.TreeNodes.Update(pk, sk) .Set(x => new TreeNodeUpdateModel { Version = x.Version + 1, DynamicFields = node.DynamicFields.ChangesOnly() }) .Where(x => x.Version == node.Version) .UpdateAsync();
- Prefix-Based Field Discovery:
-
Empty Conditional Expression Handling - Conditional filter/condition expressions that resolve to all-skip conditions now gracefully execute without a filter instead of throwing "Invalid FilterExpression: The expression can not be empty" error
- When all conditional clauses evaluate to skip (e.g.,
x => true || x.Status == status), the operation executes without a filter/condition - Applies to
SetFilterExpressiononQueryRequestBuilderandScanRequestBuilder - Applies to
SetConditionExpressiononPutItemRequestBuilder,UpdateItemRequestBuilder, andDeleteItemRequestBuilder - Eliminates the need to wrap
.WithFilter()calls in conditional checks when using conditional filter patterns - Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.3, 4.1-4.4
Usage:
// Safe to use even when all conditions might skip var orders = await table.Orders.Query(x => x.CustomerId == customerId) .WithFilter(x => (string.IsNullOrWhiteSpace(status) || x.Status == status) && (string.IsNullOrWhiteSpace(category) || x.Category == category)) .ToListAsync(); // If both status and category are null/empty, query executes without filter
- When all conditional clauses evaluate to skip (e.g.,
-
Consistent Null Handling in Update Expressions - BREAKING CHANGE:
nullin conditional expressions now consistently sets DynamoDB NULL instead of skipping the update- Before:
flag ? value : nullwould skip the property update whenflagwas false - After:
flag ? value : nullsets the property to DynamoDB NULL type whenflagis false - New
NoUpdate()Method: Usex.Property.NoUpdate()to explicitly skip updating a property - Migration: Replace
flag ? value : nullwithflag ? value : x.Property.NoUpdate()for skip behavior - Requirements: 1.1-1.4, 2.1-2.6, 3.1-3.3, 4.1-4.2
Migration Example:
// Before (v0.x): null in false branch skipped the update .Set(x => new UserUpdateModel { Name = shouldUpdate ? newName : null // Skipped when !shouldUpdate }) // After (v1.0): null sets DynamoDB NULL, use NoUpdate() to skip .Set(x => new UserUpdateModel { Name = shouldUpdate ? newName : x.Name.NoUpdate() // Skipped when !shouldUpdate }) // Setting to NULL (new explicit behavior) .Set(x => new UserUpdateModel { MiddleName = null // Sets attribute to DynamoDB NULL type })
Null vs NoUpdate() vs Remove():
Method DynamoDB Result Use Case = nullSET attr = NULL Set attribute to DynamoDB NULL type .NoUpdate()No operation Skip updating this property conditionally .Remove()REMOVE attr Delete the attribute entirely - Before:
-
PaginationExtensions.GetEncodedPaginationToken() - Updated to support
QueryOperationResponseandScanOperationResponsetypes- New overload for
QueryOperationResponse- use viabuilder.Response?.GetEncodedPaginationToken() - New overload for
ScanOperationResponse- use viabuilder.Response?.GetEncodedPaginationToken() - New overload for raw AWS SDK
ScanResponsefor direct SDK usage - Existing
QueryResponseoverload retained for backward compatibility - All overloads return empty string when
LastEvaluatedKeyis null or empty
Usage:
// Query pagination (recommended) var query = table.Users.Query().Where(x => x.TenantId == tenantId).Take(25); var users = await query.ToListAsync(); var nextToken = query.Response?.GetEncodedPaginationToken() ?? string.Empty; // Scan pagination var scan = table.Logs.Scan().Take(100); var logs = await scan.ToListAsync(); var nextToken = scan.Response?.GetEncodedPaginationToken() ?? string.Empty;
- New overload for
-
Comprehensive FluentResults API Integration - Complete Result pattern alternative to traditional async/exception-based API
- New
GetItemAsyncResult(),PutAsyncResult(),UpdateAsyncResult(),DeleteAsyncResult()extension methods for all CRUD operations - New
ToListAsyncResult(),ToCompositeEntityAsyncResult(),ToCompositeEntityListAsyncResult()for query and scan operations - New
ExecuteAsyncResult()for batch operations (BatchGetBuilder,BatchWriteBuilder,BatchPartiQLBuilder) - New
ExecuteAsyncResult()for transaction operations (TransactionWriteBuilder,TransactionGetBuilder) - New
ExecuteAndMapAsyncResult<T1,...,T8>()tuple methods for batch get operations - New
SpatialQueryAsyncResult()for geospatial proximity and bounding box queries - Comprehensive error type hierarchy with
DynamoDbErrorbase class and typed errors:- Transaction errors:
TransactionCancelledError,TransactionConflictError,TransactionInProgressError,OptimisticLockingError - Configuration errors:
MissingClientError,ClientMismatchError,EmptyOperationError,OperationLimitExceededError - Mapping errors:
MappingError,DiscriminatorMismatchError,ProjectionValidationError,ExpressionTranslationError - Validation errors:
SchemaValidationError,EmptyCollectionError,FormatStringError - Storage errors:
BlobStorageError,EncryptionError,DecryptionError - Geospatial errors:
SpatialQueryError,InvalidCoordinatesError,InvalidBoundingBoxError
- Transaction errors:
DynamoDbErrors.FromException()factory method maps all AWS SDK and library exceptions to typed errorsUnprocessedItemsWarningandBatchStatementErrorWarningfor partial success scenarios in batch operations- All errors include
ErrorCodeproperty for programmatic error handling - Cancellation tokens properly re-throw
OperationCanceledExceptionwithout wrapping - Requirements: 1.1-1.5, 2.1-2.7, 3.1-3.3, 4.1-4.5, 5.1-5.5, 6.1-6.5, 7.1-7.4, 10.1-10.3, 11.1-11.3, 12.1-12.2, 14.1-14.17, 16.1-16.3
Usage:
using Oproto.FluentDynamoDb.FluentResults; // Traditional (throws exceptions) var user = await table.Users.Get(userId).GetItemAsync(); // FluentResults (returns Result<T>) var result = await table.Users.Get(userId).GetItemAsyncResult(); if (result.IsSuccess) var user = result.Value; else foreach (var error in result.Errors.OfType<DynamoDbError>()) Console.WriteLine($"[{error.ErrorCode}]: {error.Message}");
- New
-
[UseFluentResults]Attribute - Source generator attribute for opt-in FluentResults API generation- Apply to entity classes to generate Result-returning convenience methods on entity accessors
HideGeneratedAsyncMethodsproperty (default:true) controls whether traditional async methods are suppressed- Generated methods:
GetAsyncResult(),PutAsyncResult(),DeleteAsyncResult(),QueryAsyncResult() - Requirements: 8.1-8.5, 9.1-9.5
Usage:
[DynamoDbTable("Users")] [UseFluentResults] public partial class User { ... } // Generated convenience methods: var result = await table.Users.GetAsyncResult(userId); var result = await table.Users.PutAsyncResult(user); var result = await table.Users.QueryAsyncResult(x => x.TenantId == tenantId);
-
Response metadata via
.Responseproperty - Request builders now expose a.Responseproperty after execution containing operation metadata. This keeps IntelliSense clean during request building while providing access to response details.QueryOperationResponse- LastEvaluatedKey, ScannedCount, ResultCount, ConsumedCapacity, HasMorePagesScanOperationResponse- LastEvaluatedKey, ScannedCount, ResultCount, ConsumedCapacity, HasMorePagesGetItemOperationResponse- ConsumedCapacity, ResponseMetadataPutItemOperationResponse- ConsumedCapacity, ResponseMetadata, ItemCollectionMetricsUpdateItemOperationResponse- ConsumedCapacity, ResponseMetadata, ItemCollectionMetricsDeleteItemOperationResponse- ConsumedCapacity, ResponseMetadata, ItemCollectionMetrics
Usage:
var builder = table.Query<User>().Where(x => x.Pk == tenantId); var users = await builder.ToListAsync(); // Access response metadata var lastKey = builder.Response?.LastEvaluatedKey; var scannedCount = builder.Response?.ScannedCount; var hasMore = builder.Response?.HasMorePages ?? false;
-
DynamicEntity and DynamicTable - Schema-less access to any DynamoDB table without defining entity classes
- New
DynamicEntityclass where all attributes are stored inDynamicFieldscollection - New
DynamicTableclass for working withDynamicEntityinstances DynamicTableKeyOptionsfor configuring partition key and sort key names/types- Typed key methods (
GetAsync(string pk),GetAsync(string pk, string sk), etc.) when key options configured - Raw
AttributeValuekey methods always available for maximum flexibility - Full Query and Scan support with lambda expressions using
DynamicFieldsindexer - Expression translator skips key validation for
DynamicEntityto allow flexible key conditions - Ideal for schema exploration, migration tools, and truly schema-less scenarios
- Requirements: 5.1-5.8, 7.1-7.6, 8.1-8.6
Usage:
// Create DynamicTable with key configuration var keyOptions = new DynamicTableKeyOptions { PartitionKeyName = "pk", PartitionKeyType = ScalarAttributeType.S, SortKeyName = "sk", SortKeyType = ScalarAttributeType.S }; var table = new DynamicTable(client, "my-table", keyOptions); // Get item with typed keys var item = await table.GetAsync("USER#123", "PROFILE"); var name = item?.DynamicFields.GetString("name"); // Query with lambda expressions var items = await table.Query() .Where(x => x.DynamicFields["pk"] == "USER#123") .ToListAsync(); // Put item var entity = new DynamicEntity(); entity.DynamicFields.SetString("pk", "USER#456"); entity.DynamicFields.SetString("sk", "PROFILE"); entity.DynamicFields.SetString("name", "Jane Doe"); await table.PutAsync(entity);
- New
-
PartiQL Support - SQL-like query capability with entity hydration
- New
PartiQLRequestBuilder<TEntity>following the same pattern as other request builders ExecutePartiQL<TEntity>(statement, params)method on table classes- Format string placeholders with format specifiers (
{0},{0:o}for ISO 8601 dates) ToListAsync()for SELECT queries with entity hydrationToCompoundEntityAsync()for compound entity table resultsExecuteAsync()for INSERT/UPDATE/DELETE statementsToRequest()to access underlyingExecuteStatementRequest- Response metadata (
ResponseMetadata,ConsumedCapacity) accessible after execution - Batch PartiQL via
DynamoDbBatch.PartiQLwithBatchPartiQLBuilderandBatchPartiQLResponse ExecuteAndMapAsync<T1>()throughExecuteAndMapAsync<T1...T8>()tuple convenience methods- Requirements: 3.1-3.6
Usage:
// SELECT query with hydration var users = await table.ExecutePartiQL<User>( "SELECT * FROM Users WHERE pk = {0}", "USER#123") .ToListAsync(); // SELECT with DateTime formatting var recentOrders = await table.ExecutePartiQL<Order>( "SELECT * FROM Orders WHERE pk = {0} AND created > {1:o}", "ORDER#456", DateTime.UtcNow.AddDays(-7)) .ToListAsync(); // INSERT/UPDATE/DELETE await table.ExecutePartiQL<User>( "UPDATE Users SET name = {0} WHERE pk = {1}", "Jane Doe", "USER#123") .ExecuteAsync(); // Batch PartiQL var (user, order) = await DynamoDbBatch.PartiQL .Add(table.ExecutePartiQL<User>("SELECT * FROM Users WHERE pk = {0}", "USER#123")) .Add(table.ExecutePartiQL<Order>("SELECT * FROM Orders WHERE pk = {0}", "ORDER#456")) .ExecuteAndMapAsync<User, Order>();
- New
-
Direct SDK Request Passing - Accept native AWS SDK request objects with response hydration
WithRequest()method on all request builders to inject pre-built SDK requests- Table-level convenience methods:
Get(GetItemRequest),Query(QueryRequest),Scan(ScanRequest), etc. - Async convenience methods:
GetAsync(GetItemRequest),QueryAsync(QueryRequest), etc. - Response metadata accessible on builder after execution
DynamoDbTransactions.WriteAsync(client, TransactWriteItemsRequest)for direct transaction executionDynamoDbTransactions.GetAsync(client, TransactGetItemsRequest)for direct transaction getsDynamoDbBatch.WriteAsync(client, BatchWriteItemRequest)for direct batch writesDynamoDbBatch.GetAsync(client, BatchGetItemRequest)for direct batch gets- Requirements: 4.1-4.9
Usage:
// Inject pre-built SDK request var sdkRequest = new GetItemRequest { TableName = "users", Key = new Dictionary<string, AttributeValue> { ["pk"] = new AttributeValue { S = "USER#123" } } }; var user = await table.Users.GetAsync(sdkRequest); // Builder pattern for response metadata access var builder = table.Users.Get(sdkRequest); var user = await builder.GetItemAsync(); var capacity = builder.Response?.ConsumedCapacity; // Direct transaction execution await DynamoDbTransactions.WriteAsync(client, existingTransactWriteRequest);
-
Table Creation from Metadata - Create DynamoDB tables programmatically from entity metadata for integration testing
- New
TableCreatorclass withCreateAsyncandBuildCreateTableRequestmethods - New
TableCreationOptionsclass for configuring billing mode, throughput, TTL, and wait behavior - New
TableCreationResultclass with table information (TableName, TableArn, TableStatus, TtlEnabled) - New
ProvisionedThroughputConfigclass for PROVISIONED billing mode configuration - Source-generated static
CreateTableAsyncmethod on table classes - Supports primary key (partition + optional sort key) configuration
- Supports Global Secondary Index (GSI) creation with projection types (ALL, KEYS_ONLY, INCLUDE)
- Supports Local Secondary Index (LSI) creation with projection types
- Supports TTL enablement when entity metadata defines TTL attribute
- Supports PAY_PER_REQUEST (default) and PROVISIONED billing modes
- Configurable wait-for-active behavior with timeout and polling interval
- Complements
ValidateSchemaAsyncfor complete table lifecycle management - Requirements: 1.1-1.5, 2.1-2.6, 3.1-3.5, 4.1-4.3, 5.1-5.4, 6.1-6.2, 7.1-7.3
Usage:
using Oproto.FluentDynamoDb.Provisioning; // Using generated static method (recommended for integration tests) var result = await UsersTable.CreateTableAsync(client, "test-users-table"); // With options var result = await UsersTable.CreateTableAsync(client, "test-users-table", new TableCreationOptions { EnableTtl = true, WaitForActive = true, WaitTimeout = TimeSpan.FromSeconds(60) }); // Using TableCreator directly var creator = new TableCreator(); var result = await creator.CreateAsync(client, "my-table", User.GetEntityMetadata()); // Inspect request before execution var request = creator.BuildCreateTableRequest("my-table", User.GetEntityMetadata());
- New
-
Schema Validation - Runtime validation of DynamoDB table schemas against entity metadata
- New
ValidateSchemaAsync(IAmazonDynamoDB, SchemaValidationOptions?)method generated on table classes - Validates primary key configuration (partition key name/type, sort key name/type/presence)
- Validates Global Secondary Index configuration (existence, key names, key types)
- Validates Local Secondary Index configuration (existence, sort key name/type)
- Validates TTL configuration (enabled status, attribute name)
- Validates index projection compatibility with projection models
SchemaValidationResultwithIsValid,Errors, andWarningspropertiesThrowOnError()method to throwSchemaValidationExceptionwhen validation failsLogResults(IDynamoDbLogger)method for logging validation resultsSchemaValidationOptionswithStrictnesssetting (Relaxed/Strict)- Comprehensive error codes (
SchemaValidationErrorCode) for programmatic handling - Warning codes (
SchemaValidationWarningCode) for non-critical differences - Designed for startup-time validation (e.g., Lambda cold start) for fail-fast behavior
- Requirements: 1.1-1.5, 2.1-2.6, 3.1-3.6, 4.2-4.6, 5.1-5.3, 6.1-6.4, 8.1-8.5, 9.1-9.4
Usage:
// Basic validation var result = await UsersTable.ValidateSchemaAsync(dynamoDbClient); if (!result.IsValid) { foreach (var error in result.Errors) Console.WriteLine($"Error: {error.Message}"); } // Fail-fast validation var result = await UsersTable.ValidateSchemaAsync(dynamoDbClient); result.ThrowOnError(); // Throws SchemaValidationException if errors exist // Strict validation (missing projection models are errors) var options = new SchemaValidationOptions { Strictness = ValidationStrictness.Strict }; var result = await UsersTable.ValidateSchemaAsync(dynamoDbClient, options); // Log validation results result.LogResults(logger);
- New
-
Dynamic Fields Support - Capture and work with DynamoDB attributes not explicitly defined in entity classes
- New
[EnableDynamicFields]attribute to opt-in to dynamic field capture on entities - New
DynamicFieldCollectionclass with typed accessors for common types (string, int, long, double, decimal, bool, DateTime, DateTimeOffset, byte[]) - New
DynamicFieldTypeenum for runtime type detection of dynamic fields - New
DynamicFieldTypeExceptionfor type mismatch errors with descriptive messages - Source generator automatically adds
DynamicFieldsproperty to entities with[EnableDynamicFields] FromDynamoDbcaptures unmapped attributes intoDynamicFieldscollectionToDynamoDbincludes dynamic fields in serialized output (mapped properties take precedence)- Expression translator support for dynamic field filtering:
x.DynamicFields["fieldName"] == value - Support for typed comparisons in expressions:
x.DynamicFields["score"] > 100 Exists()andNotExists()methods for attribute existence checks in expressions- Update expression support via
DynamicFieldsproperty on update models withDynamicFieldCollection - Dynamic field values redacted in logs by default (configurable via
SensitiveLoggingproperty) - Full AOT compatibility with no reflection at runtime
- Change Tracking - Automatic tracking of dynamic field modifications for efficient updates
DynamicFieldCollectionautomatically tracks changes after entity deserializationChangesOnly()method returns a new collection with only added/modified fields and tracked removalsChangesOnly(resetTracking: false)preserves tracking for retry scenariosResetChangeTracking()method to manually clear all tracked changesHasChangesproperty to check if any modifications have been madeRemovedFieldsproperty provides access to fields marked for removal- Generated update models include nullable
DynamicFieldsproperty for lambda expression updates - Null
DynamicFieldsin update model leaves existing dynamic fields unchanged - Requirements: 11.1-11.7, 12.1-12.4
- Requirements: 1.1-1.4, 2.1-2.5, 3.1-3.4, 4.1-4.4, 5.1-5.4, 6.1-6.4, 7.1-7.3, 8.1-8.4, 9.1-9.3, 10.1-10.3
Usage:
// Enable dynamic fields on entity [DynamoDbTable("products")] [EnableDynamicFields] public partial class Product { [PartitionKey] [DynamoDbAttribute("pk")] public string ProductId { get; set; } = string.Empty; } // Read dynamic fields with typed accessors var product = await table.Products.GetAsync(productId); var color = product.DynamicFields.GetString("color"); var weight = product.DynamicFields.GetInt("weight_grams"); // Write dynamic fields product.DynamicFields.SetString("material", "Cotton"); product.DynamicFields.SetInt("size_us", 10); await table.Products.PutAsync(product); // Update dynamic fields using change tracking product.DynamicFields.SetDecimal("sale_price", 24.99m); product.DynamicFields.Remove("temporary_note"); await table.Products.Update(productId) .Set(x => new ProductUpdateModel { DynamicFields = product.DynamicFields.ChangesOnly() }) .UpdateAsync(); // Filter by dynamic fields var blueProducts = await table.Products.Scan() .WithFilter(x => x.DynamicFields["color"] == "Blue") .ToListAsync(); // Check field existence var productsWithWarranty = await table.Products.Scan() .WithFilter(x => x.DynamicFields.Exists("warranty_months")) .ToListAsync();
- New
-
[LocalSecondaryIndex]Attribute - Support for Local Secondary Index definitions in entity metadata- New
[LocalSecondaryIndex(indexName)]attribute for marking LSI sort key properties - LSIs share the same partition key as the base table
IndexTypeenum to distinguish between GSI and LSI inIndexMetadata- Source generator emits correct
IndexTypefor both GSI and LSI attributes - Schema validation validates LSI configuration against actual table
- Requirements: 4.1, 7.1, 7.2
Usage:
[DynamoDbTable("orders")] public partial class Order { [PartitionKey] [DynamoDbAttribute("pk")] public string CustomerId { get; set; } = string.Empty; [SortKey] [DynamoDbAttribute("sk")] public string OrderId { get; set; } = string.Empty; // Local Secondary Index - shares partition key with base table [LocalSecondaryIndex("orders-by-date")] [DynamoDbAttribute("order_date")] public string OrderDate { get; set; } = string.Empty; }
- New
-
Enhanced
IndexMetadata- Additional metadata for schema validation and toolingIndexTypeproperty to distinguish GSI from LSIPartitionKeyAttributeNameandPartitionKeyAttributeTypepropertiesSortKeyAttributeNameandSortKeyAttributeTypepropertiesProjectionTypeproperty (All, KeysOnly, Include)HasProjectionModelproperty indicating if a projection model is defined- Requirements: 7.1, 7.2, 10.1, 10.2, 10.3
-
Enhanced
EntityMetadata- Additional metadata for schema validationPartitionKeyAttributeNameandPartitionKeyAttributeTypepropertiesSortKeyAttributeNameandSortKeyAttributeTypepropertiesTtlAttributeNameproperty for TTL configuration- Requirements: 2.1, 2.2, 5.1, 5.2
-
[RequireWriteTransaction]Attribute - New attribute to enforce transactional writes for specific entity types- Marks entity classes as requiring write operations within a transaction
- Put, Update, Delete, and BatchWrite operations throw
InvalidOperationExceptionwhen attempted outside a transaction - TransactWrite operations are allowed and proceed normally
RequiresWriteTransactionstatic property added toIDynamoDbEntityinterfaceRequiresWriteTransactionproperty added toEntityMetadatafor tooling support- Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6
Usage:
[DynamoDbTable("FinancialTransactions")] [RequireWriteTransaction] public partial class Transaction { // Properties... } // This throws InvalidOperationException: await table.Transactions.Put(transaction).PutAsync(); // This is allowed: await DynamoDbTransactions.Write() .Put(table.Transactions, transaction) .ExecuteAsync();
-
Default Request Options in FluentDynamoDbOptions - Configure common request settings once and apply to all operations
UseConsistentRead(bool)- Default consistent read setting for Get and Query operationsReturnConsumedCapacity(ReturnConsumedCapacity)- Default consumed capacity reporting for all operationsReturnItemCollectionMetrics(ReturnItemCollectionMetrics)- Default item collection metrics for write operationsReturnValues(ReturnValue)- Default return values for Put, Update, and Delete operations- Explicit builder method calls override default options
- Method names match existing request builder methods for consistency
- Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6
Usage:
var options = new FluentDynamoDbOptions() .UseConsistentRead(true) .ReturnConsumedCapacity(ReturnConsumedCapacity.TOTAL) .ReturnValues(ReturnValue.ALL_NEW); var table = new UsersTable(client, "users", options); // All operations now use these defaults var user = await table.Users.Get(userId).GetItemAsync(); // Uses consistent read
-
Custom Namespace Support for Generated Table Classes - Control the namespace of generated table classes
- New
Namespaceproperty on[DynamoDbTable]attribute - When specified, generated table class is placed in the custom namespace
- When not specified, uses the entity's namespace (existing behavior)
- Appropriate using directives generated for referenced types
- Requirements: 2.1, 2.2, 2.3
Usage:
[DynamoDbTable("users", Namespace = "MyApp.Data.Tables")] public partial class User { // Entity stays in its declared namespace } // Generated UsersTable class is in MyApp.Data.Tables namespace
- New
-
New Example Applications - Three new example applications demonstrating advanced features
- JsonBlobDemo - Demonstrates JSON blob serialization with System.Text.Json (AOT and reflection modes) and Newtonsoft.Json
- S3BlobDemo - Demonstrates S3 blob storage integration with
[BlobReference]attribute for storing large data externally - EncryptionDemo - Demonstrates field encryption with
[Encrypted]attribute and sensitive data logging with[Sensitive]attribute - All examples follow the established pattern with interactive console menus and comprehensive README documentation
- Requirements: 2.1-2.8, 3.1-3.8, 4.1-4.11
-
TransactionDemo RequireWriteTransaction Demonstration - Updated TransactionDemo example to showcase the
[RequireWriteTransaction]attribute- New
FinancialTransactionentity demonstrating transactional write enforcement - Interactive demonstration showing direct write failure and transactional write success
- Requirements: 5.1-5.6
- New
-
Blob Storage Redesign - Complete redesign of the blob storage feature with improved semantics, lazy loading, and failure handling strategies
- New
[BlobStorage]attribute replaces[BlobReference]with clearer semantics - New
BlobData<T>wrapper type for lazy/eager loading control - New
IBlobStorageStrategyinterface for coordinating failure handling between blob storage and DynamoDB operations - Built-in
BestEffortCleanupStrategy(default) - attempts to clean up orphaned blobs on DynamoDB write failure - Built-in
NoCleanupStrategy- simple implementation for non-critical data where orphaned blobs are acceptable LazyLoadproperty on[BlobStorage]attribute to defer blob loading until explicitly requestedBlobData<T>.LoadAsync()method for explicit lazy loadingBlobData<T>.Value,ReferenceKey,IsLoaded,HasPendingDataproperties for state inspectionBlobData<T>.Create(T value)factory method for creating instances with data to be storedBlobStoreOptionsclass for optional metadata (ContentType, Metadata, Tags)BlobStorageExceptionfor blob storage operation failures- Integration with
[JsonBlob]attribute for JSON serialization before blob upload - Integration with
[Encrypted]attribute for encryption before blob upload - Integration with
[Sensitive]attribute for log redaction - Automatic strategy lifecycle invocation in Put, Update, Delete, Batch, and Transaction operations
FluentDynamoDbOptions.WithBlobStorageStrategy(IBlobStorageStrategy)for custom strategy configuration- Requirements: 1.1-1.3, 2.1-2.7, 3.1-3.4, 4.1-4.6, 5.1-5.4, 6.1-6.3, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.3, 11.1-11.5, 12.1-12.6, 13.1-13.4
Usage:
// Entity definition with BlobStorage [DynamoDbTable("documents")] public partial class Document { [PartitionKey] [DynamoDbAttribute("pk")] public string Id { get; set; } = string.Empty; // Eager loading (default) - data loaded during deserialization [BlobStorage] [DynamoDbAttribute("content")] public BlobData<byte[]> Content { get; set; } = default!; // Lazy loading - data loaded on explicit LoadAsync() call [BlobStorage(LazyLoad = true)] [DynamoDbAttribute("thumbnail")] public BlobData<byte[]> Thumbnail { get; set; } = default!; } // Configuration var options = new FluentDynamoDbOptions() .WithBlobStorage(new S3BlobProvider(s3Client, "my-bucket")) .WithBlobStorageStrategy(new BestEffortCleanupStrategy(provider)); // Optional, this is the default var table = new DocumentTable(dynamoDbClient, "documents", options); // Creating and storing blob data var document = new Document { Id = "doc-123", Content = BlobData<byte[]>.Create(fileBytes), Thumbnail = BlobData<byte[]>.Create(thumbnailBytes) }; await table.Documents.PutAsync(document); // Retrieving with eager loading var loaded = await table.Documents.GetAsync("doc-123"); var content = loaded.Content.Value; // Already loaded // Retrieving with lazy loading var lazyLoaded = await table.Documents.GetAsync("doc-123"); await lazyLoaded.Thumbnail.LoadAsync(); // Explicit load var thumbnail = lazyLoaded.Thumbnail.Value;
- New
-
DynamoDbTableBase Removed - Table classes are now fully source-generated without inheritance
- Generated table classes no longer inherit from
DynamoDbTableBase - All functionality (Client, Name, Options, Query, Get, Put, Update, Delete) is now generated directly
- Entity accessor visibility controlled by
[GenerateAccessors]attribute - Index accessors generated for all defined GSIs and LSIs
- Breaking Change: Code that directly references
DynamoDbTableBasewill no longer compile - No Migration Required: Code using generated table classes continues to work unchanged
- Requirements: 1.1-1.6
Migration (only if directly referencing DynamoDbTableBase):
// Before: Direct reference to base class (rare) public void ProcessTable(DynamoDbTableBase table) { ... } // After: Use the generated table class directly public void ProcessTable(UsersTable table) { ... } // Or use duck typing / interfaces if needed
- Generated table classes no longer inherit from
-
Logging Runtime Configuration - Removed
DISABLE_DYNAMODB_LOGGINGconditional compilation in favor of runtime configuration- All
#if !DISABLE_DYNAMODB_LOGGINGpreprocessor directives removed from source code - Logging is now controlled entirely via
FluentDynamoDbOptions.WithLogger()at runtime NoOpLogger.Instanceprovides zero-overhead logging when disabled (viaIsEnabled()check pattern)- Users no longer need to recompile to enable/disable logging
- Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6
Migration:
// Before: Compile-time conditional (no longer supported) // <DefineConstants>DISABLE_DYNAMODB_LOGGING</DefineConstants> // After: Runtime configuration // Logging disabled (default - zero overhead) var table = new UsersTable(client, "users"); // Logging enabled var options = new FluentDynamoDbOptions() .WithLogger(loggerFactory.ToDynamoDbLogger<UsersTable>()); var table = new UsersTable(client, "users", options);
- All
-
Documentation Reorganization - Split
STSIntegration.mdinto two focused documents for better discoverability- New
docs/advanced-topics/ClientConfiguration.mdcovering development environments (DynamoDB Local, LocalStack), custom client settings, multi-region deployments, and proxy configuration - New
docs/advanced-topics/ScopedSecurity.mdcoveringWithClient()method for per-request client customization, STS-scoped credentials, and multi-tenancy patterns - Deleted
docs/advanced-topics/STSIntegration.mdafter content migration - Updated
docs/advanced-topics/README.mdto reflect new document structure
- New
-
Example Projects Configuration Pattern - Refactored all example projects (TodoList, TransactionDemo, InvoiceManager, StoreLocator) to follow the recommended configuration pattern
- Configuration now built once at application level in Program.cs
- Table names passed explicitly to constructors for runtime configurability
- Removed redundant custom table classes that only contained constructors
- StoreLocator retains utility methods (
SelectS2Level,SelectH3Resolution) while removing constructor boilerplate - Updated README files to reflect new project structure and patterns
-
Encrypted Properties Automatically Marked as Sensitive - Properties with
[Encrypted]attribute now automatically haveIsSensitive = true- No need to apply both
[Encrypted]and[Sensitive]attributes - Encrypted property values are automatically redacted in logs
- Existing entities with both attributes continue to work without changes
- Requirements: 1.1, 1.2, 1.3
- No need to apply both
-
Query Capabilities Derived from Key Attributes -
SupportedOperationsnow automatically derived from key metadata- Partition key properties: Support
Equalsoperation - Sort key properties: Support
Equals,BeginsWith,Between,GreaterThan,LessThanoperations - No longer need to manually specify operations via
[Queryable]attribute - Requirements: 3.2, 3.3, 3.4
- Partition key properties: Support
-
Write Request Builder Type Constraints -
PutItemRequestBuilder<TEntity>,UpdateItemRequestBuilder<TEntity>, andDeleteItemRequestBuilder<TEntity>now requireTEntity : class, IDynamoDbEntity- Enables AOT-safe transaction validation via static interface members
- Existing code using entities that implement
IDynamoDbEntity(all source-generated entities) is unaffected - Requirements: 4.2, 4.3, 4.4
-
[Queryable]Attribute - Deprecated in favor of automatic derivation from key attributes- Compiler warning
DYNDB103emitted when[Queryable]is used - Query capabilities are now derived from
[PartitionKey]and[SortKey]attributes - Will be removed in v1.0
- Requirements: 3.1
Migration:
// Before (deprecated): [Queryable(SupportedOperations = new[] { DynamoDbOperation.Equals })] [PartitionKey] [DynamoDbAttribute("pk")] public string UserId { get; set; } // After (automatic): [PartitionKey] // Automatically supports Equals [DynamoDbAttribute("pk")] public string UserId { get; set; }
- Compiler warning
-
[BlobReference]Attribute - Deprecated in favor of[BlobStorage]withBlobData<T>wrapper type- Compiler warning
DYNDB104emitted when[BlobReference]is used - New
[BlobStorage]attribute provides clearer semantics and lazy/eager loading control - New
BlobData<T>wrapper type encapsulates blob storage behavior - Will be removed in v1.0
- Requirements: 1.2
Migration:
// Before (deprecated): [BlobReference(BlobProvider.S3)] [DynamoDbAttribute("data")] public byte[] Data { get; set; } // After (new pattern): [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;
- Compiler warning
- AwsEncryptionSdkOptions Data Key Caching Properties - Removed non-functional properties from
AwsEncryptionSdkOptions- Removed
DefaultCacheTtlSeconds,MaxMessagesPerDataKey,MaxBytesPerDataKey, andCacheEntryCapacity - The AWS Encryption SDK for .NET does not support data key caching; these properties had no effect
EnableCaching(keyring object caching) andAlgorithmproperties are retainedEncryptedAttribute.CacheTtlSecondsandFieldEncryptionContext.CacheTtlSecondson the core library interface are unchanged
- Removed
-
GeoHash Query Bug - Fixed interpolated string bug in GeoHash BETWEEN queries
- Changed
$"geohash_cell BETWEEN {0} AND {1}"to"geohash_cell BETWEEN {0} AND {1}"in StoreLocator example - The
$prefix caused{0}and{1}to be treated as literal text instead of format placeholders - This resulted in "Invalid KeyConditionExpression" errors when executing GeoHash spatial queries
- Requirements: 6.1, 6.2, 6.3, 6.4
- Changed
-
Documentation API Pattern Corrections - Fixed incorrect batch and transaction operation examples across documentation
- Corrected batch operation examples to use
DynamoDbBatch.WriteandDynamoDbBatch.Getstatic entry points instead of constructor-based patterns - Corrected transaction operation examples to use
DynamoDbTransactions.WriteandDynamoDbTransactions.Getstatic entry points instead of constructor-based patterns - Corrected
CommitAsync()toExecuteAsync()for transaction execution - Files corrected:
BasicOperations.md,PerformanceOptimization.md,GlobalSecondaryIndexes.md,CompositeEntities.md,MultiEntityTables.md,QUICK_REFERENCE.md,DeveloperGuide.md - See
docs/DOCUMENTATION_CHANGELOG.mdfor detailed before/after patterns
- Corrected batch operation examples to use
-
Documentation API Style Corrections - Fixed incorrect API method names and patterns
- Corrected
ExecuteAsync<T>()toGetItemAsync<T>()on GetItemRequestBuilder in multiple files - Corrected non-existent
DynamoDbTableBase<T>generic type to use concrete typed table classes with entity accessors - Updated migration examples to use lambda expressions and entity accessor patterns
- Files corrected:
AdvancedTypesMigration.md,CodeExamples.md,PerformanceOptimization.md,AdoptionGuide.md - See
docs/DOCUMENTATION_CHANGELOG.mdPart 6 for detailed before/after patterns
- Corrected
-
NuGet Package Contents - Fixed packaging to exclude test and example projects
- Added
<IsPackable>false</IsPackable>to all unit test projects - Added
<IsPackable>false</IsPackable>to integration test project - Added
<IsPackable>false</IsPackable>to example projects - Added
<IsPackable>false</IsPackable>to AOT and API consistency test projects - Removed duplicate icon files from test/example project directories
- Requirements: 6.1, 6.2, 6.3, 6.4
- Added
-
HydratorGenerator Parameter Mismatch - Fixed source generator bug causing compilation errors for entities with
[BlobReference]attributesHydratorGeneratorwas generating calls toFromDynamoDbAsyncandToDynamoDbAsyncwith incorrect parameter order- Named parameters were incorrectly used, causing type mismatch errors (e.g.,
FluentDynamoDbOptionspassed whereIFieldEncryptor?expected) - Fixed by using positional parameters matching the generated method signatures
- Affected file:
Oproto.FluentDynamoDb.SourceGenerator/Generators/HydratorGenerator.cs
0.8.0 - 2025-12-05 (Preview Release)
Note: This is the first public preview release of Oproto.FluentDynamoDb. While the core features are production-ready, some experimental features are still evolving.
Production-ready (core focus)
- Strongly-typed entity modeling and repositories
- Single-table, multi-entity patterns (e.g., Invoice + Lines)
- Query and scan builders (LINQ-style expressions)
- Batch operations and transactional helpers
- Source generation (no reflection, AOT-friendly)
Experimental / evolving
- Geospatial indexing (GeoHash, S2, H3)
- S3-backed blob storage
- KMS-based field encryption
- Attributes for defining Entities may evolve over time
These experimental features are available for early testing and feedback, but may change shape before 1.0 and do not yet have full demo coverage or documentation.
-
Namespace Reorganization - Internal types moved from
Oproto.FluentDynamoDb.Storageto dedicated namespaces for better separation of concernsIDynamoDbEntity,IProjectionModel,IDiscriminatedProjectionmoved toOproto.FluentDynamoDb.EntitiesEntityMetadata,PropertyMetadata,RelationshipMetadata,IndexMetadata,IEntityMetadataProvidermoved toOproto.FluentDynamoDb.MetadataIAsyncEntityHydrator,IEntityHydratorRegistry,DefaultEntityHydratorRegistrymoved toOproto.FluentDynamoDb.HydrationIFieldEncryptor,FieldEncryptionContextmoved toOproto.FluentDynamoDb.Providers.EncryptionIBlobStorageProvider,IJsonBlobSerializermoved toOproto.FluentDynamoDb.Providers.BlobStorageMappingErrorHandler,DynamoDbMappingException,DiscriminatorMismatchException,ProjectionValidationException,FieldEncryptionExceptionmoved toOproto.FluentDynamoDb.MappingDynamoDbOperationContext,DynamoDbOperationContextDiagnostics,OperationContextDatamoved toOproto.FluentDynamoDb.ContextOproto.FluentDynamoDb.Storagenamespace now contains only physical storage abstractions:DynamoDbTableBase,DynamoDbIndex,IDynamoDbTable- Requirements: 1.1, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.3, 7.1-7.3, 8.1-8.3, 9.1-9.4, 10.1-10.3, 11.1
Migration:
// Before: All types in Storage namespace using Oproto.FluentDynamoDb.Storage; // After: Import specific namespaces as needed using Oproto.FluentDynamoDb.Entities; // IDynamoDbEntity, IProjectionModel, IDiscriminatedProjection using Oproto.FluentDynamoDb.Metadata; // EntityMetadata, PropertyMetadata, etc. using Oproto.FluentDynamoDb.Hydration; // IAsyncEntityHydrator, IEntityHydratorRegistry using Oproto.FluentDynamoDb.Providers.Encryption; // IFieldEncryptor, FieldEncryptionContext using Oproto.FluentDynamoDb.Providers.BlobStorage; // IBlobStorageProvider, IJsonBlobSerializer using Oproto.FluentDynamoDb.Mapping; // MappingErrorHandler, exceptions using Oproto.FluentDynamoDb.Context; // DynamoDbOperationContext using Oproto.FluentDynamoDb.Storage; // DynamoDbTableBase, DynamoDbIndex (unchanged)
-
JSON Serializer Runtime Configuration -
[JsonBlob]properties now require runtime configuration instead of compile-time assembly attributesIDynamoDbEntityinterface methodsToDynamoDbandFromDynamoDbnow acceptFluentDynamoDbOptions?instead ofIDynamoDbLogger?- Removed
[assembly: DynamoDbJsonSerializer]attribute - no longer supported - Removed
JsonSerializerTypeenum - no longer needed [JsonBlob]properties now require.WithSystemTextJson()or.WithNewtonsoftJson()onFluentDynamoDbOptionsat runtime- Clear runtime exception thrown when
[JsonBlob]property is used without configured serializer - Requirements: 1.1, 1.2, 6.1, 6.2
Migration:
// Before: Compile-time assembly attribute (no longer supported) [assembly: DynamoDbJsonSerializer(JsonSerializerType.SystemTextJson)] // After: Runtime configuration via FluentDynamoDbOptions var options = new FluentDynamoDbOptions() .WithSystemTextJson(); // Or .WithNewtonsoftJson() var table = new DocumentTable(client, "Documents", options);
Custom Serializer Options:
// System.Text.Json with custom options var options = new FluentDynamoDbOptions() .WithSystemTextJson(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }); // Newtonsoft.Json with custom settings var options = new FluentDynamoDbOptions() .WithNewtonsoftJson(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include, DateFormatString = "yyyy-MM-dd" }); // System.Text.Json with AOT-compatible JsonSerializerContext var options = new FluentDynamoDbOptions() .WithSystemTextJson(MyJsonContext.Default);
-
Scan Opt-In Pattern - Scan operations now require explicit opt-in via
[Scannable]attribute- Removed generic
Scan<TEntity>()method fromDynamoDbTableBaseto prevent accidental table scans - Scan operations are expensive and not a recommended DynamoDB access pattern
- Entities must now have
[Scannable]attribute to enable Scan operations - Use entity accessor
table.Entitys.Scan()ortable.Scan()for default entity - Generic
table.Scan<TEntity>()method is still available when entity has[Scannable]attribute - Requirements: 1.1
Migration:
// Before: Scan was always available var allOrders = await table.Scan<Order>().ToListAsync(); // After: Add [Scannable] attribute to entity [DynamoDbEntity] [Scannable] // Required for Scan operations public partial class Order : IDynamoDbEntity { ... } // Then use entity accessor var allOrders = await table.Orders.Scan().ToListAsync();
- Removed generic
-
IJsonBlobSerializer Interface - New abstraction for JSON serialization of
[JsonBlob]properties with runtime configurationIJsonBlobSerializerinterface in core library withSerialize<T>andDeserialize<T>methodsSystemTextJsonBlobSerializerimplementation inOproto.FluentDynamoDb.SystemTextJsonpackageNewtonsoftJsonBlobSerializerimplementation inOproto.FluentDynamoDb.NewtonsoftJsonpackageWithSystemTextJson()extension method onFluentDynamoDbOptionswith overloads for default, customJsonSerializerOptions, and AOT-compatibleJsonSerializerContextWithNewtonsoftJson()extension method onFluentDynamoDbOptionswith overloads for default and customJsonSerializerSettingsJsonSerializerproperty onFluentDynamoDbOptionsfor accessing configured serializerWithJsonSerializer(IJsonBlobSerializer?)builder method for custom serializer implementations- Full AOT compatibility when using
JsonSerializerContextoverload - Customizable serializer options (camelCase, null handling, date formats, etc.)
- Clear runtime exception with guidance when serializer not configured
- Source generator emits diagnostic warning (DYNDB102) when
[JsonBlob]used without JSON package reference - Requirements: 2.1, 3.1, 3.2, 3.3, 3.4, 4.1, 4.2, 4.3, 5.1, 5.2
Usage Examples:
// Default System.Text.Json configuration var options = new FluentDynamoDbOptions() .WithSystemTextJson(); // Custom System.Text.Json options var options = new FluentDynamoDbOptions() .WithSystemTextJson(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); // AOT-compatible with JsonSerializerContext var options = new FluentDynamoDbOptions() .WithSystemTextJson(MyJsonContext.Default); // Newtonsoft.Json with custom settings var options = new FluentDynamoDbOptions() .WithNewtonsoftJson(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include }); // Use with table var table = new DocumentTable(client, "Documents", options);
-
Lambda Expression Where() for Put and Delete - Type-safe condition expressions for Put and Delete operations
- Added
Where<TEntity>(Expression<Func<TEntity, bool>>)extension method forPutItemRequestBuilder<TEntity> - Added
Where<TEntity>(Expression<Func<TEntity, bool>>)extension method forDeleteItemRequestBuilder<TEntity> - Supports
AttributeExists()andAttributeNotExists()extension methods in lambda expressions - Supports comparison operators (
==,!=,<,>,<=,>=) in lambda expressions - Consistent API across all request builders (Query, Update, Put, Delete)
- Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 4.1, 4.2
Usage Examples:
// Conditional put - only if item doesn't exist await table.Orders.Put(order) .Where(x => x.Pk.AttributeNotExists()) .PutAsync(); // Conditional delete - only if item exists await table.Orders.Delete(pk, sk) .Where(x => x.Pk.AttributeExists()) .DeleteAsync(); // Conditional delete with comparison await table.Orders.Delete(pk, sk) .Where(x => x.Status == "pending") .DeleteAsync();
- Added
-
Scan() Method on Entity Accessors - Type-safe Scan operations via entity accessors
- Generated
Scan()method on entity accessors for entities with[Scannable]attribute - Parameterless
Scan()returningScanRequestBuilder<TEntity> Scan(string, params object[])with filter expressionScan(Expression<Func<TEntity, bool>>)with lambda filter- Requirements: 1.1, 1.2, 4.3
Usage Examples:
// Simple scan var allOrders = await table.Orders.Scan().ToListAsync(); // Scan with lambda filter var activeOrders = await table.Orders.Scan(x => x.Status == "active").ToListAsync(); // Scan with format string filter var recentOrders = await table.Orders.Scan("CreatedAt > {0}", cutoffDate).ToListAsync();
- Generated
-
StoreLocator Adaptive Precision - Multi-precision spatial indexing for the StoreLocator example application
- Automatic precision selection based on search radius for optimal query performance
- S2 precision levels: Level 14 (~284m) for ≤2km, Level 12 (~1.1km) for 2-10km, Level 10 (~4.5km) for >10km
- H3 precision levels: Resolution 9 (~174m) for ≤2km, Resolution 7 (~1.2km) for 2-10km, Resolution 5 (~8.5km) for >10km
- Multi-precision storage: stores now indexed at three precision levels simultaneously
- New GSIs for each precision level (s2-index-fine/medium/coarse, h3-index-fine/medium/coarse)
- Display of precision level and cell size in search results
- Eliminates cell limit errors for searches up to 50km radius
- Property-based tests validating precision selection and multi-precision storage
- Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.4
-
Documentation Overhaul - Comprehensive documentation improvements for accuracy, organization, and maintainability
- New
docs/advanced-topics/InternalArchitecture.mddocumenting internal interfaces, source generator pipeline, and component relationships - New
docs/reference/ApiReference.mdwith express list of all request builders, entity accessors, and direct async methods - New
.kiro/steering/documentation.mdestablishing documentation standards for API style priority, method verification, and attribution - H3 third-party attribution added to
THIRD-PARTY-NOTICES.mdfollowing S2 format with Apache License 2.0 notice - Organization attribution (Oproto Inc, oproto.com, oproto.io, fluentdynamodb.dev, Dan Guisinger) added to README.md and docs/README.md
- Updated
docs/INDEX.mdanddocs/README.mdnavigation with links to new documentation pages - New "Repository Pattern with Table Class" section in
docs/CodeExamples.mddemonstrating how to use table classes as repositories with controlled access using[GenerateAccessors]attribute
- New
-
FluentDynamoDbOptions Configuration Pattern - New centralized configuration object for AOT-compatible service registration
FluentDynamoDbOptionsclass with immutableWith*methods for fluent configurationWithLogger(IDynamoDbLogger)for logging configurationWithBlobStorage(IBlobStorageProvider)for blob storage integrationWithEncryption(IFieldEncryptor)for field-level encryptionAddGeospatial()extension method in Geospatial package for spatial query support- Internal
IEntityHydratorRegistryfor async entity hydration without reflection - Internal
ICollectionFormatterRegistryfor type-safe collection formatting - All services registered at startup, eliminating runtime reflection
- Thread-safe and immutable - safe for concurrent use across multiple tables
- Configuration isolation - each table instance maintains independent configuration
- Default options work for core operations without optional packages
- Requirements: 1.1, 1.2, 1.3, 1.4, 5.1, 5.2, 5.3, 7.2, 7.3, 7.4, 8.1, 8.2, 8.3, 8.5
Usage Examples:
// Configure options with all services var options = new FluentDynamoDbOptions() .WithLogger(new ConsoleLogger()) .WithBlobStorage(new S3BlobStorageProvider(s3Client)) .WithEncryption(new KmsFieldEncryptor(kmsClient)) .AddGeospatial(); // Extension from Geospatial package // Create table with options var table = new UserTable(dynamoDbClient, "users", options); // Or use default options for basic operations var simpleTable = new UserTable(dynamoDbClient, "users", new FluentDynamoDbOptions());
Migration Notes:
- Old constructors accepting individual parameters are deprecated but still work
- Migrate to
FluentDynamoDbOptionspattern for AOT compatibility - See Configuration Guide for migration examples
-
IGeospatialProvider Interface - Abstraction for geospatial operations enabling AOT-compatible spatial queries
IGeospatialProviderinterface in main library for geospatial operationsCreateBoundingBox()methods for radius and coordinate-based bounding boxesGetGeoHashRange(),GetS2CellCovering(),GetH3CellCovering()for cell calculationsGeoBoundingBoxResultstruct for bounding box resultsDefaultGeospatialProviderimplementation in Geospatial package- Eliminates reflection-based geospatial method discovery
- Clear exception messages when geospatial provider not configured
- Requirements: 2.1, 2.2, 2.3, 2.4
-
IAsyncEntityHydrator Interface - Type-safe async entity hydration without reflection
IAsyncEntityHydrator<T>interface for async entity hydrationIEntityHydratorRegistryfor hydrator registration and lookup- Source generator emits hydrator implementations for entities with blob references
- Eliminates
GetMethod()reflection forFromDynamoDbAsyncdiscovery - Automatic fallback to synchronous
FromDynamoDbwhen no hydrator registered - Requirements: 3.1, 3.2, 3.4
-
ICollectionFormatterRegistry Interface - Type-safe collection formatting without Activator.CreateInstance
ICollectionFormatterRegistryinterface for collection formatter registration- Source generator emits type-specific formatters for collection properties with format strings
- Eliminates
Activator.CreateInstancefor generic collection creation - Preserves collection types (HashSet, List, etc.) during formatting
- Requirements: 4.1, 4.2, 4.3, 4.4
-
WithClient Method on Request Builders - Direct
WithClient(IAmazonDynamoDB client)method on all request builders for AOT-compatible client swapping- Available on
QueryRequestBuilder,GetItemRequestBuilder,PutItemRequestBuilder,UpdateItemRequestBuilder,DeleteItemRequestBuilder, andScanRequestBuilder - Returns the same builder instance for fluent chaining
- Replaces reflection-based
WithClientExtensionsfor AOT/trimmer compatibility - Requirements: 5.1, 5.2, 5.4
- Available on
-
S2 and H3 Geospatial Indexing - Advanced spatial indexing support using Google S2 and Uber H3 hierarchical cell systems
- S2 cell encoding/decoding with configurable precision levels (0-30)
- H3 hexagonal cell encoding/decoding with configurable resolutions (0-15)
- Cell covering algorithms for radius and bounding box queries
- Spiral-ordered results (closest cells first) for optimal pagination
- Neighbor cell calculation for comprehensive spatial coverage
- Parent/child cell navigation for hierarchical queries
- International Date Line crossing support with automatic bounding box splitting
- Polar region handling with latitude clamping
- Cell count estimation for query cost prediction
- Hard limits on cell counts to prevent expensive queries (default: 100, max: 500)
S2CellandH3Cellvalue types with bounds, neighbors, parent, and childrenS2CellCoveringandH3CellCoveringfor computing cell coveringsS2EncoderandH3Encoderfor coordinate-to-cell conversion- Extension methods:
ToS2Cell(),ToS2Token(),ToH3Cell(),ToH3Index() - GSI-based spatial query support for efficient DynamoDB queries
- Fluent query extensions:
WithinRadiusS2(),WithinRadiusH3(),WithinBoundingBoxS2(),WithinBoundingBoxH3() - Continuation token support for paginated spatial queries
- Full AOT compatibility for Native AOT deployments
- Comprehensive property-based tests using FsCheck
-
Geospatial Query Support - New
Oproto.FluentDynamoDb.Geospatialpackage for location-based queries using GeoHash- GeoHash encoding/decoding with configurable precision (1-12 characters)
- Efficient proximity queries using GeoHash prefixes
- Bounding box queries for rectangular regions
- Radius-based queries (kilometers, miles, meters)
GeoLocationvalue type for latitude/longitude coordinates with validationGeoBoundingBoxfor defining rectangular search areasGeoHashCellrepresenting GeoHash grid cells with bounds- Extension methods for
GeoLocation:ToGeoHash(),ToGeoHashCell(),FromGeoHash() - Extension methods for
GeoBoundingBox:GetGeoHashCells(),GetGeoHashPrefixes() - Query builder extensions:
WithinRadius(),WithinBoundingBox() - Automatic neighbor cell calculation for boundary queries
- Support for polar regions and international date line
- Precision guide for accuracy vs. performance tradeoffs
- Comprehensive documentation with examples and limitations
- Full AOT compatibility for Native AOT deployments
- Performance optimized: <1 microsecond encoding/decoding
-
Transaction and Batch API Redesign - Reusable request builders for composing transaction and batch operations
- New
DynamoDbTransactions.WriteandDynamoDbTransactions.Getentry points for transaction operations - New
DynamoDbBatch.WriteandDynamoDbBatch.Getentry points for batch operations - Reuse existing request builders (Put, Update, Delete, Get, ConditionCheck) within transaction/batch contexts
- Access to all string formatting, lambda expressions, and source-generated key methods
- Marker interfaces (
ITransactablePutBuilder,ITransactableUpdateBuilder, etc.) for type-safe builder composition - New
ConditionCheckBuilder<TEntity>for transaction condition checks without data modification - Automatic client inference from request builders with validation for consistent client usage
WithClient()pattern for explicit client specification supporting scoped IAM credentials- Transaction-level and batch-level configuration (ReturnConsumedCapacity, ClientRequestToken, ReturnItemCollectionMetrics)
- Field encryption support in transaction and batch operations with automatic parameter encryption
- Type-safe response deserialization with
GetItem<TEntity>(index),GetItems<TEntity>(indices), andExecuteAndMapAsync<T1, T2, ...>() - Comprehensive validation with clear error messages for operation limits and configuration issues
- Logging and diagnostics for transaction/batch operations with operation counts and consumed capacity
- Full support for string formatting with placeholders (e.g.,
Where("pk = {0}", value)) - Full support for lambda expressions (e.g.,
Set(x => new UpdateModel { Value = "123" })) - Source-generated key methods work seamlessly (e.g.,
table.Update(pk, sk).Set(...)) - Automatic extraction and preservation of expression attribute names and values
- Ignores transaction/batch-incompatible settings (item-level ReturnValues, ReturnConsumedCapacity, etc.)
- New
-
Entity-Specific Update Builders - Simplified update operations with entity-specific builders that eliminate verbose generic parameters
- Generated entity-specific update builder classes (e.g.,
UserUpdateBuilder) for each entity - Automatic type inference - entity type and update expressions type inferred from accessor
- Simplified
Set()method requiring onlyTUpdateModelgeneric parameter instead of three - Fluent chaining maintains proper return types throughout the builder chain
- All extension methods automatically wrapped with entity-specific return types
- Covariant return types for base class methods (e.g.,
ReturnAllNewValues()) - Full backward compatibility - existing base builders continue to work unchanged
- Better IntelliSense support with cleaner method signatures
- Reduced cognitive load when writing update operations
- Generated entity-specific update builder classes (e.g.,
-
Convenience Methods - Simplified methods that combine builder creation and execution in a single call
GetAsync()- Simple get operations returning entity directlyPutAsync()- Simple put operations without return valuesDeleteAsync()- Simple delete operations without return valuesUpdateAsync()- Update operations with configuration action- Support for both partition key only and composite key operations
- Overloads for entity objects and raw
Dictionary<string, AttributeValue> - Base class methods on
DynamoDbTableBasefor generic operations - Zero performance overhead - thin wrappers around existing extension methods
- Reduced boilerplate for common CRUD operations
- Improved code readability for simple operations
-
Raw Dictionary Support - Direct support for
Dictionary<string, AttributeValue>in all operations- Convenience methods accept raw attribute dictionaries
- Builder pattern methods accept raw attribute dictionaries
- Useful for testing, debugging, and migration scenarios
- Enables dynamic schema scenarios without entity classes
- Works with all operations: Get, Put, Update, Delete
- Full support for conditions and return values with raw dictionaries
-
DateTime Kind Preservation - Explicit timezone handling for DateTime properties
- New
DateTimeKindparameter in[DynamoDbAttribute]to specify timezone behavior - Support for
DateTimeKind.Utc,DateTimeKind.Local, andDateTimeKind.Unspecified - Automatic conversion to specified kind during serialization (ToDynamoDb)
- Automatic kind assignment during deserialization (FromDynamoDb)
- Preserves timezone information across round-trip operations
- Defaults to
DateTimeKind.Unspecifiedfor backward compatibility - Works seamlessly with format strings for combined timezone and formatting control
- New
-
Format String Application in Serialization - Consistent format string handling across all operations
- Format strings from
[DynamoDbAttribute]now applied during ToDynamoDb serialization - Format-aware parsing during FromDynamoDb deserialization
- Support for DateTime formats (e.g., "yyyy-MM-dd", "o", custom patterns)
- Support for numeric formats (e.g., "F2" for decimals, "D5" for integers)
- Support for all IFormattable types with CultureInfo.InvariantCulture
- Comprehensive error handling with clear messages for invalid formats
- Backward compatible - properties without format strings use default serialization
- Format strings from
-
Encryption Support in Update Expressions - Field-level encryption now works in expression-based updates
- Encrypted properties automatically encrypted in update expressions
- Deferred encryption architecture - encryption happens at request builder layer
- Async encryption support without blocking calls
- Parameter metadata tracking for encryption requirements
- Clear error messages when encryption is required but not configured
- Support for multiple encrypted properties in single update
- Works with format strings for encrypted formatted values
- Consistent encryption behavior across PutItem, UpdateItem, and TransactWrite operations
-
Expression-Based Update Operations - Type-safe update operations with compile-time validation and IntelliSense support
- Source-generated
{Entity}UpdateExpressionsand{Entity}UpdateModelclasses for type-safe updates UpdateExpressionProperty<T>wrapper type enabling type-scoped extension methods- Extension methods for update operations:
Add(),Remove(),Delete(),IfNotExists(),ListAppend(),ListPrepend() - Type constraints ensure operations are only available for compatible property types
- Automatic translation of C# lambda expressions to DynamoDB update expression syntax
- Support for SET, ADD, REMOVE, and DELETE operations in a single expression
- Nullable type support - Extension methods work with nullable properties (
int?,HashSet<T>?,List<T>?, etc.) - Arithmetic operations - Support for arithmetic in SET clauses (e.g.,
x.Score + 10,x.Total = x.A + x.B) - Format string application - Automatic application of format strings from entity metadata (DateTime, numeric formatting)
- DynamoDB function support:
if_not_exists(),list_append(),list_prepend() - Comprehensive error handling with descriptive exception messages
- Full IntelliSense support with operation discovery based on property types
- AOT-compatible with no runtime code generation
- Backward compatible with existing string-based update expressions
- Breaking Change: Cannot mix expression-based and string-based Set() methods in the same builder (throws
InvalidOperationExceptionwith clear guidance) - Comprehensive XML documentation with examples for all APIs
- Source-generated
-
DynamoDB Streams Support - New
Oproto.FluentDynamoDb.Streamspackage for processing DynamoDB stream events- Fluent API for type-safe stream record processing with
Process<TEntity>()extension method - Separate package to avoid bundling Lambda dependencies in non-stream applications
[GenerateStreamConversion]attribute for opt-in stream conversion code generation- Generated
FromDynamoDbStream()andFromStreamImage()methods for Lambda AttributeValue deserialization - Event-specific handlers:
OnInsert(),OnUpdate(),OnDelete(),OnTtlDelete(),OnNonTtlDelete() - TTL-aware delete handling to distinguish manual vs. automatic deletions
- LINQ-style entity filtering with
Where()for post-deserialization filtering - Key-based pre-filtering with
WhereKey()for performance optimization - Discriminator-based routing for single-table designs with
WithDiscriminator() - Pattern matching support for discriminators (prefix, suffix, contains, exact)
- Table-integrated stream processors with generated
OnStream()methods - Automatic discriminator registry generation for table-level stream configuration
- Comprehensive exception types:
StreamProcessingException,StreamDeserializationException,StreamFilterException,DiscriminatorMismatchException - Full AOT compatibility for Native AOT Lambda deployments
- Support for encrypted field deserialization in stream records
- Fluent API for type-safe stream record processing with
-
API Documentation Style - All documentation now consistently shows three API styles in priority order
- Lambda expressions shown first (preferred) with type-safety benefits highlighted
- Format strings shown second as alternative approach
- Manual WithValue approach shown third for explicit control scenarios
- Updated
docs/core-features/BasicOperations.mdanddocs/core-features/QueryingData.mdwith consistent ordering - Updated
docs/advanced-topics/ManualPatterns.mdto reference preferred lambda approach
-
AOT/Trimmer Compatibility Improvements - Removed all reflection usage from main library code
MetadataResolvernow usesIEntityMetadataProviderinterface constraint instead of reflection-based method lookupProjectionExtensionsnow usesIProjectionModelandIDiscriminatedProjectioninterfaces instead of reflection-based property discovery- All source-generated entity classes implement the new interfaces automatically
- Test projects refactored to use
InternalsVisibleToinstead of reflection for internal member access DynamicCompilationHelpercentralizes unavoidable reflection in source generator tests with proper suppressions- Requirements: 2.1, 2.2, 2.4, 3.1, 4.1, 4.3
-
Complete Reflection Elimination in Main Library - All AOT-unsafe reflection patterns removed
ExpressionTranslatornow usesIGeospatialProviderinstead ofAssembly.GetType()andGetMethod()for geospatial operationsExpressionTranslatorusesMemberExpression.Memberdirectly instead ofGetProperty()for property accessUpdateExpressionTranslatorusesICollectionFormatterRegistryinstead ofActivator.CreateInstancefor collection formattingDynamoDbResponseExtensionsusesIEntityHydratorRegistryinstead ofGetMethod()for async hydrationEnhancedExecuteAsyncExtensionsusesIEntityHydratorRegistryinstead ofGetMethod()for entity conversion- All main library files now pass AOT-safety analysis with no reflection warnings
- Requirements: 6.1, 6.2, 6.3, 6.4
-
DynamoDbTableBase Constructor Changes - Updated to accept FluentDynamoDbOptions
- Old constructors accepting individual logger/encryptor parameters are deprecated
- New constructor accepts
FluentDynamoDbOptionsfor centralized configuration - Request builders receive options for proper service access
- Logger and encryptor extracted from options internally
- Requirements: 7.2, 5.3
-
IWithDynamoDbClient Interface Extended - Now exposes FluentDynamoDbOptions
- Added
Optionsproperty toIWithDynamoDbClientinterface - All request builders (
QueryRequestBuilder,ScanRequestBuilder,UpdateItemRequestBuilder) implement the new property - Extension methods can access options for proper service configuration
- Requirements: 5.3
- Added
-
Source generation now uses nested classes to avoid namespace collisions
-
Enhanced source generator to support Lambda AttributeValue types alongside SDK AttributeValue types
- Source Code Comment Cleanup - Removed requirement/fix/issue references from source code comments
- Cleaned comments in
Oproto.FluentDynamoDb/andOproto.FluentDynamoDb.SourceGenerator/projects - Preserved XML documentation for public APIs and comments explaining complex logic
- Removed TODO comments referencing completed work
- Cleaned comments in
- Documentation Accuracy - Verified and updated code examples across all documentation
- Verified examples in
docs/getting-started/,docs/core-features/, anddocs/advanced-topics/ - Updated outdated API references to match current implementation
- Added documentation for entity accessors, Keys.Pk()/Keys.Sk() usage, and lambda SET operations
- Verified examples in
- Example Entity Cleanup - Cleaned up all example project entities to follow correct attribute patterns
- Removed incorrect
[DynamoDbEntity]attribute from table entities (only needed for nested map types) - Removed manual
: IDynamoDbEntityinterface implementations (auto-generated by source generator) - Removed redundant
CreatePk()andCreateSk()methods that duplicate source-generatedKeysclass functionality - Added
Prefixconfiguration to[PartitionKey]and[SortKey]attributes for proper key formatting - Updated example code to use source-generated
Entity.Keys.Pk()andEntity.Keys.Sk()methods - Affected projects: OperationSamples, InvoiceManager, StoreLocator, TransactionDemo, TodoList
- Fixed documentation examples in
docs/DOCUMENTATION_CHANGELOG.md,docs/examples/ProjectionModelsExamples.md,docs/core-features/ProjectionModels.md, anddocs/advanced-topics/FieldLevelSecurity.md
- Removed incorrect
- WithClientExtensions - Removed
Oproto.FluentDynamoDb/Requests/Extensions/WithClientExtensions.cs- Extension methods replaced with instance methods of the same name and signature
- No code changes required - existing
builder.WithClient(client)calls work unchanged - Requirements: 2.1, 2.3
-
Documentation API Corrections - Comprehensive fix for incorrect API method references across all documentation
- Replaced
ExecuteAsync()with correct method names throughout documentation:GetItemAsync()for GetItemRequestBuilderPutAsync()for PutItemRequestBuilderUpdateAsync()for UpdateItemRequestBuilderDeleteAsync()for DeleteItemRequestBuilderToListAsync()for QueryRequestBuilder and ScanRequestBuilderCommitAsync()for transaction builders
- Fixed return value access patterns to use
ToDynamoDbResponseAsync()when accessingresponse.Attributes - Added alternative examples using
DynamoDbOperationContext.Currentfor context-based access - Updated XML documentation comments in source files (DeleteItemRequestBuilder, UpdateItemRequestBuilder, PutItemRequestBuilder)
- Corrected examples in 20+ documentation files across getting-started, core-features, advanced-topics, examples, and reference sections
- Created
docs/DOCUMENTATION_CHANGELOG.mdfor tracking documentation corrections separately from code changes - Updated
.kiro/steering/documentation.mdwith documentation changelog requirements
- Replaced
-
Options Propagation in Batch and Transaction Get Operations - Fixed
FluentDynamoDbOptionsnot being passed to entity deserializationBatchGetResponsenow accepts and propagatesFluentDynamoDbOptionstoFromDynamoDb()callsTransactionGetResponsenow accepts and propagatesFluentDynamoDbOptionstoFromDynamoDb()callsBatchGetBuildercaptures options from request builders and passes to responseTransactionGetBuildercaptures options from request builders and passes to response- Enables JSON deserialization of
[JsonBlob]properties in batch/transaction get results - Enables logging during entity hydration in batch/transaction operations
-
Options Propagation in Source-Generated Hydrators - Fixed
IAsyncEntityHydratornot receiving options- Added
FluentDynamoDbOptions? options = nullparameter toIAsyncEntityHydrator<T>interface methods - Updated
HydratorGeneratorto generate code that accepts and passes options toFromDynamoDbAsync() - Updated
EnhancedExecuteAsyncExtensionsto pass options toHydrateAsync()andSerializeAsync()calls - Enables field encryption and JSON serialization in async hydration scenarios
- Added
-
Composite Entity Assembly (ToCompositeEntityAsync) - Fixed
ToCompositeEntityAsync()not populating related entity collections- Fixed
IsMultiItemEntityflag not being set for entities with[RelatedEntity]attributesEntityAnalyzernow setsIsMultiItemEntity = truewhen entity has relationships- Enables proper multi-item
FromDynamoDbcode generation for composite entities
- Fixed wildcard pattern matching for multi-segment sort key patterns
- Patterns like
"INVOICE#*#LINE#*"now correctly match sort keys like"INVOICE#INV-001#LINE#1" - Implemented regex-based pattern matching with proper delimiter handling
- Supports
#,_, and:delimiters with automatic detection
- Patterns like
- Fixed primary entity identification in multi-item queries
FromDynamoDbnow identifies the primary entity item using the entity's sort key pattern- Non-collection properties are populated from the primary entity, not the first item
- Returns
nullwhen no primary entity item is found in the result set
- Added comprehensive property-based tests for all correctness properties
- Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 4.1, 4.2, 4.3, 5.1, 5.2, 5.3
- Fixed
-
Compile Warning Reduction - Eliminated all 1,182 compile-time warnings across the solution
- Reduced warning count from 1,182 to 0 (target was < 100)
- Fixed Roslyn analyzer warnings (RS2008, RS1032) in Source Generator project
- Added analyzer release tracking files (AnalyzerReleases.Shipped.md, AnalyzerReleases.Unshipped.md)
- Disabled AOT/trimming analysis for Source Generator project (runs at compile time, not in user binaries)
- Fixed nullable reference type warnings (CS8604, CS8601, CS8602, CS8625, CS8618) in main library
- Added pragma suppressions for AOT warnings (IL2026, IL3050) in DynamoDbMappingException (debugging only)
- Added pragma suppressions for example code files (CS0618, CS1998) - documentation examples
- Suppressed test project warnings appropriately (nullable, async, xUnit, AOT/trimming)
- Fixed nullable warning in SpatialQueryExtensions for pagination token handling
- Fixed nullable warning in DynamoDbIndex constructor
- Fixed nullable warning in UpdateExpressionTranslator for list item handling
- Improved logger call patterns in BatchGetBuilder, QueryRequestBuilder, and ScanRequestBuilder
- All test projects now have appropriate NoWarn settings for test-specific warnings
- Build now completes with 0 warnings and 0 errors on clean build
-
GetItemRequestBuilder - Fixed bug where empty
ExpressionAttributeNamesdictionary was being set when no attribute names were used, causing DynamoDB to reject requests with "ExpressionAttributeNames can only be specified when using expressions" error -
GitHub Actions - Fixed parallel build issues causing file locking conflicts with source generator by adding
/maxcpucount:1flag to build commands -
Sensitive Data Redaction - Fixed sensitive data not being redacted in log messages for properties marked with
[Sensitive]attribute- Added
IsSensitiveproperty toPropertyMetadatafor runtime sensitivity detection - Updated
ExpressionTranslator.CaptureValue()to checkPropertyMetadata.IsSensitivefor redaction decisions - Updated
MapperGeneratorto populateIsSensitive = truein generatedPropertyMetadatafor sensitive properties - Sensitive property values now correctly show as
[REDACTED]in debug logs while actual values are used in DynamoDB queries
- Added
-
Source Generator Missing Using Directive - Fixed compilation errors in generated code when using
FluentDynamoDbOptions- Added missing
using Oproto.FluentDynamoDb;directive toTableGenerator.csandEntitySpecificUpdateBuilderGenerator.cs - Generated table classes and update builders now correctly resolve
FluentDynamoDbOptionstype
- Added missing
-
Fixed duplicate index generation on tables
-
Fixed fluent chaining in
TypeHandlerRegistrationto allow multiple.For<T>()calls in discriminator-based routing -
Format String Application in Update Expressions - Format strings now consistently applied in all update expression operations
- Format strings from entity metadata now applied in SET operations
- Format strings applied in arithmetic operations (e.g.,
x.Score + 10) - Format strings applied in DynamoDB functions (IfNotExists, ListAppend, ListPrepend)
- Ensures data consistency across PutItem, UpdateItem, and TransactWrite operations
- Previously, format strings were only applied in some contexts, leading to inconsistent data formats
- Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
0.5.0 - 2025-11-01
- Source generation for automatic entity mapping, field constants, and key builders
- Fluent API for all DynamoDB operations (Get, Put, Query, Scan, Update, Delete)
- Expression formatting with String.Format-style syntax for concise queries
- LINQ expression support for type-safe queries with lambda expressions
- Composite entities support for complex data models and multi-item patterns
- Custom client support via
.WithClient()for STS credentials and multi-region setups - Batch operations (BatchGet, BatchWrite) with expression formatting
- Transaction support (TransactWrite, TransactGet) for multi-table operations
- Stream processing with fluent pattern matching for Lambda functions
- Field-level security with
[Sensitive]attribute for logging redaction - Field-level encryption with
[Encrypted]attribute and KMS integration - Multi-tenant encryption support with per-context keys
- Comprehensive logging and diagnostics system with
IDynamoDbLoggerinterface - Microsoft.Extensions.Logging adapter package (
Oproto.FluentDynamoDb.Logging.Extensions) - Conditional compilation support to disable logging in production builds
- Structured logging with event IDs and log levels
- Operation context (
DynamoDbOperationContext) for accessing metadata - Global Secondary Index (GSI) support with dedicated query builders
- Pagination support with
IPaginationRequestinterface - AOT (Ahead-of-Time) compilation compatibility
- Trimmer-safe implementation for Native AOT scenarios
- S3 blob storage integration (
Oproto.FluentDynamoDb.BlobStorage.S3) - KMS encryption integration (
Oproto.FluentDynamoDb.Encryption.Kms) - FluentResults integration (
Oproto.FluentDynamoDb.FluentResults) - Newtonsoft.Json serialization support (
Oproto.FluentDynamoDb.NewtonsoftJson) - System.Text.Json serialization support (
Oproto.FluentDynamoDb.SystemTextJson) - Comprehensive documentation with guides for getting started, core features, and advanced topics
- Integration test infrastructure with DynamoDB Local support
- Unit test coverage across all major components
- Format specifiers for DateTime (
:o), numeric (:F2), and other types - Sensitive data redaction in logs to protect PII
- Diagnostic utilities for debugging and troubleshooting
- Performance metrics collection for operation monitoring
- N/A (Initial release)
- N/A (Initial release)
- N/A (Initial release)
- N/A (Initial release)
- Field-level encryption with AWS KMS for protecting sensitive data at rest
- Automatic redaction of sensitive fields in logs to prevent PII exposure
- Multi-tenant encryption key isolation for secure multi-tenancy scenarios