Skip to content

Precompiled Queries and NativeAOT

The provider supports EF Core's generated query interceptors. It does not add a separate source generator. Microsoft.EntityFrameworkCore.Tasks finds query calls during the build, asks the provider to compile them, and writes the interceptors.

Experimental support

Precompiled-query and NativeAOT support relies on EF Core's internal precompilation APIs and a version-specific rewrite of EF Core-generated code. It is not yet a production-stability guarantee. CI exercises interceptor generation and NativeAOT publish-and-run execution for both EF Core 10 and EF Core 11. Test against your model and workload before using it in production.

Project setup

Add the EF Core Tasks package at the same version as your other EF Core packages, enable NativeAOT, and allow EF Core's generated interceptor namespace:

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InterceptorsNamespaces>
    $(InterceptorsNamespaces);Microsoft.EntityFrameworkCore.GeneratedInterceptors
  </InterceptorsNamespaces>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tasks"
                    Version="10.0.12"
                    PrivateAssets="all" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Design"
                    Version="10.0.12"
                    PrivateAssets="all" />
</ItemGroup>

Every project that declares a context or a query needs the Tasks reference and the interceptor namespace; PublishAot belongs only in the published startup project. Microsoft.EntityFrameworkCore.Design must also be resolvable in each generating project: EF Core 10's Tasks package brings it transitively, while EF Core 11's declares no dependencies, so reference it explicitly. Without the interceptor namespace entry, the generated interceptor file fails to compile.

Use the Tasks package version matching the EF Core version selected by your provider package. The example uses EF Core 10.0.12. For EF Core 11, pin to the exact build your application's other EF Core 11 packages use (for example 11.0.0-rc.1.26425.128) rather than a floating range — EF Core 11 is still prerelease, and an unpinned range can silently resolve to a different Tasks build than your EF Core packages.

Publish for a concrete runtime identifier:

dotnet publish --configuration Release --framework net10.0 --runtime linux-x64

Publishing generates both the compiled model and query interceptors. No generated files need to be checked into source control. EF Core 10 generates only when PublishAot (or EFOptimizeContext, which EF Core 11 removed) is set; EF Core 11 generates on any publish. To generate during build instead, set EFScaffoldModelStage and EFPrecompileQueriesStage to build (plus EFOptimizeContext=true on EF Core 10).

Manual generation

Use publish or configured Tasks generation by default. If a project intentionally retains generated interceptor source and invokes dotnet ef dbcontext optimize --precompile-queries --nativeaot manually, delete prior generated interceptor output before its tooling build. C# interceptors bind to source locations, so editing an intercepted query can make old output fail compilation before the regeneration command runs. Build the tooling inputs, run the command, then build again to compile the new output.

When a referenced context library already owns a checked-in compiled model, generate only query interceptors in the query project with --no-scaffold; generating another compiled model creates duplicate types. Keep the EF CLI/tooling, Tasks, Design package, EF Core runtime, and provider versions aligned.

Query shape

Write one statically visible LINQ chain in application code. Runtime scalar values are query parameters; passing a key or cancellation token does not itself make a query dynamic.

The local aliases below are an AOT/precompiled-query portability pattern. They are not required for ordinary JIT EF Core queries: use a direct context reference and method parameters there when that is clearer. Use aliases only when the query must be discovered and translated by the EF source precompiler:

internal static async Task<List<Order>> LoadOrdersAsync(
    OrdersContext context,
    string customerIdArg,
    string[] statusesArg,
    CancellationToken cancellationToken)
{
    var db = context;
    var customerId = customerIdArg;
    var statuses = statusesArg;
    var queryCancellationToken = cancellationToken;

    return await db.Orders
        .Where(order => order.CustomerId == customerId
            && ((IEnumerable<string>)statuses).Contains(order.Status))
        .ToListAsync(queryCancellationToken);
}

If this shape fails before DynamoDB translation, reduce it to a minimal EF Core repro rather than assuming a DynamoDB LINQ or PartiQL limitation.

The generated interceptor contains a compact PartiQL template. Scalar values become positional parameters. Local collections are expanded to the required number of positional parameters at runtime; an empty or null collection becomes a false predicate. Property reads are generated from the compiled model so configured value converters are retained.

Generated read templates resolve DynamoDB table and secondary-index names from the runtime EF model. Runtime resource-name configuration is honored by precompiled queries; generated write templates resolve the table name for ExecuteUpdateAsync and ExecuteDeleteAsync.

An EF compiled model contains the physical names it was generated with and bypasses OnModelCreating. If, and only if, the physical names differ between the environments you promote one artifact through, supply them with UseDynamo(o => o.RuntimeResourceNames(...)), which applies them to the runtime model when it is initialized. Precompiled queries then use them without regenerating anything. This is an advanced facility, not a general recommendation; see Runtime Resource Names.

The explicit IEnumerable<T> cast avoids the compiler selecting a span-based Contains overload for local arrays, which EF Core's query precompiler cannot currently translate.

