# Source generator and analyzers

> Register every function class of a project with one generated call, and find the types a Native AOT app has not given its JsonSerializerContext while the project builds.

Web page: https://damikun.github.io/inngest-dotnet/guides/source-generator/

The `Inngest` package carries a source generator, two analyzers and a code fix. The compiler loads them from the
package, so there is nothing more to install: every project that references `Inngest`, directly or through
`Inngest.AspNetCore` or `Inngest.Connect`, has them. They are built against Roslyn 4.8, the compiler of the first .NET 8
SDK and of Visual Studio 17.8, and run in that compiler and in every newer one.

- The generator writes `AddInngestFunctions()`, which registers the function classes of the project. A project turns it
  on; until then it writes nothing, and the project builds as it did before.
- An analyzer, with the generator, says why a class with `[InngestFunction]` is not registered.
- The other analyzer, in an app that turns reflection-based JSON off, finds the types that no `JsonSerializerContext`
  lists. A code fix lists them.

## Register the function classes

Turn the generator on in the project file:

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

It is off by default so that a project which registers its classes by hand, or has a method of its own named
`AddInngestFunctions`, is not changed by an upgrade. Then give each function class `[InngestFunction]`, and register all
of them with one call:

```csharp title="Functions.cs"
using Inngest;

public sealed record OrderPlaced(string OrderId) : IInngestEventData
{
    public static string EventName => "shop/order.placed";
}

[InngestFunction("charge-order", Retries = 3)]
[EventTrigger<OrderPlaced>]
public sealed class ChargeOrder : InngestFunction<OrderPlaced>
{
    public override async Task<object?> ExecuteAsync(FunctionContext<OrderPlaced> context, CancellationToken cancellationToken)
    {
        await context.Step.RunAsync("charge", () => context.Logger.LogInformation("Charging {OrderId}", context.Event.Data.OrderId));
        return null;
    }
}

[InngestFunction("send-receipt")]
[EventTrigger<OrderPlaced>]
public sealed class SendReceipt : InngestFunction<OrderPlaced>
{
    public override Task<object?> ExecuteAsync(FunctionContext<OrderPlaced> context, CancellationToken cancellationToken) =>
        Task.FromResult<object?>(null);
}
```

```csharp title="Program.cs"
using Inngest;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddInngest(options => options.AppId = "shop")
    .AddInngestFunctions();

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

`AddInngestFunctions()` is the same as writing `AddFunction<ChargeOrder>()` and `AddFunction<SendReceipt>()`, for every
class. The compiler finds the classes while it builds the project, from the attribute, so there is no assembly scanning
at run time and nothing for the trimmer to keep. What the generator writes for the project above, without the comments and the `GeneratedCode` attribute:

```csharp
#pragma warning disable CS0612, CS0618

namespace Inngest
{
    internal static class InngestFunctionRegistration_Shop
    {
        internal static global::Inngest.IInngestBuilder AddInngestFunctions(this global::Inngest.IInngestBuilder builder)
        {
            global::System.ArgumentNullException.ThrowIfNull(builder);

            global::Inngest.InngestBuilderExtensions.AddFunction<global::ChargeOrder>(builder);
            global::Inngest.InngestBuilderExtensions.AddFunction<global::SendReceipt>(builder);

            return builder;
        }
    }
}
```

The order of the calls is the order of the classes' names, so adding a file never reorders them. The method is in the
`Inngest` namespace, with `AddFunction`, and it returns the builder, so more registrations can follow it. The file
disables the warnings for obsolete code, so that a function class marked `[Obsolete]` can be registered without a
warning in a file you cannot edit.

`AddFunction<T>()` still reads the attributes of each class when the app starts, as it does when you call it yourself.
The generator decides which classes to register, not what their options are.

### Classes it leaves out

The method registers a class that has `[InngestFunction]`, derives from `InngestFunction<TEventData>` or
`InngestFunction<TEventData, TResult>`, and that generated code in the same project can create and name. A class that
does not fit is left out, and the project builds with a warning on it that says why. These warnings are about the
generated method, so a project that has not turned generation on gets none of them: a generic class it closes by hand,
or a private class it registers from inside the class that holds it, is not a mistake there.

| Diagnostic | The class |
| - | - |
| [`INNGEST001`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest001) | has the attribute but is not a function: it does not derive from `InngestFunction<TEventData>` or `InngestFunction<TEventData, TResult>`. |
| [`INNGEST002`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest002) | is abstract. |
| [`INNGEST003`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest003) | is generic, or inside a generic class. |
| [`INNGEST004`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest004) | is private, `protected` or `private protected`, or file-local, or inside a type that is. A `protected internal` class is registered. |
| [`INNGEST005`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest005) | has no public constructor. |

Public and internal classes are registered, nested ones too, in any namespace or none. A class registered in code with
`FunctionOptions` needs no attribute, and so is not in the generated method.

### Several projects

Each project that turns generation on gets a method of its own, for its own classes, in the namespace `Inngest`. The
method is `internal` unless the project says otherwise, which keeps it from meeting the one in the project that
references it, but two cases make a call ambiguous (`CS0121`), and the compiler says so:

- A project that already has a method of its own called `AddInngestFunctions(this IInngestBuilder)`, such as a helper
  that registers its functions, meets the generated one as soon as it has a class with `[InngestFunction]`.
- A project that another project sees the internals of, through `InternalsVisibleTo`, such as a library and the test
  project of the app, shares the library's internal method with it. The test project's own method is then ambiguous with
  the library's.

`InngestFunctionsMethodName` renames the generated method, in either project. To register a library's functions in the
app, give the library a name of its own and make the method public:

```xml title="Shop.Billing.csproj"
<PropertyGroup>
  <InngestGenerateFunctionRegistration>true</InngestGenerateFunctionRegistration>
  <InngestFunctionsMethodName>AddBillingFunctions</InngestFunctionsMethodName>
  <InngestFunctionsAccessibility>public</InngestFunctionsAccessibility>
