Skip to content

How Queries Execute

The provider compiles LINQ expressions into PartiQL SELECT statements and executes them using DynamoDB's ExecuteStatement API, with any unsupported operators failing at translation time rather than silently falling back to client evaluation.

LINQ to PartiQL

When you execute a LINQ query, the provider runs it through a multi-stage compilation pipeline before any network call is made:

  1. DynamoQueryableMethodTranslatingExpressionVisitor walks the LINQ expression tree and builds a SelectExpression — an internal query model representing the projected columns, WHERE predicate, ORDER BY orderings, and evaluation limit.
  2. DynamoQueryTranslationPostprocessor finalizes the SelectExpression: it applies discriminator predicates when shared-table, inheritance, or multi-concrete table groups need type filtering, and runs index selection analysis to determine whether a GSI or LSI should be used.
  3. DynamoQuerySqlGenerator converts the SelectExpression into a PartiQL SELECT statement with positional ? placeholders and a matching AttributeValue parameter list.

FindAsync uses EF Core's normal entity-finder path. It checks the change tracker first; if the entity is not tracked, EF Core produces a primary-key equality query that the provider translates through the same async PartiQL execution pipeline.

var orders = await db.Orders
    .Where(o => o.CustomerId == customerId && o.Status == "PENDING")
    .OrderBy(o => o.CreatedAt)
    .Select(o => new { o.OrderId, o.Total })
    .ToListAsync(cancellationToken);

// Generated PartiQL:
// SELECT "OrderId", "Total"
// FROM "Orders"
// WHERE "CustomerId" = ? AND "Status" = ?
// ORDER BY "CreatedAt" ASC

Identifiers are always double-quoted in generated PartiQL, and quoted identifiers are case-sensitive. The parameter values are serialized as AttributeValue objects; no literal values appear in the SQL text.

The ExecuteStatement Model

All queries execute via the DynamoDB ExecuteStatement API — not via the Query or Scan SDK methods. ExecuteStatement accepts a PartiQL SELECT and a list of positional parameters, then returns a page of results and an optional continuation token.

Two fields on the request control result size:

  • Limit — an evaluation budget: the number of items DynamoDB reads before applying filter expressions. It is not a guarantee of how many matching items are returned. A Limit(25) query may return anywhere from 0 to 25 items depending on how many of the evaluated items satisfy the WHERE predicate.
  • NextToken — a continuation cursor returned by DynamoDB when the response was cut short by the Limit or by the 1 MB processed-data cap per request. The provider follows NextToken automatically for unbounded queries; for paged queries it exposes the token via ToPageAsync.

Note

DynamoDB stops processing a request at either the Limit item count or 1 MB of processed data, whichever comes first. A page can therefore contain zero matching items while still returning a non-null NextToken when many non-matching items were evaluated. Always check NextToken == null (not Items.Count) to determine end of results.

Client-Side vs Server-Side Evaluation

The provider has a strict server-side-first policy: if a LINQ operator cannot be translated to PartiQL, the translation fails with an InvalidOperationException at compile time. The provider never silently evaluates unsupported operators in-process against a full table scan.

The one sanctioned form of client-side evaluation is explicit: calling .AsAsyncEnumerable() marks the boundary between server execution and LINQ-to-objects. Any LINQ operators chained after AsAsyncEnumerable() run in-process against the result stream the server returned.

// Server evaluates the WHERE and Limit; client selects the first match.
var active = await db.Orders
    .Where(o => o.CustomerId == customerId && o.IsActive)
    .Limit(50)
    .AsAsyncEnumerable()
    .FirstOrDefaultAsync(cancellationToken);

Some projection shaping also runs client-side. Computed expressions in Select (string methods, arithmetic, constructor calls) and nested complex-property materialization are applied by the shaper lambda after the server returns the raw AttributeValue rows. This is expected behavior and is documented per operator on the Projection page.

Debugging Generated PartiQL

Use EF Core's ToQueryString() to inspect generated PartiQL without sending a request to DynamoDB. Parameterized queries include comment lines with positional parameter names (p0, p1, ...) and formatted DynamoDB AttributeValue values before the PartiQL text.

var partiQl = db.Orders
    .Where(o => o.CustomerId == customerId)
    .ToQueryString();

// -- p0='CUSTOMER#123'
// SELECT "pk", "sk", "$type", "total"
// FROM "Orders"
// WHERE "CustomerId" = ?

ToQueryString() is for debugging only: it does not run scan warnings, log query execution events, or execute ExecuteStatement.

Asynchronous execution

All DynamoDB query execution is async. The AWS SDK exposes no synchronous statement execution, so the provider is async-only: attempting to enumerate query results synchronously throws InvalidOperationException with guidance to use ToListAsync, FirstAsync, or AsAsyncEnumerable(). Non-executing APIs such as ToQueryString() keep working.

Precompiled queries

When EF Core query precompilation is enabled, EF Core generates interceptors for query calls during the build. The provider stores the translated PartiQL as a compact command template in those interceptors. At runtime it supplies scalar and collection parameter values, creates DynamoDB AttributeValue parameters, and executes the same query pipeline used by normal compiled queries.

The generated materializer resolves each projected property through the compiled EF model. This preserves property-specific value converters without serializing the provider's internal query expression tree into generated source.

See Precompiled Queries and NativeAOT for project setup and current restrictions, including the tested NativeAOT model limitations.

Async execution

All database queries use async methods such as ToListAsync() and FirstOrDefaultAsync(). This also applies to generated precompiled queries. Synchronous query operators and enumeration throw InvalidOperationException. Find() can return an already-tracked entity without executing a query. Use FindAsync() for database lookups and ToPageAsync() when you need explicit continuation-token control.

See also