Restrictions

  • Query calls must be visible to the EF Core build task. Dynamically assembled expression trees are not supported by query precompilation.
  • The normal provider translation limits still apply. Unsupported LINQ operators fail during the build instead of first failing at runtime.
  • A local collection used with Contains is limited to DynamoDB's supported PartiQL parameter count — 50 values when the comparison targets the partition key and 100 otherwise. DynamoDB enforces these limits per ExecuteStatement, and the generated interceptor expands the collection into positional parameters at runtime, so the collection shares that budget. The provider stops reading after the first excess item and throws.
  • NativeAOT publishing may emit trim and dynamic-code analysis warnings from EF Core, the AWS SDK, and provider features outside precompiled query execution. Reviewed warning IDs are pinned in CI with documented rationale; warnings are accepted only when their source is known.
  • Limit(n) and WithNextToken(...) both precompile and participate in NativeAOT, with either a literal or a captured-local (runtime-varying) argument — for example context.Items.Where(...).Limit(pageSize).WithNextToken(nextToken).ToListAsync(), where pageSize and nextToken are ordinary local variables. The generated interceptor is a single, reusable executor: calling it with different pageSize/nextToken values does not regenerate or re-translate the query.
  • A parameterized argument to a provider fluent method must be a local variable, not a bare reference to the enclosing method's own parameter or a field/constant. Assign the value to a local first (var pageSize = pageSizeArg;) before using it in the query — EF Core's precompiler only recognizes locals and lambda parameters when re-interpreting the query-building method; a bare method parameter or field reference fails with Encountered unknown identifier name '...', which doesn't correspond to a lambda parameter or captured variable.
  • ToPageAsync(...) does not currently precompile, and so cannot run in a NativeAOT-published binary (a query with no generated interceptor has no JIT fallback). This is an upstream EF Core limitation, not a gap in this provider: ToPageAsync(...) already translates and executes correctly through the provider's normal pipeline outside NativeAOT. EF Core's precompiler discovers query roots through a closed, internal mechanism that recognizes only a fixed set of terminal methods declared on EF Core's own types — provider-defined terminals like ToPageAsync currently have no way to participate in that discovery, so the call is silently skipped (no generated interceptor, no build-time error). Limit(pageSize).WithNextToken(nextToken).ToListAsync() — a recognized EF terminal — precompiles and NativeAOT-executes correctly, proving the underlying pagination mechanics work. For a tracked, non-empty result, this is enough for full pagination: the page's NextToken is available via EntityEntry.GetExecuteStatementResponse() (see Pagination), verified under a precompiled NativeAOT query in this provider's own runtime tests. ToPageAsync(...) is still required outside NativeAOT when there is no tracked entity to read the token from — projections, no-tracking queries, and empty pages. Under NativeAOT those shapes have no supported pagination path. Tracked upstream: dotnet/efcore#38962. See Limitations.
  • EF Core's precompiler currently rejects nullable-coalescing projections before provider translation, independent of the above.
  • The tested NativeAOT path covers entity materialization with string, nullable numeric, Boolean, binary, converted scalar, and one-dimensional array properties with non-nullable elements. Entity materialization requiring a read from a non-public mapped field is not supported. This includes mutable field-backed primitive List<T>, HashSet<T>, and Dictionary<string, T> properties, and may include auto-properties when EF Core chooses their backing field. Entities with complex properties and complex collections materialize, but under NativeAOT they must be queried with AsNoTracking() until EF Core supports change tracking for complex collections in compiled models (dotnet/efcore#37750). On EF Core 10 only, a precompiled no-tracking query generates invalid C# for an entity type declared in any (dotted) namespace, so on EF Core 10 a complex collection can only be queried under NativeAOT for an entity type in the global namespace. EF Core 11 does not have this defect. See Limitations for both. See Limitations before using NativeAOT with collection-valued entity members.
  • ExecuteUpdateAsync is precompilable under the same rules: the setter property selectors must be discoverable statically, and the normal key-targeting restrictions apply. On EF Core 10 a single precompiled ExecuteUpdate cannot mix constant and computed (self-referencing) setter values; the build fails with a clear error. Split such updates into separate calls. On EF Core 11 the mixed shape is supported.
  • ExecuteDeleteAsync is precompilable under the normal key-targeting restrictions.

Verification

Run generated-interceptor checks for both supported EF Core versions:

task test:aot-generation FRAMEWORK=net10.0
task test:aot-generation FRAMEWORK=net11.0

The provider verifies the generated EF Core 10 and EF Core 11 executor templates in its per-framework generation tests. Publish and run the NativeAOT runtime tests with task test:aot-publish FRAMEWORK=net10.0 (or net11.0) — this is the NativeAOT path gated by CI for both EF Core versions. The tests run parameterized and materializing queries plus a SaveChanges write against DynamoDB Local. They check NativeAOT execution and materialized values. Generation and parity tests separately check generated PartiQL templates and execution behavior. If interceptor generation reports an incompatible EF Core version or executor preamble, update the provider rewrite and its compatibility tests together.

For query translation details, see How Queries Execute. For all provider restrictions, see Limitations.