Source generator and analyzers
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
JsonSerializerContextlists. A code fix lists them.
Register the function classes
Section titled “Register the function classes”Turn the generator on in the project file:
<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:
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);}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:
#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
Section titled “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 |
has the attribute but is not a function: it does not derive from InngestFunction<TEventData> or InngestFunction<TEventData, TResult>. |
INNGEST002 |
is abstract. |
INNGEST003 |
is generic, or inside a generic class. |
INNGEST004 |
is private, protected or private protected, or file-local, or inside a type that is. A protected internal class is registered. |
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
Section titled “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:
<PropertyGroup> <InngestGenerateFunctionRegistration>true</InngestGenerateFunctionRegistration> <InngestFunctionsMethodName>AddBillingFunctions</InngestFunctionsMethodName> <InngestFunctionsAccessibility>public</InngestFunctionsAccessibility></PropertyGroup>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, and the default is used.
What it does not do
Section titled “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
Section titled “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 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
TEventDataandTResultof 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
TofRunAsync<T>, the input and result ofAIWrapAsync, the data thatWaitForEventAsync<T>andWaitForSignalAsync<T>wait for, the result ofInvokeAsync, the response ofInferAsync<T>and the dataPublishAsync<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 asclass 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.
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:
warning INNGEST010: 'Receipt' is the result of function 'OnRefund', but no JsonSerializerContext lists it with[JsonSerializable], and reflection-based JSON is turned offThe 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
Section titled “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
Section titled “What the analyzer does not check”- Values whose type is only known at run time, such as an
objectthat holds a record. That includes the data thatInvokeAsyncsends, the events your code sends and the body of anInferAsyncrequest. List those types yourself, as the Native AOT guide 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “INNGEST011”Warning. The type is anonymous, which no context can list. Use a record, a class or a struct, and list it.
inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.