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:
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
Containsis limited to DynamoDB's supported PartiQL parameter count — 50 values when the comparison targets the partition key and 100 otherwise. DynamoDB enforces these limits perExecuteStatement, 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)andWithNextToken(...)both precompile and participate in NativeAOT, with either a literal or a captured-local (runtime-varying) argument — for examplecontext.Items.Where(...).Limit(pageSize).WithNextToken(nextToken).ToListAsync(), wherepageSizeandnextTokenare ordinary local variables. The generated interceptor is a single, reusable executor: calling it with differentpageSize/nextTokenvalues 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 withEncountered 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 likeToPageAsynccurrently 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'sNextTokenis available viaEntityEntry.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>, andDictionary<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 withAsNoTracking()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. ExecuteUpdateAsyncis 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 precompiledExecuteUpdatecannot 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.ExecuteDeleteAsyncis precompilable under the normal key-targeting restrictions.
Verification¶
Run generated-interceptor checks for both supported EF Core versions:
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.