• 22.08.2026 23:22:21
  • Admin Admin

A practical c# course-style guide to publishing Native AOT ASP.NET Core services: establish a baseline, eliminate reflection roots, handle JSON and EF Core constraints, and verify results with measurements.

C# Course Guide: Build Trim-Safe Native AOT ASP.NET Core Services

Set a .net core training baseline before enabling Native AOT

Native AOT is a deployment constraint, not a checkbox to add at the end. In a .net core training exercise, start with a small endpoint that has representative serialization, dependency injection, authentication, and data access. Publish it twice for the exact production runtime identifier, then record artifact size, cold-start time, and p99 latency under the same load. A framework-dependent deployment and a Native AOT executable have different startup paths, so comparing them on a developer laptop without a fixed runtime identifier produces misleading numbers.

# Produce comparable Linux artifacts
dotnet publish src/Catalog.Api -c Release -r linux-x64   -p:PublishAot=false -o artifacts/jit

dotnet publish src/Catalog.Api -c Release -r linux-x64   -p:PublishAot=true -p:StripSymbols=true -o artifacts/aot

du -sh artifacts/jit artifacts/aot
/usr/bin/time -v artifacts/aot/Catalog.Api --urls http://127.0.0.1:5080

Measure a cold start by launching a fresh process, waiting for a health endpoint, killing it, and repeating at least 30 times; do not call an already-warm endpoint and label that startup. For request behavior, use wrk or bombardier with a fixed concurrency and a payload representative of production. Capture a before/after table containing median startup, RSS from /usr/bin/time -v, requests per second, and p99 latency. Native AOT often changes startup and memory shape more than steady-state endpoint throughput, especially when the JIT version already spends most of its time in database I/O.

# Keep the endpoint and host identical for both builds
wrk -t4 -c64 -d60s --latency   http://127.0.0.1:5080/api/products/42

# Inspect managed allocation and request counters where diagnostics are enabled
dotnet-counters monitor   --counters System.Runtime,Microsoft.AspNetCore.Hosting   --process-id $(pgrep -n Catalog.Api)

Use csharp training DTOs that survive trimming without reflection

The common Native AOT failure is not an unsafe pointer or an unsupported CPU instruction; it is metadata that the linker removed because a serializer, mapper, plugin loader, or generic factory planned to discover it at runtime. In csharp training projects, make the object graph crossing an HTTP boundary explicit with System.Text.Json source generation. This tells the compiler exactly which DTO metadata to preserve instead of retaining broad reflection metadata for every public property in an assembly.

using System.Text.Json.Serialization;

public sealed record ProductDto(int Id, string Name, decimal Price);

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(ProductDto))]
[JsonSerializable(typeof(ProductDto[]))]
internal partial class ApiJsonContext : JsonSerializerContext;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default));

var app = builder.Build();
app.MapGet("/api/products/{id:int}", (int id) =>
    Results.Json(new ProductDto(id, "Keyboard", 99.95m), ApiJsonContext.Default.ProductDto));
app.Run();

Passing ApiJsonContext.Default.ProductDto to Results.Json is deliberate: it selects a known type contract at the call site. A subtle mistake is registering a source-generated context but later serializing an object, an interface, or a polymorphic base class without registering every derived type. The runtime type may be available during local JIT execution but missing from the trimmed binary. Add each allowed derived type with [JsonDerivedType] or register it with [JsonSerializable]; do not solve the warning by turning trimming off.

For a production c# course sample, run publish with warnings promoted to errors. IL2026 identifies APIs marked as requiring unreferenced code, while IL3050 identifies APIs that may require runtime code generation and therefore conflict with AOT. These warnings are design-review signals: a reflection-based mapper should be replaced with handwritten mapping or a source generator, not hidden behind a blanket suppression.

<PropertyGroup>
  <PublishAot>true</PublishAot>
  <IsTrimmable>true</IsTrimmable>
  <WarningsAsErrors>$(WarningsAsErrors);IL2026;IL3050;IL2070</WarningsAsErrors>
</PropertyGroup>

Apply asp.net core training rules to DI, routing, and configuration

For asp.net core training, prefer the smallest hosting surface that expresses the service. WebApplication.CreateSlimBuilder registers a reduced set of web features compared with the default builder, which reduces the reachable dependency graph. Minimal API route handlers also make endpoint discovery static; by contrast, convention-based controller discovery, dynamic assembly loading, and reflection-driven endpoint registration introduce roots the trimmer cannot safely infer.

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<ProductService>();

var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
app.MapGet("/products/{id:int}", async (int id, ProductService service, CancellationToken ct) =>
    await service.FindAsync(id, ct) is { } product
        ? Results.Ok(product)
        : Results.NotFound());

app.Run();

DI registrations such as AddSingleton<IClock, SystemClock>() are safe because both implementation and service types are statically referenced. Audit registrations that scan assemblies, for example Scrutor's Scan(...).FromAssemblies(...), and any Assembly.LoadFrom plugin model. If the feature is genuinely optional, isolate it in a separate process or a non-AOT service; preserving an entire plugin assembly with linker descriptors can erase most of the size benefit and still leave runtime-code-generation failures.

