Limitations¶
The DynamoDB EF Core provider does not support all standard EF Core features. This page is the authoritative reference for what is not supported, why, and what workaround (if any) applies.
NativeAOT and precompiled queries¶
NativeAOT query support is experimental and follows EF Core's precompiled-query restrictions. Queries must be discoverable as static LINQ expressions during the build. Queries assembled at runtime from expression trees cannot be intercepted and precompiled.
NativeAOT publishing can report trimming and dynamic-code warnings from EF Core, the AWS SDK, or provider paths outside query execution. The provider's smoke build allows those warnings while AOT support remains experimental; a warning-free trimmed application is not yet guaranteed.
CI publishes and runs the NativeAOT smoke application for both EF Core 10 and EF Core 11.
The tested native path supports scalar entity properties, including nullable numbers, Boolean,
binary, configured scalar conversions, and one-dimensional arrays with non-nullable elements.
Enums without an explicit converter also materialize directly (stored as their numeric DynamoDB
value). List, set, and dictionary primitive collections work when their elements are themselves
primitives or use their own element-level value converter (for example List<Guid> or
List<SomeEnum> with string element conversion). Collections of non-primitive, non-convertible
element types — complex/owned entity types aside from dedicated complex-collection support — and
property-level value converters composed over the collection itself are rejected at compiled-model
generation time.
NativeAOT precompiled queries do not currently support entity materialization that requires EF Core
to read a non-public mapped field. This includes mutable field-backed collection properties such as
List<T>, HashSet<T>, and Dictionary<string, T>; auto-properties are affected when EF Core
selects their backing field. The current EF Core generated field-read accessor is invalid in
NativeAOT. Keep those collection or dictionary values out of entities materialized by precompiled
NativeAOT queries until EF Core resolves the issue.
Field-only properties can hit the same limitation. Basic scalar properties and arrays are covered by the native smoke test because their materialization uses a field write, not a field read.
Query execution is asynchronous only. Synchronous query operators and enumeration throw
InvalidOperationException; use ToListAsync, FirstAsync, ToPageAsync, or
AsAsyncEnumerable.
Limit(n) and WithNextToken(...) both precompile normally, including with runtime-varying
(local-variable) arguments — see Precompiled Queries and
NativeAOT for the local-variable requirement.
ToPageAsync(...) does not, and so cannot run in a NativeAOT-published binary (a query with no
generated interceptor has no JIT fallback — not a build-time error; dotnet publish succeeds and
the failure only surfaces at execution). This is an upstream EF Core limitation, not a gap in
how this provider translates or executes pagination: ToPageAsync(...) works correctly through the
provider's normal pipeline outside NativeAOT, but EF Core's precompiler discovers query roots
through a closed, internal mechanism with no registration point for provider-defined terminal
methods, so the call is silently skipped. Limit(...).WithNextToken(...).ToListAsync() — a
recognized EF terminal — precompiles and executes correctly under NativeAOT, proving the pagination
mechanics work. For a tracked, non-empty result this is enough for full pagination:
EntityEntry.GetExecuteStatementResponse() exposes the page's NextToken (see
Pagination), verified against a
precompiled NativeAOT query. ToPageAsync(...) remains necessary for projections, no-tracking
queries, and empty pages, where there is no tracked entity to read the token from. Tracked upstream:
dotnet/efcore#38962. See Precompiled Queries and
NativeAOT for details.
Nullable-coalescing projections such as Select(x => x.OptionalCount ?? -1) are rejected by the EF
Core C#-to-LINQ translator before provider translation — this one is an intentional build-time
error, not a runtime fallback. Use supported query shapes, or run either of these query shapes
without precompilation.
Precompiled-query generation upstream of the provider cannot handle complex-type members: a query
that materializes or filters on a complex property fails during dotnet publish with an EF Core
generated-code error. Avoid complex properties in contexts precompiled for NativeAOT.
Primitive-collection properties materialize their codec from compiled-model generated code under NativeAOT. A primitive-collection property that also carries a property-level value converter is not supported on that path and fails when the compiled model is generated. Converters on the collection elements are supported.
Converted values keep one interpreted seam under NativeAOT: value-converter delegate compilation falls back to the .NET expression interpreter, matching EF Core's own NativeAOT behavior. Query translation, parameter serialization, and materialization codecs themselves use generated or hand-written code only.
On EF Core 10, precompiled query generation cannot combine constant and computed (self-referencing,
for example Count + 1) setter values in a single ExecuteUpdate: EF Core's design-time generator
misroutes the captured-variable extraction and would silently write the wrong value. The provider
fails the build with a clear error for that shape; split the update into separate
ExecuteUpdateAsync calls (one for constants, one for computed values). EF Core 11 supports the
mixed shape.
Precompiled query constants and parameters must bind to a mapped entity property. The generated
code resolves each value's type mapping through the property that owns it (including element
mappings of primitive collections, for example Contains over a List<T> attribute). A query
value whose type mapping is not associated with any mapped property fails at precompile time with
an InvalidOperationException instead of silently deferring the failure to first query execution,
where it would use reflection that is not supported under NativeAOT.
See Precompiled Queries and NativeAOT for supported setup and verification.
Database lifecycle¶
SaveChangesnever creates DynamoDB tables. CallEnsureCreatedAsyncexplicitly or provision tables outside the provider.- Lifecycle APIs are async-only. Synchronous
EnsureCreated,EnsureDeleted, andCanConnectthrowNotSupportedException. EnsureCreatedAsynccreates missing tables withPAY_PER_REQUESTbilling. It can add missing GSIs to existing on-demand tables, but cannot add GSIs to provisioned tables because lifecycle throughput configuration is not exposed yet. It cannot add LSIs after table creation. Lifecycle wait polling, backoff, timeout, and whether to wait for completion are configurable.- Existing schema validation is limited to table key schema and secondary-index key/projection shape.
Includesecondary-index projection cannot be created yet because non-key projected attributes are not represented in provider metadata.
See Table Lifecycle for full behavior and seeding semantics.
Unsupported LINQ Operators¶
The following operators throw InvalidOperationException at translation time. The provider does
not fall back to in-process evaluation for these — the exception surfaces before any DynamoDB
request is sent.
See Supported Operators for the full list of what does translate.
| Category | Operators | Why |
|---|---|---|
| Aggregation | Count, LongCount, Sum, Average, Min, Max |
DynamoDB PartiQL has no query aggregate functions; primitive collection count/length is a supported size(attr) exception |
| Grouping | GroupBy |
GROUP BY is not supported in DynamoDB PartiQL |
| Joins | Join, GroupJoin, LeftJoin, RightJoin, SelectMany, DefaultIfEmpty |
DynamoDB does not support cross-item joins |
| Set operations | Union, Concat, Except, Intersect |
Not supported in DynamoDB PartiQL |
| Offset / paging | Skip, Take, ElementAt, ElementAtOrDefault |
DynamoDB has no offset semantics — use Limit(n) for an evaluation budget; direct primitive LIST ElementAt(constant) is a supported list-index exception |
| Element operators | Any(predicate), All except inline finite collection predicate rewrites |
General server-side element operators are not supported; primitive collection Any() is a supported size(attr) > 0 exception |
| Reverse traversal | Last, LastOrDefault, Reverse |
Requires reverse index traversal, not implemented |
| Deduplication | Distinct |
SELECT DISTINCT is not supported in DynamoDB PartiQL |
| Type casting | Cast<T> |
Not supported; TPH discriminator filtering via OfType<TDerived>() and is requires active discriminator metadata; GetType() checks require active discriminator metadata and are limited to exact concrete mapped entity types |
| Conditional skipping | SkipWhile, TakeWhile |
Not supported |
Queryable Contains over query sources |
Queryable.Contains(dbSet, item) |
Not supported; in-memory membership translates to IN, native collection membership to contains |
Value-converted enum numeric casts are also rejected when compared to parameters. For example, (int)entity.Status == value is not translated if Status uses .HasConversion<string>(), because DynamoDB stores the converted string value. Compare entity.Status to an enum value directly, or map the enum numerically.
Complex property-to-property equality and equality against complex object parameters or inline complex object constants are supported.
Complex Type Equality¶
Complex type equality (==) translates to whole-map attribute equality in PartiQL. DynamoDB compares the entire stored map, not individual properties. If a DynamoDB item contains unmapped attributes written outside EF Core (out-of-band writes), two items that are structurally equal by CLR properties may not compare equal at the DynamoDB level because the stored maps differ.
Rely on complex type equality only when EF Core is the sole writer of those attributes. If out-of-band writes are possible, compare individual scalar properties instead.
Scalar value-converted collection membership is not translated. entity.Values.Contains(value) is
supported for native DynamoDB primitive list/set attributes, but not when Values is serialized into
a scalar string or blob by a property value converter. In that case DynamoDB would evaluate substring
or binary containment rather than collection membership.
byte[] properties map to one DynamoDB Binary attribute. The provider translates binary length,
non-empty checks, and equality (Length, parameterless Any(), and SequenceEqual). It does not
translate byte-level membership or indexing (Contains(byte), [index], First()) because
DynamoDB PartiQL has no byte-level operators over Binary attributes.
Native DynamoDB primitive collection attributes support a few direct operations: membership
Contains(value), parameterless Any(), Count/Count()/Length, direct list indexing or
ElementAt(index) with a constant numeric index, and First()/FirstOrDefault() as constant
index 0. Dynamic list indexes are not supported. These translate to PartiQL contains, size,
or list-index expressions. They do not enable general list-element query pipelines; predicate
Any/Count, list-element filtering, ordering, paging, joins, set operations, and direct LIST
projection shaping remain unsupported.
Inline finite primitive collection predicates such as new[] { 1, 2 }.Any(x => x == entity.Value)
and new[] { 1, 2 }.All(x => x != entity.Value) are rewritten to finite scalar predicates. Other
predicate aggregates, including Count(predicate), Min, and Max, remain unsupported server-side.
Workaround for unsupported operators: switch to AsAsyncEnumerable() before the unsupported
operator to move evaluation in-process:
// ❌ Throws at translation time
var count = await context.Orders.CountAsync();
// ✅ In-process
var count = await context.Orders.AsAsyncEnumerable().CountAsync();
In-process evaluation fetches all matching pages from DynamoDB before applying the operator. Use with care on large result sets.
Take vs Limit(n)¶
Take(n) is not translated — use the DynamoDB-specific Limit(n) extension instead. The
distinction matters: Limit(n) maps to ExecuteStatementRequest.Limit, which is an evaluation
budget (DynamoDB reads up to n items then filters). It is not a result count. See
Ordering and Limiting for details.
Query Shape Constraints¶
First / FirstOrDefault — Key-Only Safe Path¶
FirstAsync and FirstOrDefaultAsync set an implicit Limit=1 on the server request. Because
DynamoDB counts evaluated items against Limit (not matched items), this is only safe when the
WHERE clause guarantees at most one evaluation pass before a match:
- No user-specified
Limit(n)on the query. - The
WHEREclause includes a partition-key equality condition. - Any sort-key predicate is a valid DynamoDB key condition (
=,<,<=,>,>=,BETWEEN,begins_with).
By default, filtered First* queries that fail the partition-key or sort-key safety checks throw
InvalidOperationException at translation time. AsUnsafeFilteredQuery() and
AllowUnsafeFilteredQueries() can bypass only that filtered First* safety validation for
controlled legacy code or tests. Explicit Limit(n) combined with First* is never supported.
Sort-key filter expressions are unsafe. SK IN (...) and SK = A OR SK = B reference only
key attributes but are DynamoDB filter expressions, not key conditions. Limit=1 on a filter
predicate can silently miss matching rows later in the partition:
var skValues = new[] { "ORDER#1", "ORDER#2" };
// ❌ Throws — SK IN is a filter expression
await context.Orders
.Where(x => x.Pk == pk && skValues.Contains(x.Sk))
.FirstOrDefaultAsync(ct);
// ✅ Client-side selection via AsAsyncEnumerable()
var result = await context.Orders
.Where(x => x.Pk == pk && skValues.Contains(x.Sk))
.AsAsyncEnumerable()
.FirstOrDefaultAsync(ct);
Unsafe filtered First* is not a best practice
AsUnsafeFilteredQuery() bypasses the provider's First / FirstOrDefault safety
validation for one query. AllowUnsafeFilteredQueries() applies the same bypass to every
query in the context.
This does not disable scan-like query protection, does not allow explicit Limit(n) or
WithNextToken() with First*, and does not change First* execution: the provider still
sends one request with implicit Limit=1 when no user limit is specified.
DynamoDB applies filters after evaluating items, so FirstOrDefaultAsync can return null
and FirstAsync can throw even when a later item would match. See AWS' notes on
filter expressions.
Use this only for tests or controlled legacy code.
var result = await context.Orders
.Where(x => x.Pk == pk && skValues.Contains(x.Sk))
.AsUnsafeFilteredQuery()
.FirstOrDefaultAsync(ct);
Exception — PK-only table. When the base table has no sort key, each partition holds at most
one item and First* with a PK equality condition is always safe.
Shared-table / inheritance. The provider injects a discriminator predicate automatically.
Server-side First* is safe only when the query evaluates at most one base-table item before
filtering: a PK-only lookup on a PK-only table, or a PK+SK equality on a PK+SK table. By default,
all other shapes throw — use AsAsyncEnumerable().FirstOrDefaultAsync(), or explicitly opt in to
unsafe filtered First* behavior when you accept the DynamoDB filter-expression risk.
Single / SingleOrDefault — Key-Condition-Only¶
SingleAsync and SingleOrDefaultAsync are supported only for key-condition-only query shapes.
The provider sends one DynamoDB request with implicit ExecuteStatementRequest.Limit = 2 and lets
EF Core enforce cardinality:
- zero returned items:
SingleAsyncthrows,SingleOrDefaultAsyncreturnsnull/ default - one returned item: returns that item
- two returned items: throws
Sequence contains more than one element.
Because DynamoDB Limit counts evaluated items, not matched rows, Single* does not allow non-key
filters, scan-like predicates, or sort-key filter expressions. Unlike First*, there is no unsafe
filtered escape hatch yet: AsUnsafeFilteredQuery() and AllowUnsafeFilteredQueries() do not
bypass Single* validation.
Allowed partition-key conditions are equality and IN. Sort-key predicates are allowed only when
they are DynamoDB key conditions (=, <, <=, >, >=, BETWEEN, begins_with). For
shared-table inheritance queries with discriminator filters, Single* requires a base-table lookup
that identifies one physical item: PK equality on a PK-only table, or PK+SK equality on a PK+SK
table. Secondary-index derived-type queries can fail validation even when they include index key
conditions. Explicit Limit(n) and WithNextToken() combined with Single* throw at translation
time.
If DynamoDB returns any NextToken for a validated Single* query, the provider throws
InvalidOperationException. This is treated as a guard against an unexpected provider/DynamoDB
invariant break, not as a signal to page. Without a continuation token, two returned items use
EF Core's normal Single* duplicate detection and throw Sequence contains more than one element.
Find — Primary-Key Lookup¶
FindAsync is supported for primary-key lookup. It checks the change tracker first and otherwise
executes a base-table key-equality PartiQL query with Limit=1. It does not use secondary indexes
or automatic index selection; use LINQ with .WithIndex(...) or automatic selection for
secondary-index lookups.
Synchronous Find follows EF Core's normal change-tracker behavior: it can return an already
tracked entity without DynamoDB I/O. If it needs to query DynamoDB, synchronous query execution
throws InvalidOperationException. Use FindAsync for database lookups:
WithNextToken Cannot Combine with First* or Single*¶
Combining .WithNextToken(token) with FirstAsync, FirstOrDefaultAsync, SingleAsync, or
SingleOrDefaultAsync throws InvalidOperationException. A seeded continuation token implies
resuming an arbitrary position in a result set, which is incompatible with provider-managed
terminal limits (Limit=1 for First*, Limit=2 for Single*).
OrderBy — Only Key Columns¶
OrderBy and OrderByDescending only accept partition-key and sort-key column expressions.
Non-key attribute ordering throws at translation time. For multi-partition queries, the partition
key must be the first ORDER BY column.
Automatic Index Selection — ALL Projection Only¶
Automatic index selection (On or SuggestOnly mode) rejects GSI/LSI candidates
whose projection type is not ALL. KEYS_ONLY and INCLUDE index candidates are logged as
rejected (DYNAMO_IDX005) and excluded from selection. Use an explicit .WithIndex("name")
hint to route to a non-ALL index.
String Function Limitations¶
string.StartsWith(s) and string.Contains(s) translate to begins_with and contains in
PartiQL for string, char, and StringComparison.Ordinal overloads. Other
StringComparison values and CultureInfo arguments throw at translation time.
DynamoDB PartiQL does not provide ends_with, regex, substring, replace, trim, or case-conversion
functions, so EndsWith, Regex, IndexOf, Substring, Replace, Trim*, ToUpper, and
ToLower are not translated.
string.Length translates to DynamoDB size(attr). DynamoDB size semantics are not guaranteed to
match .NET UTF-16 string.Length for all Unicode text, so avoid relying on it for non-ASCII string
length rules.
SELECT * Never Emitted¶
The provider always emits an explicit column list. This means projected types must have all required attributes available in the index or table projection. See Projection.
Write Constraints¶
Synchronous SaveChanges Not Supported¶
SaveChanges() throws NotSupportedException. Use SaveChangesAsync().
The AWS SDK for .NET exposes only async I/O for DynamoDB; the provider does not wrap async calls synchronously to avoid deadlocks in ASP.NET Core and other async-first hosts.
Key Mutation Not Supported¶
Changing a primary key (partition key or sort key) value on an entity and calling
SaveChangesAsync throws NotSupportedException. DynamoDB items are identified by their key
attributes; updating a key requires deleting the old item and inserting a new one. The provider
does not perform this two-step operation automatically — detach and re-add the entity with the
new key instead.
DynamoDB Transaction Limits¶
DynamoDB ExecuteTransaction enforces two hard limits:
-
Maximum 100 write statements per transaction. When
AutoTransactionBehaviorisWhenNeededorAlwaysand the save unit exceedsMaxTransactionSize(default 100, max 100), the provider throwsInvalidOperationExceptionunlessTransactionOverflowBehavior.UseChunkingis configured. -
No duplicate items within a single transaction. Writing the same DynamoDB item more than once in a single transaction throws
InvalidOperationException— the provider validates this client-side before sending the request to DynamoDB.
See Transactions for configuration details.
acceptAllChangesOnSuccess: false Restrictions¶
Chunked transactional writes (TransactionOverflowBehavior.UseChunking) and non-atomic batched
writes (AutoTransactionBehavior.Never) both require acceptAllChangesOnSuccess: true.
Calling SaveChangesAsync(acceptAllChangesOnSuccess: false) with either path throws, because
partial chunk commits must be accepted immediately in the change tracker to avoid replaying
already-persisted writes on retry.
PartiQL Statement Length Limit¶
DynamoDB enforces an 8 192-byte limit on ExecuteStatement statement text. The provider
validates statement length before sending and throws InvalidOperationException if the limit is
exceeded. This can happen with entities that have a large number of scalar properties. Consider
splitting such entities across multiple SaveChanges calls or reducing the number of mapped
properties.
EF Core Bulk Operations¶
ExecuteUpdateAsync() is supported as a single-item, key-targeted update: the WHERE clause must
equality-constrain the full primary key, the result is 1 on success and 0 when the target
item is missing (verified on DynamoDB Local; the real service may report 1 for a non-matching
update), and numeric self-referencing arithmetic is limited to addition and subtraction. See
ExecuteUpdateAsync for the full behavior
contract.
ExecuteDeleteAsync() is not implemented. Bulk deletions must be performed by loading entities,
removing them from the change tracker, and calling SaveChangesAsync() per item (or by issuing a
key-targeted DELETE statement against the client).
BatchExecuteStatement Partial Success¶
When AutoTransactionBehavior.Never is set, the provider executes writes via BatchExecuteStatement.
DynamoDB executes each statement independently — a batch can partially succeed, meaning some
writes commit while others fail. The provider throws if the response contains any failed
operations, but successful statements within that batch have already been persisted.
Modeling Constraints¶
Relationships and Foreign Keys Are Not Supported¶
The provider does not support EF Core relationship modeling. HasOne(...), HasMany(...),
WithOne(...), WithMany(...), HasForeignKey(...), skip navigations, and relationship
attributes such as [ForeignKey] and [InverseProperty] throw during model building or model
validation.
DynamoDB has no relational foreign-key enforcement or joins. Model embedded document data with EF Core complex types, and model separate DynamoDB items or tables as separate root entity types without EF navigation relationships.
// ✅ Embedded data: complex types
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(x => x.Profile);
b.ComplexCollection(x => x.Contacts);
});
// ❌ Not supported: relational navigation/foreign-key modeling
modelBuilder.Entity<Order>()
.HasOne(x => x.Customer)
.WithMany(x => x.Orders)
.HasForeignKey(x => x.CustomerId);
Owned Entity Types Are Not Supported¶
The provider does not support EF Core owned entity types. OwnsOne(...), OwnsMany(...), and
other owned-type configuration paths throw during model validation with guidance to switch to
complex types instead.
Use EF Core complex types for embedded document data:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(x => x.Profile);
b.ComplexCollection(x => x.Contacts);
});
Key Configuration¶
Root entities may use HasPartitionKey(...)/HasSortKey(...), EF HasKey(...), [Key], [PrimaryKey], or conventions. DynamoDB table keys are limited to one partition key plus optional sort key; EF keys with more than two properties are rejected.
Key properties must be non-nullable and resolve to a DynamoDB key-compatible provider type:
string, a numeric type (int, long, decimal, etc.), or byte[]. bool key properties
are rejected — bool has no built-in converter to a key-compatible type. Other non-primitive
types such as Guid, DateTime, and enum work because EF Core's built-in converters map
them to key-compatible store types (for example Guid/DateTime to string, and enum to
its numeric underlying value).
All entity types mapped to the same table must agree on key shape (PK-only or PK+SK) and must use identical physical attribute names for the partition key and sort key.
See Entities and Keys.
Secondary-Index Key Constraints¶
Secondary-index key properties follow the same type requirements as table keys but may be nullable (items without a scalar key-compatible value for a GSI/LSI key attribute are simply not indexed).
Local secondary indexes additionally require the table to define a sort key.
See Secondary Indexes.
Primitive Collection CLR Shapes¶
Primitive collection properties are supported only for specific CLR shapes. Custom or derived collection types throw during model validation.
| Collection kind | Supported CLR shapes |
|---|---|
| List | T[], List<T>, IList<T> |
| Set | HashSet<T>, ISet<T>, IReadOnlySet<T> |
| Dictionary | Dictionary<string, TValue>, IDictionary<string, TValue>, IReadOnlyDictionary<string, TValue>, ReadOnlyDictionary<string, TValue> |
Dictionary keys must be string. Non-string-keyed dictionary types are not supported.
Complex collection properties use a narrower CLR shape set than primitive collections. Complex
collections support only List<T> and IList<T>. ICollection<T>, IReadOnlyList<T>, and
arrays are not supported for complex collections. See Complex Types
for complex collection mapping details.
Concurrency Tokens — Application-Managed Only¶
Concurrency tokens (IsConcurrencyToken() / [ConcurrencyCheck]) are supported, but the
provider does not generate or increment token values automatically. Your application code must
update the token value before calling SaveChangesAsync.
IsRowVersion() and ValueGenerated.OnAddOrUpdate throw during model validation because the
provider cannot guarantee auto-increment semantics on DynamoDB item writes.
Discriminator Constraints¶
A discriminator is configured by default for every mapped entity type. Shared-table mappings and inheritance rely on it for type-safe filtering and materialization. The following constraints are validated at startup:
- Discriminator values must be unique within the table group.
- All entity types in the group must use the same discriminator attribute name.
- The discriminator attribute name must not collide with any mapped property attribute name (partition key, sort key, or regular properties).
The default discriminator attribute name is $type. Existing tables without this attribute must be
backfilled or mapped with HasNoDiscriminator().
See Single-Table Design.
Behavioral Differences from Standard EF Core¶
Synchronous execution¶
Normal and generated precompiled synchronous query methods throw InvalidOperationException.
Use async query methods for database access. Synchronous writes remain unsupported; use
SaveChangesAsync(). Find() can still return an already-tracked entity without querying.
ToQueryString() Is Debug-Only¶
IQueryable<T>.ToQueryString() returns generated PartiQL and formatted parameter comments without
sending a request to DynamoDB. It does not execute scan warnings, log command events, or validate
that DynamoDB accepts the statement at runtime. See Diagnostics and Logging.
Parameterized Null Inconsistency¶
When a nullable variable is null at runtime in a comparison (x.Prop == someVar where
someVar is null), the provider parameterizes the query as WHERE "Prop" = ? with an
AttributeValue { NULL = true }. This matches attributes stored with the DynamoDB NULL type but
does not match MISSING attributes (attributes absent from the item).
By contrast, a constant null comparison (x.Prop == null) translates to
"Prop" IS NULL OR "Prop" IS MISSING, which covers both representations.
DynamoDB PartiQL does not support attr IS ? (parameterized IS), so the two behaviors cannot
be unified. If you need to match both NULL and MISSING via a runtime variable, use explicit
functions:
// Explicit: matches both NULL and MISSING at runtime
.Where(x => EF.Functions.IsNull(x.Prop) || EF.Functions.IsMissing(x.Prop))
Two-Column Nullable Comparison¶
Comparing two nullable columns directly (x.A == x.B where both are nullable) generates a
binary = predicate. When either column holds a NULL type or is MISSING, DynamoDB PartiQL
returns MISSING (not TRUE) for the equality comparison — the row is excluded from results.
There is no provider-level workaround for this shape.
Consistent Read Semantics Follow the Final Query Source¶
The provider can set ExecuteStatementRequest.ConsistentRead globally with
options.ConsistentRead(true) or per query with .WithConsistentRead(). Per-query settings take
precedence, including .WithConsistentRead(false) overriding a global strongly consistent default.
Strong consistency is sent only when the finalized query source is the base table or an LSI. If a
global strongly consistent default query is finalized to a GSI through explicit index routing or
automatic index selection, the provider leaves ConsistentRead unset because DynamoDB GSIs are
always eventually consistent. If a query explicitly calls .WithConsistentRead() and the finalized
source is a GSI, the provider throws before sending the request.
The provider does not warn or fail for scan-like queries. It passes allowed consistency settings through to DynamoDB and lets DynamoDB apply the service semantics for the specific statement.
Per-Entity Response Metadata Requires Tracking¶
context.Entry(entity).GetExecuteStatementResponse() returns null for entities loaded via
AsNoTracking(). The ExecuteStatementResponse is stored in a shadow property that only exists
on tracked entity entries. See Diagnostics and Logging.
Owned Types in Select Project the Full Container¶
Accessing a nested owned property path in a Select projection
(x.Profile.Address.City) triggers client-side extraction: the full owned container attribute
("Profile") is fetched from DynamoDB and the nested value is read in-process. The path does
translate server-side in Where predicates.