ASP.NET Core 10 APIs · Lesson 15

Source-generated JSON and AOT constraints

Compile an explicit serialization contract without claiming an AOT publish.

External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Make the serialized type set discoverable

A general runtime can discover serialization metadata through reflection, but trimming and NativeAOT require deliberate attention to reachable types and dynamic behavior. The external project declares an explicit JsonSerializerContext for Item, inserts it into HTTP JSON options and disables reflection-based JSON defaults. It also enables the request delegate generator. These settings make a normal build and real requests exercise generated contracts rather than quietly relying on a reflection fallback. They do not constitute a NativeAOT publish, nor prove that every future dependency or route is compatible with a particular native target.

Check the intended wire shape independently

The portable companion writes a JSON object with lowercase name and quantity properties through Utf8JsonWriter, then reads those two scalar properties and prints rune:3. It does not declare a pretend generated context inside the browser compiler. The external GET returns the same intended item shape, while a valid POST binds the supplied item and returns it through the generated metadata path. A quantity-zero POST takes an explicit empty bad-request branch. The normal build compiles the real generated context implementation, and successful requests with reflection defaults disabled demonstrate the supported Item contract for this project.

Connect the context to the actual HTTP serializer

Declaring a context alone is not enough if the HTTP endpoint still uses unrelated serializer options. The project inserts AppJsonContext.Default into the TypeInfoResolverChain used for HTTP JSON and declares camel-case names in the generation settings. Item is a separate public record, and its context declaration lives in another complete project file. Those files must be saved together before building. A later model or wrapper changes the set of types crossing the wire, so update the generated contract deliberately rather than adding an arbitrary resolver that silently restores reflection and conceals the missing metadata.

AOT compatibility extends beyond successful JSON requests

The project uses CreateSlimBuilder to keep its hosting surface focused, but a successful normal run does not prove native compilation, trimming correctness or platform dependency support. Libraries using dynamic code, reflection-based discovery or unsupported features can introduce separate constraints. Publish diagnostics and target-platform execution are evidence that must come from an actual publish workflow. This lesson intentionally reports compilation and loopback request results only. Do not rename those results as a NativeAOT release, and do not treat setting a project property as proof that the application has been tested after native compilation.

Practice: add a real wire type and observe its metadata boundary

Add a second explicit response record and an endpoint returning it. First leave that type out of the JsonSerializable declarations in a separate project copy, rebuild and observe the failure when its endpoint is exercised with reflection defaults disabled. Then add the intended generated metadata and verify the independently specified JSON shape. Keep the original Item endpoints as regression checks. The hint is to inspect the actual type being serialized, including wrappers or collections, instead of assuming a declaration for one model automatically covers every unrelated response shape a future handler might return.

Practice: specify a separate native-publish experiment

Choose one supported host platform and a documented native target, then write a publish checklist for this exact project and its dependencies. Record toolchain prerequisites, publish command, diagnostics and post-publish request checks before attempting it. If the environment lacks the toolchain, stop the experiment there and report that limitation without relabeling the normal build. Keep source-generation checks separate from the native step so a missing compiler does not hide JSON contract regressions. The exercise is to define honest evidence for the stronger claim, not to remove diagnostics until a property appears enabled.

Keep generation, trimming and publishing as separate claims

The portable writer proves an intended representation independently of framework serialization. The external project proves its generated Item context compiles and serves the tested requests with JSON reflection defaults disabled. A real NativeAOT publish would add another distinct result, which this lesson does not claim. Preserve that separation when upgrading packages or adding endpoints, and keep every serialized type, serializer option and error path visible in the contract. Generated metadata is a useful foundation for constrained deployment, but the complete application and its target environment remain the unit that a publish workflow ultimately has to verify.

  • Connect generated type metadata to the serializer that the endpoint actually uses.
  • Report normal compilation, source generation and a tested NativeAOT publish separately.
External .NET 10 SDK 10.0.403; ASP.NET Core server on your own desktop. Kestrel does not run in the SharpRune browser console.

Source-generated JSON and AOT constraints — real ASP.NET Core 10 project

Set up the SDK project

  1. Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
  2. From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
  3. Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
  4. Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
  5. GET /item and POST JSON {"name":"rune","quantity":2}; compare a quantity-zero POST. This exercise builds and runs normally; it does not execute dotnet publish -p:PublishAot=true.

Download complete SDK project ↗

Project files

WebLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
    <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
  </PropertyGroup>
</Project>
global.jsonDownload file ↗
{
  "sdk": {
    "version": "10.0.403",
    "rollForward": "latestPatch"
  }
}
Program.csDownload file ↗
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
var app = builder.Build(); app.MapGet("/item", () => TypedResults.Ok(new Item("rune", 3)));
app.MapPost("/item", Results<Ok<Item>, BadRequest> (Item item) => item.Quantity > 0 ? TypedResults.Ok(item) : TypedResults.BadRequest()); app.Run();
Item.csDownload file ↗
public sealed record Item(string Name, int Quantity);
AppJsonContext.csDownload file ↗
using System.Text.Json.Serialization;
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Item))]
public partial class AppJsonContext : JsonSerializerContext { }

Expected result

GET /item returns {"name":"rune","quantity":3}; a valid POST returns the same model and a nonpositive quantity returns 400. JSON reflection defaults are disabled. No NativeAOT publishing result is claimed.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using var bytes = new System.IO.MemoryStream(); using (var writer = new Utf8JsonWriter(bytes)) { writer.WriteStartObject(); writer.WriteString("name", "rune"); writer.WriteNumber("quantity", 3); writer.WriteEndObject(); }
using var document = JsonDocument.Parse(bytes.ToArray()); var root = document.RootElement; Console.WriteLine(root.GetProperty("name").GetString() + ":" + root.GetProperty("quantity").GetInt32());
Open C# practice

C# practice output

rune:3
Try this
Generated JSON metadata must cover actual wire types; a normal build is not evidence of a completed NativeAOT publish.

Further reading