</PropertyGroup>
```

```csharp title="Program.cs"
builder.Services
    .AddInngest(options => options.AppId = "shop")
    .AddInngestFunctions()
    .AddBillingFunctions();
```

Two public methods with the same name in the same namespace are ambiguous in the same way, so a public method needs a name
of its own. A library that keeps the default internal method can instead register its functions in a public method of its
own: `public static IInngestBuilder AddBillingFunctions(this IInngestBuilder builder) => builder.AddInngestFunctions();`.

The class that holds the method is named after the assembly. Characters an identifier cannot hold become underscores, and a
name that had any also gets a short hash of the assembly's name, so that `Shop-Billing` and `Shop.Billing` do not become
the same class.

The MSBuild properties:

| Property | Default | Meaning |
| - | - | - |
| `InngestGenerateFunctionRegistration` | `false` | `true` writes the method and reports the classes it leaves out. |
| `InngestFunctionsMethodName` | `AddInngestFunctions` | The name of the generated method. It must be a C# identifier. |
| `InngestFunctionsAccessibility` | `internal` | `public` or `internal`, for the class and the method. |
| `DisableInngestAnalyzers` | `false` | `true` takes everything the package runs in the compiler out of the build: the generator and both analyzers. |

A value the generator cannot use is reported as [`INNGEST006`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest006), and the default is used.

### What it does not do

- It registers the classes of the project it compiles, not those of the projects it references. Each of those has its
  own method, or none.
- It leaves no class out on request. A class that has the attribute is registered by the method, and a second
  registration by hand fails when the app starts, because the function id is used twice. A project with a function that
  needs `AddFunction<T>(configure)` does not turn generation on, and registers all its functions by hand.
- It does not compute the options at compile time. They are read from the attributes when the app starts.

## Check the JSON of a Native AOT app

An app that is published with Native AOT or trimmed has no reflection-based JSON, so every type that Inngest reads or
writes for your functions must be listed by a source-generated `JsonSerializerContext`. A type that is not fails when
the function runs, with a `NotSupportedException`. The [Native AOT guide](https://damikun.github.io/inngest-dotnet/guides/native-aot.md) explains
the context. The analyzer finds the types that are missing while the project builds.

It runs when the project sets `PublishAot` or `PublishTrimmed` to `true`, or `JsonSerializerIsReflectionEnabledByDefault`
to `false`. A project that sets `JsonSerializerIsReflectionEnabledByDefault` to `true` is not checked. In any other
project the analyzer does nothing. It reads the properties of the project it compiles, and a library does not publish:
the function classes of a library that sets none of them are not checked, even when the app that uses the library is
published with Native AOT. Set `JsonSerializerIsReflectionEnabledByDefault` to `false` in the library to have it checked.
The property only matters to an app, so it changes nothing else in a library.

It looks at:

- the event data and the result type of every function class, that is, the `TEventData` and `TResult` of its base class;
- the event data type of a function registered with `AddFunction<TEventData>(options, handler)`;
- the type that a step saves with the run and reads back: the `T` of `RunAsync<T>`, the input and result of
  `AIWrapAsync`, the data that `WaitForEventAsync<T>` and `WaitForSignalAsync<T>` wait for, the result of
  `InvokeAsync`, the response of `InferAsync<T>` and the data `PublishAsync<T>` publishes.

A type counts as listed when a context lists it with `[JsonSerializable]`, or when a type that a context lists reaches it
the way the generator of `System.Text.Json` does when it writes the metadata of that type:

- through a public property or field, or one that is not public but has `[JsonInclude]`, unless it has `[JsonIgnore]`;
  a private, internal or protected member without `[JsonInclude]`, a static member and an explicit interface
  implementation are not part of the metadata;
- through a parameter of the constructor it would call: the one marked `[JsonConstructor]`, or the only public one when
  there is no public constructor without parameters. The parameters of any other constructor are not reached;
- as the element type of a collection, or a type argument of a framework generic such as `Dictionary<K, V>`. A class that
  is a collection, such as `class Items : List<Line>`, is written as its elements, and its own members and constructors
  are not part of the metadata;
- as a derived type named by `[JsonDerivedType]`.

A type with a `[JsonConverter]` of its own is written by the converter, and nothing is reached through its members. The
context can be in the project or in a project it references. The context that the SDK adds to the serializer options of
every app is counted too, so `FunctionFailure`, `SendEventsResult`, `JsonElement`, `object` and what those hold, such as
`string`, are never asked for. An anonymous type cannot be listed at all, so it gets a message of its own.

The analyzer's idea of what is listed is checked against the generator of `System.Text.Json`, of .NET 8 and .NET 10, by
the tests of this package.

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

public sealed record Refund(string OrderId);

public sealed record Receipt(string Id);

[InngestFunction("refund")]
public sealed class OnRefund : InngestFunction<Refund, Receipt>
{
    public override Task<Receipt> ExecuteAsync(FunctionContext<Refund> context, CancellationToken cancellationToken) =>
        Task.FromResult(new Receipt(context.Event.Data.OrderId));
}

[JsonSerializable(typeof(Refund))]
internal sealed partial class ShopJson : JsonSerializerContext;
```