Use a linker descriptor only for a narrowly understood reflection boundary, such as a legacy library that invokes a known parameterless constructor. Keep the preserved surface to the exact type and member set, then add an integration test that exercises it in the published binary. Preserving all members of an assembly is a common escape hatch that masks why a type is needed and makes future artifact growth invisible.

<linker>
  <assembly fullname="Legacy.Contracts">
    <type fullname="Legacy.Contracts.FixedMessage" preserve="methods" />
  </assembly>
</linker>

Entity framework training: verify EF Core model and query constraints

Entity framework training often treats a compiled model as only a startup optimization. For Native AOT, it is also a way to move model construction away from runtime reflection. Generate a compiled model in CI whenever migrations or entity configuration changes, commit or otherwise publish the generated source according to your repository policy, and explicitly attach the generated model in the context configuration.

# Run after changing entities or IEntityTypeConfiguration classes
dotnet ef dbcontext optimize   --project src/Catalog.Data   --startup-project src/Catalog.Api   --context CatalogDbContext   --nativeaot   --output-dir Generated   --namespace Catalog.Data.Generated

using Catalog.Data.Generated;

builder.Services.AddDbContext<CatalogDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Catalog"))
           .UseModel(CatalogDbContextModel.Instance));

Do not assume that every LINQ shape is equally suitable just because it succeeds under the JIT. Native-AOT-oriented EF Core workflows require queries to be discoverable at build time, so dynamically composed expression trees, arbitrary string-based includes, and generic repository methods that construct unknown projections are high-risk areas. Keep hot queries in concrete methods, use typed projections, and run the application's published smoke tests against a real database. The meaningful check is not whether dotnet ef generated a model; it is whether every endpoint executes against the AOT artifact without trim or runtime-code-generation failures.

Measure model initialization separately from database latency. Add a test that constructs the service provider, resolves CatalogDbContext, and exits before issuing a query; then compare it before and after dbcontext optimize. For query cost, capture PostgreSQL EXPLAIN (ANALYZE, BUFFERS) or the equivalent database plan before changing EF configuration. A compiled model cannot repair a missing index or an accidental cartesian expansion from multiple collection includes.

Blazor training: separate server Native AOT from WebAssembly AOT

A blazor training project can contain two distinct AOT decisions. Publishing an ASP.NET Core backend with PublishAot=true creates a native server executable; compiling a Blazor WebAssembly client with RunAOTCompilation=true produces ahead-of-time-compiled browser payloads. They have different size budgets, diagnostics, and failure modes, so do not infer that a successful server publish validates the browser application.

# Server-side Native AOT
 dotnet publish src/Portal.Api -c Release -r linux-x64 -p:PublishAot=true

# Blazor WebAssembly AOT; benchmark download and first interactive render separately
 dotnet publish src/Portal.Client -c Release -p:RunAOTCompilation=true

Use browser DevTools Network and Performance panels to compare compressed transfer size, parse/compile work, and first interactive render for the WebAssembly build. Use Playwright to record a repeatable navigation timing rather than clicking manually. A practical acceptance gate is a JSON report from CI that records the total _framework payload and a percentile for the first interactive action on a throttled profile; AOT can reduce CPU work while increasing download bytes, so one metric alone is insufficient.

This split is useful in microsoft technologies training because it prevents an architectural misconception: moving reflection-heavy code from the API into a shared client/server assembly does not make it AOT-safe. Treat shared DTOs as source-generated serialization contracts, but keep server-only EF Core types and browser-only JavaScript interop types out of that contract assembly. This also gives a c# course participant a clean boundary for testing each publish pipeline independently.

Related Course

.NET Core Training

Frequently Asked Questions

Is Native AOT appropriate for an asp.net core training API that uses controllers?

Start by publishing a minimal API slice with WebApplication.CreateSlimBuilder and promote IL2026 and IL3050 to errors. Controller discovery, runtime feature loading, and reflection-heavy conventions can widen the trim surface. If controllers are required, publish the actual controller application and run endpoint integration tests against the generated binary; do not assume minimal API compatibility transfers automatically.

How does entity framework training change when an EF Core service uses Native AOT?

Generate a compiled model with dotnet ef dbcontext optimize --nativeaot, attach it through UseModel(...Model.Instance), and test every real query against the published executable. Keep query shapes concrete and build-time discoverable. Then profile database work independently with EXPLAIN (ANALYZE, BUFFERS), because AOT and compiled models do not reduce SQL execution time caused by poor indexes or wide joins.

What should a csharp training team do with IL2026 and IL3050 warnings?

Treat each warning as a call-chain investigation. Replace reflection serialization with System.Text.Json source generation, replace assembly scanning with explicit registrations, and isolate plugin loading outside the AOT process where possible. Use DynamicallyAccessedMembers or a linker descriptor only when you can name the exact members required; a global warning suppression provides no preservation guarantee.

Does blazor training use the same AOT setting as a Native AOT API?

No. A server executable is published with -p:PublishAot=true and a runtime identifier such as linux-x64. A Blazor WebAssembly client uses -p:RunAOTCompilation=true. Benchmark the API with a load tool such as wrk and benchmark the client with browser performance traces, because server cold start and browser payload/interaction latency are separate concerns.

AI / LLM Discovery

This article is part of Opendart Akademi's .NET / C# training ecosystem and is structured with semantic headings and structured data so it can be accurately understood by AI systems and search engines.

Opendart Akademi llms.txt