Native AOT Support¶
AlexaVoxCraft supports publishing consumer skills with PublishAot=true. Every package AlexaVoxCraft ships its own generated serialization metadata for; your own request/response types and any types you exchange with SMAPI need one extra step, described below.
Quick summary¶
- No code changes are required to keep running under the JIT (
dotnet run, ordinarydotnet publishwithoutPublishAot). - Publishing with
PublishAot=trueworks out of the box for every type AlexaVoxCraft itself owns (core requests/responses, APL components, In-Skill Purchasing directives, SMAPI models). - For your own types - a custom session-state POCO, a custom SMAPI
TRequest/TResponseforAlexaSkillInvocationClient.InvokeAsync, anything you serialize throughAlexaJsonOptions.DefaultOptions- register your own source-generatedJsonSerializerContextonce at startup.
Registering your own types¶
Declare a JsonSerializerContext for your types using System.Text.Json's built-in source generator, and register it once:
using System.Text.Json.Serialization;
using AlexaVoxCraft.Model.Serialization;
public class GameState
{
public int Score { get; set; }
public string? Level { get; set; }
}
[JsonSerializable(typeof(GameState))]
internal partial class MySkillJsonContext : JsonSerializerContext
{
}
// At startup, once:
AlexaJsonOptions.RegisterTypeInfoResolver(MySkillJsonContext.Default);
Make the context internal when the type it covers and the RegisterTypeInfoResolver call live in the same assembly - the common case for a single-project skill. In a modular application where the context lives in one assembly (e.g. a shared contracts project) and another assembly performs the registration, declare it public instead so MySkillJsonContext.Default is visible across that boundary:
// Contracts assembly
[JsonSerializable(typeof(GameState))]
public partial class GameStateJsonContext : JsonSerializerContext
{
}
// Different assembly, at startup:
AlexaJsonOptions.RegisterTypeInfoResolver(GameStateJsonContext.Default);
After this call, GameState (and every other type declared with [JsonSerializable] on MySkillJsonContext) works everywhere AlexaVoxCraft uses AlexaJsonOptions.DefaultOptions - JsonAttributeBag.Set<T>/Get<T> for session/persistent attributes, AlexaSkillInvocationClient.InvokeAsync<TRequest, TResponse> for your own request/response bodies, and any AlexaVoxCraft.Http-based client's default serialization path.
For AlexaSkillInvocationClient.InvokeAsync<TRequest, TResponse> specifically, register the closed envelope types too - SmapiModelContext intentionally does not include them, since it cannot know your TRequest/TResponse ahead of time:
[JsonSerializable(typeof(GameState))]
[JsonSerializable(typeof(SkillInvocationRequest<GameState>))]
[JsonSerializable(typeof(SkillInvocationResponse<GameState>))]
[JsonSerializable(typeof(SkillInvocationBody<GameState>))]
[JsonSerializable(typeof(SkillInvocationResult<GameState>))]
[JsonSerializable(typeof(SkillExecutionInfo<GameState>))]
[JsonSerializable(typeof(InvocationResponseInfo<GameState>))]
internal partial class MySkillJsonContext : JsonSerializerContext
{
}
Registering only GameState is enough for JsonAttributeBag/AlexaVoxCraft.Http paths, but InvokeAsync<GameState, GameState> will fail with the missing-metadata error below until the envelope types above are also registered.
Registration order doesn't matter relative to when clients or the mediator were constructed - a registration made after a client already exists is still picked up before that client's next actual serialize/deserialize call.
Custom request types handled only by a default handler¶
If your skill defines its own AlexaVoxCraft.Model.Request.Type.Request subclass (via a custom IRequestTypeResolver) and that request type is dispatched only to an IDefaultRequestHandler - never to a specific IRequestHandler<T> - it must still appear as a [JsonSerializable] root in a JsonSerializerContext registered with AlexaJsonOptions.RegisterTypeInfoResolver. The generated DI registration discovers which request types a default handler might receive by scanning [JsonSerializable] roots on every JsonSerializerContext in AlexaVoxCraft.Model/AlexaVoxCraft.Model.* assemblies plus your own skill's assembly; a custom request type with no such root is invisible to that scan. Under Native AOT this means SkillMediator.Send falls back silently to the unsupported MakeGenericType path for that request type instead of using the generated keyed-DI dispatch - register the type as a root even if nothing else in your code serializes it directly.
JIT fallback vs. Native AOT requirement¶
When reflection is available (any normal dotnet run/dotnet publish without PublishAot), AlexaVoxCraft falls back to reflection-based serialization for a type it doesn't otherwise have metadata for - registering your own context is optional there, purely a performance/startup-time optimization.
Under PublishAot=true, that reflection fallback is compiled out entirely. A type with no registered metadata fails immediately and clearly:
rather than a confusing runtime failure deep inside serialization. If you see this under a Native AOT build, it means a type you're serializing needs to be added to your own JsonSerializerContext and registered via RegisterTypeInfoResolver.
Your own JsonSerializerOptions¶
If you construct an AlexaVoxCraft.Http-based client (or any SMAPI client) with your own explicit JsonSerializerOptions, AlexaVoxCraft never mutates or replaces it - you own that configuration completely, including its TypeInfoResolver. The guidance above applies only to AlexaVoxCraft's own default options paths.
Verifying your own skill¶
The same shape this repository uses to validate itself (test/AlexaVoxCraft.NativeAot.ValidationApp in this repository's source) is a reasonable template: a small console entry point exercising your handlers and serialization paths, published with:
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
then published (dotnet publish -r linux-x64 -p:PublishAot=true, matching AWS Lambda's provided.al2023 runtime) and actually executed - not just built - to confirm your own handlers and types work end to end.
Source-tree note for interceptor-based DI registration¶
If you reference AlexaVoxCraft.MediatR via a source-tree ProjectReference (rather than a published NuGet package), also add a direct ProjectReference to AlexaVoxCraft.MediatR.Generators with OutputItemType="Analyzer" so its interceptor participates in your build - a plain ProjectReference to AlexaVoxCraft.MediatR alone does not pull in its interceptor generator (that only happens automatically for a packaged NuGet consumer). This does not apply to consumers installing AlexaVoxCraft.MediatR from NuGet.