With `PublishAot` on, the project above builds with a warning on `Receipt`:

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

The code fix, in the IDE, adds `[JsonSerializable(typeof(Receipt))]` to a context of the project, and the `using`
directive the type needs if the file lacks it. It offers one fix for each context the project has. Fixing all
occurrences in a document, a project or a solution adds every missing type to the chosen context in one edit, each type
once. It offers nothing when the project has no context: declare one first, as the Native AOT guide shows.

### Why the generator does not list the types itself

The generator knows these types, so it could write the `[JsonSerializable]` attributes. It cannot: the generator of
`System.Text.Json` reads the attributes of your source, and a source generator's output is not input to another
generator. Code that Inngest's generator added to the context would not be seen. The code fix writes the attribute into
your own file instead.

### What the analyzer does not check

- Values whose type is only known at run time, such as an `object` that holds a record. That includes the data that
  `InvokeAsync` sends, the events your code sends and the body of an `InferAsync` request. List those types yourself, as
  the [Native AOT guide](https://damikun.github.io/inngest-dotnet/guides/native-aot.md#which-types-to-list) says.
- Typed realtime topics, and publishing or subscribing through `IInngestRealtime`, which read their type through the same
  options. The data that a step publishes is checked.
- Contexts that are built in code, and options whose resolver chain does not contain the context. Test the paths that
  matter after you publish.

## Diagnostics

Every diagnostic has an id that `.editorconfig` can configure, such as `dotnet_diagnostic.INNGEST010.severity = none`,
and `#pragma warning disable` works on the class or the call the diagnostic is on. `DisableInngestAnalyzers` removes
them all, with the generator, for the case that one of them gets in the way of a build.

### INNGEST001

Warning. A class has `[InngestFunction]` but does not derive from `InngestFunction<TEventData>` or
`InngestFunction<TEventData, TResult>`. The attribute does nothing on any other class: the class cannot be registered,
by `AddFunction<T>()` or by the generated method, and Inngest never runs it. Add the base class, or remove the
attribute. Reported in a project that has turned generation on.

### INNGEST002

Warning. The class is abstract. The service provider cannot create it, and `[InngestFunction]` is not inherited, so the
classes that derive from it are not registered either. Put the attribute on each concrete class.

### INNGEST003

Warning. The class is generic, or inside a generic class. The generator does not know which type arguments the app
uses. Register the closed types with `AddFunction<T>()`, or suppress the warning on a class that is only a template.

### INNGEST004

Warning. The class, or a type that contains it, is private, `protected`, `private protected` or file-local. The generated
method is outside it. Make the class public or internal, with every type that contains it, or register it with
`AddFunction<T>()` from inside the containing type.

### INNGEST005

Warning. The class has no public constructor. The service provider calls public constructors only, so every call of the
function would fail. Add one.

### INNGEST006

Warning. `InngestGenerateFunctionRegistration` is neither `true` nor `false`, `InngestFunctionsMethodName` is not a C#
identifier, or `InngestFunctionsAccessibility` is neither `public` nor `internal`. The default is used.

### INNGEST010

Warning. A type that Inngest serializes for a function is missing from the `JsonSerializerContext`, in a project that
turns reflection-based JSON off. Add `[JsonSerializable(typeof(...))]` for it to a context, which the code fix does.

### INNGEST011

Warning. The type is anonymous, which no context can list. Use a record, a class or a struct, and list it.
