Complex Properties and Collections¶
Complex properties are stored inline within the owning entity's DynamoDB item as nested maps or lists, with no separate table or key.
Use complex types for embedded DynamoDB document data. Do not model embedded data with EF Core relationships, foreign keys, navigations, or owned entity types; those relational modeling features are not supported by this provider. Separate DynamoDB items or tables should be modeled as separate root entity types without EF navigation relationships.
Complex Properties¶
ComplexProperty(...) maps a value-object-style member to a DynamoDB map (AttributeValue.M)
embedded within the owning item. Plain nested POCO members are auto-discovered by convention; use
ComplexProperty(...) when you want to customize the discovered member or configure it explicitly.
The CLR type may be declared as an EF Core complex type explicitly, typically with
[ComplexType] or modelBuilder.ComplexType<T>(), but that is not required for basic provider
discovery.
[ComplexType]
public class CustomerProfile
{
public string? DisplayName { get; set; }
public int? Age { get; set; }
}
modelBuilder.Entity<Customer>(builder =>
{
builder.ToTable("Customers");
builder.HasPartitionKey(x => x.Pk);
builder.ComplexProperty(x => x.Profile);
});
Under the default CamelCase naming convention, the Profile property is stored as "profile" in
DynamoDB:
{
"pk": { "S": "CUSTOMER#1" },
"profile": {
"M": {
"displayName": { "S": "Ada" },
"age": { "N": "31" }
}
}
}
To store the complex property under a different attribute name, use HasAttributeName(...) on
the complex-property builder:
Container attribute names follow the naming convention
The DynamoDB attribute key for the complex-property map is subject to the root entity's naming
convention, just like any other property. Use `HasAttributeName(...)` on the complex-property
builder to override it. See [Attribute Naming](../configuration/attribute-naming.md) for how
conventions propagate to nested complex properties.
Explicit configuration is optional for basic discovery
Use `ComplexProperty(...)` when you need nested configuration such as explicit attribute names,
converters, or other metadata overrides. For a plain nested POCO member, the provider
discovers it automatically as a complex property.
Complex Collections¶
ComplexCollection(...) maps a collection property to a DynamoDB list (AttributeValue.L).
Plain collection properties whose element type is a nested POCO are auto-discovered by
convention for the provider's supported complex collection CLR shapes: List<T> and IList<T>.
Use ComplexCollection(...) when you need to customize the discovered collection or its element
members.
Collection elements can themselves contain nested complex properties.
The collection is stored as a List:
{
"pk": { "S": "CUSTOMER#1" },
"contacts": {
"L": [
{ "M": { "email": { "S": "ada@example.com" } } }
]
}
}
Supported CLR collection shapes: List<T>, IList<T>. ICollection<T>,
IReadOnlyList<T>, and arrays are not supported for complex collections.
Collection updates replace the full list
Complex collection updates are written as full-list replacements of the containing DynamoDB
attribute. Modifying, adding, or removing an element updates the entire list value, not an
in-place list element delta.
Query Behavior¶
Filtering¶
Nested complex-property paths translate to dot-notation in PartiQL, and list index access translates to bracket-notation:
Complex property-to-property equality also translates when both sides are complex attributes on the same item:
Equality against a complex object parameter or inline complex object constant also translates and binds the value as a DynamoDB map parameter.
Whole-complex equality caveats
Whole-complex equality follows DynamoDB map comparison semantics. It compares the exact stored
map shape, so an omitted nested attribute is not necessarily equal to an explicit DynamoDB
NULL entry. Also, a complex-map equality predicate does not by itself target a partition; add
a partition-key predicate when you want to avoid scan-like execution.
Projections¶
Nested path access in Select is not supported server-side. The provider projects the top-level
complex-property container and extracts nested values on the client:
The generated PartiQL selects the whole container:
profile is read from DynamoDB and Address.City is extracted during client-side shaping.
SelectMany is not translated
Direct querying of complex collections via `SelectMany` is not supported. `.Include()` has no
effect on complex properties and can be omitted.
Nesting Limits and Constraints¶
Nesting and Size Limits¶
Complex properties are embedded in the same DynamoDB item as the root entity. That means all nested data shares one item-size budget and is read or written as part of that root item.
DynamoDB item size limit
DynamoDB imposes a maximum item size of 400 KB. Deeply nested or large complex collections
count against this limit.
Complex types can be nested to any depth. A complex collection can contain nested complex properties, which can themselves contain further complex properties.
In practice, keep nesting depth and collection size intentional: deeper or larger graphs are valid, but they increase item size and payload cost for every read and write of the owning entity.
builder.ComplexProperty(x => x.Profile, profile =>
{
profile.ComplexProperty(x => x.PreferredAddress);
profile.ComplexProperty(x => x.BillingAddress);
});
builder.ComplexCollection(x => x.Contacts, contact =>
contact.ComplexProperty(x => x.Address));
Null and Missing Attribute Behavior¶
When reading complex properties, the provider distinguishes optional vs required semantics from
the EF model and applies them consistently for both missing attributes and explicit DynamoDB
NULL values.
Null and missing attribute handling:
| Scenario | Behavior |
|---|---|
Optional complex property missing or NULL in DynamoDB |
Materializes as null |
Required complex property missing or NULL in DynamoDB |
Throws InvalidOperationException |
| Any complex property present with a non-map wire shape | Throws InvalidOperationException |
| Optional complex path null-propagates in projections | null, not an error |
Strict materialization
Unlike the Cosmos DB EF provider, this provider does not silently skip missing required
properties on complex types. If the DynamoDB attribute is absent or is a DynamoDB `NULL` and
the EF property is required, materialization throws. Likewise, if a complex property is present
but encoded with the wrong DynamoDB wire shape (for example `S` instead of `M`), materialization
throws even when the CLR property is nullable. Design your schemas accordingly.
Relationships vs Complex Types¶
Use complex types when nested data belongs to the same DynamoDB item and is read or written with that item. This matches DynamoDB maps and lists.
Do not use EF relationship APIs for DynamoDB document nesting:
// ❌ Not supported
modelBuilder.Entity<Customer>()
.HasMany(x => x.Contacts)
.WithOne();
// ✅ Supported
modelBuilder.Entity<Customer>()
.ComplexCollection(x => x.Contacts);
Use separate root entities only when the data is stored as separate DynamoDB items or in separate tables. Keep those roots independent in EF: query them separately and connect them in application code by key values if needed.