# Native AOT and trimming

> Publish an app with Inngest functions as a trimmed or Native AOT binary by giving the SDK source-generated JSON metadata for your types.

Web page: https://damikun.github.io/inngest-dotnet/guides/native-aot/

The SDK is trimming and AOT compatible: its own types use source-generated `System.Text.Json` metadata and it
does not rely on reflection. Your types are the only thing left to cover. Everything the SDK serializes for you,
event data, step results, invoke payloads and function output, goes through `InngestOptions.SerializerOptions`,
and in a trimmed or AOT app that needs a source-generated `JsonSerializerContext` that knows your types.

## Set it up

Turn on Native AOT in the project:

```xml title="Shop.csproj"
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

Declare a context with every type that crosses the wire, and put it first in the resolver chain:

```csharp title="Program.cs"
using System.Text.Json.Serialization;
using Inngest;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services
    .AddInngest(options =>
    {
        options.AppId = "shop";
        options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJson.Default);
    })
    .AddFunction<Greeting>(new FunctionOptions { Id = "greet", Triggers = [Trigger.Event("demo/greet")] },
        async (context, cancellationToken) =>
        {
            var reply = await context.Step.RunAsync("compose", () => new Reply($"Hello, {context.Event.Data.Name}!"));
            await context.Step.SleepAsync("pause", TimeSpan.FromSeconds(1));
            return reply;
        });

var app = builder.Build();
app.MapInngest();
app.Run();

public sealed record Greeting(string Name);

public sealed record Reply(string Text);

[JsonSerializable(typeof(Greeting))]
[JsonSerializable(typeof(Reply))]
internal sealed partial class AppJson : JsonSerializerContext;
```

`samples/Inngest.AotSample` in the repository is this app. It publishes with no trimming or AOT warnings and runs
against the dev server as a native binary:

```bash
dotnet publish -c Release -r linux-x64
```

## Which types to list

List every type that the SDK serializes or deserializes for your code:

- the event data type of every function, `TEventData`;
- the result type of every `RunAsync`, the `T` in `Task<T>`;
- the type each function returns as its output;
- the `TData` of `WaitForEventAsync<TData>`, and the `TResult` and `data` of `InvokeAsync`;
- the runtime types of `Data` and `User` in the events you send;
- for each function that runs the chunks of a [fan-out](https://damikun.github.io/inngest-dotnet/guides/fan-out.md#choosing-the-sizes),
  `FanOutChunk<TItem>` and `FanOutOutcome<TResult>[]`. The methods return arrays, and the engine writes a function's
  output by its runtime type, so a function that returns what `MapAsync` gave lists `TResult[]`.

The SDK covers its own public types, such as `FunctionFailure`, `SendEventsResult`, `JsonElement` and `object`, so a
step without a result or an untyped event needs nothing extra.

Anonymous types cannot be source generated, so use records where the other guides use `new { ... }`. A type the
context does not know fails at run time with a `NotSupportedException` from `System.Text.Json`, in the step or call
that serializes it; the publish warnings will not catch it, so test the paths that matter.

## Let the build find what is missing

A forgotten type is the one thing the publish warnings do not catch. The `Inngest` package therefore includes an
analyzer that runs in a project which turns reflection-based JSON off: `PublishAot` or `PublishTrimmed` set to `true`,
or `JsonSerializerIsReflectionEnabledByDefault` set to `false`. It warns about every type of the list above that no
context lists, for the event data and the result of your function classes, the event data of a delegate function and the
types the steps save, such as the result of each `RunAsync<T>`:

```text
warning INNGEST010: 'Receipt' is the result of function 'OnRefund', but no JsonSerializerContext lists it with
[JsonSerializable], and reflection-based JSON is turned off
```

In the IDE, a code fix adds the `[JsonSerializable]` attribute to your context. It is an analyzer and not a generator
that fills the context, because the generator of `System.Text.Json` does not see code that another generator writes.
[Source generator and analyzers](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#check-the-json-of-a-native-aot-app) says what
the analyzer checks and what it leaves to you, and covers the generator that registers the function classes.

## Webhooks

[`MapInngestWebhook`](https://damikun.github.io/inngest-dotnet/guides/webhooks.md) needs nothing added to the context. The payload of a webhook is a
`JsonElement`, which the SDK covers, so the event goes out as the provider sent it, and the verifiers use
`System.Security.Cryptography` without reflection. What stays yours is the data type of the function that receives the
event, as for any event, and the type a `Transform` puts in `Data` instead of the payload, such as a `JsonObject` with a
header added. An app that maps endpoints for Stripe, GitHub, Shopify, Clerk and an HMAC provider publishes as a Native AOT
binary without warnings, and verifies and sends their webhooks. `samples/Inngest.AotSample` maps a Stripe endpoint, so
the publish in CI covers the package.

## Class functions and middleware

`AddFunction<TFunction>` and `AddMiddleware<TMiddleware>` keep the constructors they need through
`DynamicallyAccessedMembers`, so class-based functions and middleware work in AOT apps without extra configuration.
`AddInngestFunctions()`, which the [source generator](https://damikun.github.io/inngest-dotnet/guides/source-generator.md) writes once a project sets
`InngestGenerateFunctionRegistration` to `true`, calls `AddFunction<TFunction>` for each class that has `[InngestFunction]`,
so the app lists no class and nothing scans for them. The AOT sample does this.

Attribute classes work too: `AddFunction<TFunction>()` reads the attributes of the class it is given when the app
starts, the generic ones such as `[EventTrigger<T>]` included, which needs no reflection beyond what trimming keeps.
Typed events need nothing either, since their names are static members. The
[AOT sample](https://github.com/damikun/inngest-dotnet/tree/main/samples/Inngest.AotSample) registers an attribute
class, sends it a typed event and invokes another function by its class, and publishes without warnings. Settings
from a configuration section, `AddInngest(builder.Configuration.GetSection("Inngest"))`, are read key by key, not
through the configuration binder, so they work in AOT apps as well.

## Packages that are not compatible

`Inngest.EntityFrameworkCore` is the exception among the packages. Entity Framework Core is not trimming or Native AOT
compatible, and the outbox runs on it, so the package does not declare itself compatible. Its code has no trim or AOT
warning, and the events it stores are serialized from the same `InngestOptions.SerializerOptions` metadata as every
other event, but an app that publishes with Native AOT cannot use it. See [Transactional outbox](https://damikun.github.io/inngest-dotnet/guides/outbox.md).

Microsoft's [Native AOT deployment](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) and
[source generation](https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/source-generation)
docs cover the general rules.
