# C# API overview

> The public types of the Inngest, Inngest.AspNetCore, Inngest.AwsLambda, Inngest.AzureFunctions, Inngest.Blazor, Inngest.Connect, Inngest.Aspire.Hosting, Inngest.OpenTelemetry, Inngest.Encryption, Inngest.Sentry, Inngest.SignalR, Inngest.EntityFrameworkCore, Inngest.Extensions.AI, Inngest.Testing, Inngest.Testing.Containers, Inngest.Testing.Containers.AspNetCore, Inngest.UserContext, Inngest.UserContext.AspNetCore and Inngest.Webhooks packages, the method and diagnostics of the source generator in Inngest, the templates of Inngest.Templates and the commands of dotnet-inngest, with what they are for.

Web page: https://damikun.github.io/inngest-dotnet/reference/api/

Every public type of the SDK, grouped by what it is for. The guides explain how to use them; this page is for
looking things up. Types are in the `Inngest` namespace unless noted.

## Registration

| Member | Purpose |
| - | - |
| `IServiceCollection.AddInngest(Action<InngestOptions>?)` | Registers the SDK and returns an `IInngestBuilder`. In `Microsoft.Extensions.DependencyInjection`. |
| `IServiceCollection.AddInngest(IConfiguration, Action<InngestOptions>?)` | The same, with settings read from a configuration section such as `Inngest` in `appsettings.json`. |
| `IInngestBuilder.AddFunction<TFunction>(FunctionOptions)` | Registers a class function with these options, resolved from a scope for every call. Attributes on the class are ignored. |
| `IInngestBuilder.AddFunction<TFunction>()` | Registers a class function configured by its [attributes](https://damikun.github.io/inngest-dotnet/reference/api.md#attributes). |
| `IInngestBuilder.AddFunction<TFunction>(Func<FunctionOptions, FunctionOptions>)` | Registers a class function with the options its attributes give, as the callback changes them. |
| `IInngestBuilder.AddInngestFunctions()` | Written into your project by the source generator, once the project sets `InngestGenerateFunctionRegistration` to `true`: registers every class that has `[InngestFunction]`, as `AddFunction<TFunction>()` does. See [Source generator and analyzers](https://damikun.github.io/inngest-dotnet/reference/api.md#source-generator-and-analyzers). |
| `IInngestBuilder.AddFunction<TEventData>(FunctionOptions, handler, onFailure?)` | Registers a delegate function with typed event data. |
| `IInngestBuilder.AddFunction(FunctionOptions, handler, onFailure?)` | Registers a delegate function with `JsonElement` event data. |
| `IInngestBuilder.AddMiddleware<TMiddleware>()` | Registers middleware, one instance per call. |
| `IInngestBuilder.AddMiddleware(InngestMiddleware)` | Registers one middleware instance for all calls. |
| `IInngestBuilder.AddConnect(Action<ConnectOptions>?)` | Runs the functions in a Connect worker. Package `Inngest.Connect`. |
| `IInngestBuilder.AddEntityFrameworkOutbox<TContext>(Action<InngestOutboxOptions>?)` | Adds the transactional outbox and its hosted dispatcher for a `DbContext`. Package `Inngest.EntityFrameworkCore`. See [Entity Framework Core outbox](https://damikun.github.io/inngest-dotnet/reference/api.md#entity-framework-core-outbox). |
| `IEndpointRouteBuilder.MapInngest(string pattern = "/api/inngest")` | Maps the endpoint Inngest calls. Package `Inngest.AspNetCore`, namespace `Microsoft.AspNetCore.Builder`. |
| `IEndpointRouteBuilder.MapInngestRealtime(string pattern, Func<HttpContext, ValueTask<RealtimeSubscription?>> subscription)` | Serves realtime messages to browsers as server-sent events; the callback returns the `RealtimeSubscription(string Channel, IReadOnlyList<string> Topics)` a request may receive, or null for 403. Package `Inngest.AspNetCore`, namespace `Microsoft.AspNetCore.Builder`. See [Realtime](https://damikun.github.io/inngest-dotnet/guides/realtime.md#with-server-sent-events). |
| `IHealthChecksBuilder.AddInngest(string? name = null, HealthStatus? failureStatus = null, IEnumerable<string>? tags = null, TimeSpan? timeout = null)` | Adds a health check that tells a readiness probe whether an app that serves functions can work with Inngest: its keys are configured and Inngest answers a request that carries the signing key. Name `"inngest"` unless given. Package `Inngest.AspNetCore`, in `Microsoft.Extensions.DependencyInjection`. See [Health checks](https://damikun.github.io/inngest-dotnet/guides/deploying.md#health-checks). |
| `IHealthChecksBuilder.AddInngest(Action<InngestHealthCheckOptions>, string? name = null, HealthStatus? failureStatus = null, IEnumerable<string>? tags = null, TimeSpan? timeout = null)` | The same, with the options set by the callback. |
| `IApplicationBuilder.UseInngestDurableEndpoints()` | Runs the endpoints marked as durable as Inngest runs. Add it after routing. Package `Inngest.AspNetCore`, namespace `Microsoft.AspNetCore.Builder`. See [Durable endpoints](https://damikun.github.io/inngest-dotnet/guides/durable-endpoints.md). |
| `IEndpointConventionBuilder.WithDurableEndpoint(DurableEndpointOptions? options = null)` | Marks an endpoint as durable, so that its handler can take `IStepTools` and run steps. Also `WithDurableEndpoint(string id)`. Package `Inngest.AspNetCore`, namespace `Microsoft.AspNetCore.Builder`. |
| `IServiceCollection.AddInngestRealtimeServerSource()` | Makes Blazor components read realtime messages from Inngest through `IInngestRealtime`, on the server. Package `Inngest.Blazor`. In `Microsoft.Extensions.DependencyInjection`. See [Realtime in Blazor](https://damikun.github.io/inngest-dotnet/guides/blazor.md). |
| `IServiceCollection.AddInngestRealtimeRelaySource(Action<RealtimeRelayOptions>)` | Makes Blazor components read the app's `MapInngestRealtime` endpoint over an `HttpClient`, in WebAssembly. Returns the `IHttpClientBuilder` of that client. Package `Inngest.Blazor`. The source registered last, of the two, is the one components get. |
| `IEndpointRouteBuilder.MapInngestWebhook(string pattern, WebhookProvider provider, WebhookOptions? options = null)` | Maps a POST endpoint that verifies a provider's webhooks and sends each as an event with the delivery id as its id. Package `Inngest.AspNetCore`, which brings in `Inngest.Webhooks`, namespace `Microsoft.AspNetCore.Builder`. See [Webhooks](https://damikun.github.io/inngest-dotnet/guides/webhooks.md). |
| `IHttpClientBuilder.AddInngestStepFetch()` | Turns the requests an `HttpClient` sends from a running function into fetch steps, which Inngest makes. Requests from step handlers and from outside functions go out as usual. Add it last. In `Microsoft.Extensions.DependencyInjection`. |
| `IInngestBuilder.Services` | The service collection the SDK is registered in. |

`AddInngest` also registers `IInngestClient`, `IInngestRunsClient` and `IInngestRealtime`, and `IStepTools`, which
resolves to the steps of the durable endpoint or function that is running.

## Options

**`InngestOptions`**: `AppId`, `AppVersion`, `EventKey`, `SigningKey`, `SigningKeyFallback`, `Environment`,
`IsDev`, `DevServerUrl`, `BaseUrl`, `ApiBaseUrl`, `EventApiBaseUrl`, `ServeOrigin`, `ServePath`, `AllowInBandSync`,
`AllowUnsignedSync`, `Streaming`, `SessionPropagation`, `Checkpointing`, `SerializerOptions`. See
[Configuration](https://damikun.github.io/inngest-dotnet/guides/configuration.md).

**`ConnectOptions`**: `InstanceId`, `MaxWorkerConcurrency`, `GatewayUrl`. See [Connect](https://damikun.github.io/inngest-dotnet/guides/connect.md).

**`InngestHealthCheckOptions`**: `RequireEventKey`, `CheckSync`, `CacheDuration` (30 seconds) and `FailureCacheDuration`
(5 seconds). See [Health checks](https://damikun.github.io/inngest-dotnet/guides/deploying.md#health-checks).

**`FunctionOptions`**, an immutable record:

| Property | Type |
| - | - |
| `Id` (required) | `string` |
| `Name` | `string?` |
| `Triggers` (required) | `IReadOnlyList<FunctionTrigger>` |
| `Retries` | `int?` |
| `Concurrency` | `IReadOnlyList<ConcurrencyLimit>?` |
| `Throttle`, `RateLimit`, `Debounce`, `BatchEvents`, `Priority` | records of the same names |
| `Idempotency` | `string?` |
| `CancelOn` | `IReadOnlyList<CancelOn>?` |
| `Timeouts` | `FunctionTimeouts?` |
| `Singleton` | `Singleton?` |
| `Middleware` | `IReadOnlyList<FunctionMiddleware>?`; see [Middleware](https://damikun.github.io/inngest-dotnet/reference/api.md#middleware) |
| `Checkpointing` | `CheckpointingOptions?` |

The flow control records and their constructor arguments and properties:

| Record | Constructor | Properties |
| - | - | - |
| `ConcurrencyLimit` | `(int Limit)`; 0 means no limit | `Key`, `Scope` (`ConcurrencyScope.Function`, `Environment`, `Account`) |
| `Throttle` | `(int Limit, TimeSpan Period)` | `Burst`, `Key` |
| `RateLimit` | `(int Limit, TimeSpan Period)` | `Key` |
| `Debounce` | `(TimeSpan Period)` | `Key`, `Timeout` |
| `BatchEvents` | `(int MaxSize, TimeSpan Timeout)` | `Key`, `If` |
| `Priority` | `(string Run)` | |
| `CancelOn` | `(string Event)` | `If`, `Match`, `Timeout` |
| `FunctionTimeouts` | | `Start`, `Finish` |
| `Singleton` | `(SingletonMode Mode)` | `Key`; modes `Skip` and `Cancel` |

**Triggers**: `Trigger.Event(string name, string? @if = null)` and `Trigger.Event<T>(string? @if = null)`, for a
typed event `T`, return an `EventTrigger`. `Trigger.Cron(string schedule, string? timezone = null)` and
`Trigger.Cron(string schedule, TimeSpan jitter, string? timezone = null)` return a `CronTrigger`, whose `Jitter`, from
1 second to 5 minutes, starts each scheduled run up to that much later. `EventTrigger` and `CronTrigger` derive from
`FunctionTrigger`. `CancelOn.For<T>()` returns a `CancelOn` for a typed event.

**`CheckpointingOptions`**, a record. Checkpointing runs the steps of a function one after another in the same call
from Inngest and saves each to Inngest as it completes, instead of ending the call after every step. It is off by
default. `InngestOptions.Checkpointing` sets it for every function, and `FunctionOptions.Checkpointing`, which wins
over it, for one function.

| Property | Default | Meaning |
| - | - | - |
| `Enabled` | `true` | `false` turns checkpointing off for one function when the app turns it on. |
| `MaxRuntime` | null: 10 seconds for the serve endpoint, 5 minutes for Connect workers | How long one call may keep running steps. `Timeout.InfiniteTimeSpan` removes the limit. |
| `BufferedSteps` | `1` | How many completed steps are saved to Inngest together. |
| `MaxInterval` | null | The longest a completed step waits to be saved while `BufferedSteps` is not reached; null waits until the buffer is full or the call ends. |

## Attributes

Each sets the `FunctionOptions` member of the same name; durations are time strings such as `"30s"` or `"1h30m"`.
`AddFunction<TFunction>()` reads them when the app starts: it starts from `[InngestFunction]` and applies the others,
which derive from `FunctionOptionsAttribute`, in ascending `Order`, then in the order they are declared.

| Attribute | Constructor | Properties |
| - | - | - |
| `InngestFunctionAttribute` | `(string id)` | `Name`, `Retries`, `Idempotency` |
| `EventTriggerAttribute`, `EventTriggerAttribute<T>` | `(string name)`, `()` | `If`; repeatable |
| `CronTriggerAttribute` | `(string schedule)` | `Timezone`, `Jitter`; repeatable |
| `ConcurrencyAttribute` | `(int limit)` | `Key`, `Scope`; repeatable |
| `ThrottleAttribute` | `(int limit, string period)` | `Burst`, `Key` |
| `RateLimitAttribute` | `(int limit, string period)` | `Key` |
| `DebounceAttribute` | `(string period)` | `Key`, `Timeout` |
| `BatchEventsAttribute` | `(int maxSize, string timeout)` | `Key`, `If` |
| `PriorityAttribute` | `(string run)` | |
| `SingletonAttribute` | `(SingletonMode mode)` | `Key` |
| `CancelOnAttribute`, `CancelOnAttribute<T>` | `(string name)`, `()` | `If`, `Match`, `Timeout`; repeatable |
| `TimeoutsAttribute` | | `Start`, `Finish` |
| `FunctionOptionsAttribute` | | `Order`; `Apply(FunctionOptions)` returns the options with the attribute applied. Derive from it and override `Configure(FunctionOptions)` for an attribute of your own. |

The generic forms take the event's name from `T`, which implements `IInngestEventData`.

## Functions

| Type | Purpose |
| - | - |
| `InngestFunction<TEventData>` | Base class of class functions. Override `ExecuteAsync(FunctionContext<TEventData>, CancellationToken)`, which returns `Task<object?>`. |
| `InngestFunction<TEventData, TResult>` | Base class of class functions with a result type: `ExecuteAsync` returns `Task<TResult>`. `InngestFunction<TEventData>` derives from it with `object?`. |
| `InngestFunction` | The base of both, which the methods that take a function class require. A class cannot derive from it directly. |
| `IReturns<TResult>` | Implemented by `InngestFunction<TEventData, TResult>`, so that `InvokeAsync<TFunction, TResult>` can require the result type. |
| `FunctionContext` | What every call has: `FunctionId`, `RunId`, `Attempt`, `MaxAttempts`, `Step`, `Logger`, `Services`, `CancellationToken`, `RequestId`, `JobId` and `Sessions`, the run's sessions that what it sends inherits. For code that does not know the event type, such as middleware: `EventName`, `EventId` and `RawEvent`, the triggering event as JSON. |
| `FunctionContext<TEventData>` | Adds `Event` and `Events`. |
| `IFailureHandler` | Implement `OnFailureAsync(FunctionContext<FunctionFailure>, CancellationToken)` to handle final failures. |
| `FunctionFailure` | The failure handler's event data: `FunctionId`, `RunId`, `Error`, `Event`. |
| `FunctionError` | A failure as Inngest stores it: `Name`, `Message`, `Stack`, and `Cause`, the error that caused it. |
| `SystemEvents` | The names of the events Inngest sends about runs: `FunctionFailed`, `FunctionFinished`, `FunctionCancelled`, `FunctionInvoked`, `ScheduledTimer`. |
| `FunctionFinished` | The data of `inngest/function.finished`, sent when a run ends however it ends: `FunctionId`, `RunId`, `Result`, `Error`, `Event`, `CorrelationId`. |
| `FunctionCancelled` | The data of `inngest/function.cancelled`: `FunctionId`, `RunId`, `Event`, `CorrelationId`. |

## Steps

**`IStepTools`**, the type of `context.Step`:

| Method | Returns |
| - | - |
| `RunAsync<T>(string id, Func<CancellationToken, Task<T>> handler)` | `Task<T>` |
| `SleepAsync(string id, TimeSpan duration)` | `Task` |
| `SleepUntilAsync(string id, DateTimeOffset until)` | `Task` |
| `WaitForEventAsync<TData>(string id, WaitForEventOptions options)` | `Task<InngestEvent<TData>?>` |
| `InvokeAsync<TResult>(string id, InvokeOptions options)` | `Task<TResult>` |
| `SendEventAsync(string id, IReadOnlyList<InngestEvent> events)` | `Task<SendEventsResult>` |
| `InvokeAsync<TFunction, TResult>(string id, object? data = null, TimeSpan? timeout = null)` | `Task<TResult>`; runs the function registered as `TFunction`, which must return a `TResult` |
| `WaitForSignalAsync<TData>(string id, WaitForSignalOptions options)` | `Task<InngestSignal<TData>?>`; null when the timeout passes first |
| `SendSignalAsync(string id, InngestSignal signal)` | `Task<string?>`: the id of the run the signal resumed, or null when no run waits for it |
| `FetchAsync(string id, HttpRequestMessage request)` | `Task<HttpResponseMessage>`; Inngest makes the request, and the response is memoized |
| `InferAsync<TResponse>(string id, InferOptions options)` | `Task<TResponse>`; Inngest sends the request to the AI provider, and the response is memoized |
| `AIWrapAsync<TInput, TResult>(string id, TInput input, Func<TInput, CancellationToken, Task<TResult>> handler)` | `Task<TResult>`; runs your own model call like a run step that the Inngest UI shows as one, with the input; a rerun with an input edited there passes the edited input |
| `RunScopedAsync<T>(string id, Func<IServiceProvider, CancellationToken, Task<T>> handler)` | `Task<T>`; runs the handler once like a run step, in a dependency injection scope of its own that is created when the handler starts and disposed when it ends. See [Scoped steps](https://damikun.github.io/inngest-dotnet/guides/scoped-steps.md) |
| `PublishAsync<T>(string id, RealtimeTopic topic, T data)` | `Task<T>`; publishes a [realtime](https://damikun.github.io/inngest-dotnet/reference/api.md#realtime) message once, tagged with the run; also for a `RealtimeTopic<T>` |
| `PublishStreamAsync(string id, RealtimeTopic topic, IAsyncEnumerable<string> chunks)` | `Task<string>`; streams the text to the topic as it arrives and memoizes all of it, so a replay does not read the chunks again |
| `ParallelAsync<T>(ParallelMode mode, Func<Task<T>> steps)` | `Task<T>`; runs the callback with the mode applied to every step it creates |

The members after the first six have default implementations, so a fake only has to implement the six. The defaults
of `InvokeAsync<TFunction, TResult>`, `WaitForSignalAsync`, `SendSignalAsync`, `FetchAsync`, `InferAsync`,
`AIWrapAsync`, `RunScopedAsync`, `PublishAsync` and `PublishStreamAsync` throw `NotSupportedException`. Every method but `ParallelAsync` also has an overload that takes a `StepOptions` in place of
the id.

`StepToolsExtensions` adds `RunAsync` overloads for handlers that take no token, return no value or are
synchronous, a non-generic `WaitForEventAsync` that returns `JsonElement` data, and `SendEventAsync` for a single
event. For a typed event `T` it adds `WaitForEventAsync<T>(string id, TimeSpan timeout, string? match = null,
string? @if = null)` and `SendEventAsync<T>` for one `T` or several. It also adds a non-generic `WaitForSignalAsync`
with `JsonElement` data, an `InferAsync` that returns `JsonElement`, an `AIWrapAsync` whose handler takes no token,
and `ParallelAsync(ParallelMode, Func<Task>)` for steps without a result. For scoped steps it adds a `RunScopedAsync`
whose handler returns no result, and `RunScopedAsync<TService, T>` and `RunScopedAsync<TService>`, whose handler takes
the `TService` that the SDK resolves from the step's scope with `GetRequiredService`. These methods also take a `StepOptions` in place of the id, except `ParallelAsync`.

| Record | Constructor | Properties |
| - | - | - |
| `StepOptions` | `(string Id)` | `Name`, shown in the Inngest UI; `ParallelMode`, for this step instead of the mode of an enclosing `ParallelAsync`. A `string` converts to it implicitly. |
| `WaitForEventOptions` | `(string Event, TimeSpan Timeout)` | `Match`, `If` |
| `InvokeOptions` | `(string FunctionId)` | `AppId`, `Data`, `User`, `Timeout`, `Meta`, the sessions of the invoked run |
| `WaitForSignalOptions` | `(string Signal, TimeSpan Timeout)` | `OnConflict`: `SignalConflict.Fail`, the default, fails the run when another run already waits for the signal; `Replace` takes the signal over |
| `InngestSignal` | `(string Name, object? Data = null)` | A signal to send. |
| `InngestSignal<TData>` | `(string Name, TData Data)` | A signal as the waiting run receives it. |
| `InferOptions` | `(AIFormat Format, Uri Url, object Body)` | `AuthKey`, `Headers` |

`ParallelMode` is `Wait`, the default, where Inngest calls the function again once all the steps started together
have finished, or `Race`, where it calls the function again each time one of them finishes. See
[Parallel steps](https://damikun.github.io/inngest-dotnet/guides/parallel-steps.md). `AIFormat` names the provider API: `OpenAIChat`,
`OpenAIResponses`, `Anthropic`, `Gemini`, `Grok` or `AzureOpenAI`.

## Fan-out

`FanOutExtensions` adds methods to `IStepTools` that run a step for each item of an `IReadOnlyList<TItem>`, with the id
`id[index]`, and return the results, as arrays like `Task.WhenAll`, in the order of the items. They need nothing from the
engine's public surface, so they also work on a fake of `IStepTools`. Each takes `FanOutOptions? options = null` and `CancellationToken cancellationToken = default`
after the arguments listed here. See [Fan-out over a list](https://damikun.github.io/inngest-dotnet/guides/fan-out.md).

| Method | Returns |
| - | - |
| `MapAsync<TItem, TResult>(string id, IReadOnlyList<TItem> items, Func<TItem, CancellationToken, Task<TResult>> handler)` | `Task<TResult[]>`; the first item that fails for good stops the list, and its `StepFailedException` is thrown |
| `ForEachAsync<TItem>(string id, IReadOnlyList<TItem> items, Func<TItem, CancellationToken, Task> handler)` | `Task`; the same for a handler without a result |
| `MapOutcomesAsync<TItem, TResult>(string id, IReadOnlyList<TItem> items, Func<TItem, CancellationToken, Task<TResult>> handler)` | `Task<FanOutOutcome<TResult>[]>`; every item runs, and each has an outcome |
| `MapInChunksAsync<TFunction, TItem, TResult>(string id, IReadOnlyList<TItem> items, int chunkSize)` | `Task<TResult[]>`; each chunk of `chunkSize` items runs in a run of the `FanOutFunction<TItem, TResult>` class `TFunction`, with the id `id[first-last]` |
| `MapOutcomesInChunksAsync<TFunction, TItem, TResult>(string id, IReadOnlyList<TItem> items, int chunkSize)` | `Task<FanOutOutcome<TResult>[]>` |

Inngest fails a run once 1,000 of its steps have completed, and every step of the run counts. So the methods without
chunks refuse a list of 1,000 items or more, and the chunked methods a `chunkSize` of 1,000 or more and a list that makes
1,000 chunks or more, with a `NonRetriableException`, which is not retried. Fewer fit when the function has other steps.

| Type | Members |
| - | - |
| `FanOutOptions`, a record | `MaxParallelism`: how many items, or chunks, run at once, all of them by default. `ParallelMode`: `Wait`, `Race`, or null for the mode of the enclosing `ParallelAsync`. `ChunkTimeout`: how long a chunk's run may take. |
| `FanOutOutcome<TResult>`, a record struct | `Index`, `Value`, `Error` (a `FunctionError?`) and `Succeeded` |
| `FanOutFunction<TItem, TResult>`, an abstract class | The function of a chunk. Implement `ProcessAsync(TItem item, CancellationToken cancellationToken)`; override `MaxParallelism`, the number of items of a chunk that run at once. Register it with `AddFunction<T>()`, with no trigger. |
| `FanOutChunk<TItem>`, a record | `(int Offset, IReadOnlyList<TItem> Items, bool FailFast)`: the event data of a chunk's run. A source-generated `JsonSerializerContext` needs it, and `FanOutOutcome<TResult>[]`, the output of the run. |

## Events

| Type | Purpose |
| - | - |
| `IInngestClient` | `SendAsync` for one `InngestEvent` or several, each also with a `SendOptions`; `SendSignalAsync(InngestSignal, CancellationToken)`, also with a `SendOptions`, which returns the id of the run the signal resumed, or null when no run waits for it; `AppId`. |
| `IInngestEventData` | Implemented by an event's data record: `static abstract string EventName` and `static virtual string? EventVersion`. |
| `InngestClientExtensions` | `SendAsync<T>(T, CancellationToken)` and `SendAsync<T>(IEnumerable<T>, CancellationToken)` for typed events. |
| `InngestEvent` | An event to send: `(string Name, object? Data = null)` with `Id`, `User`, `Timestamp`, `Version`, `Meta`. |
| `InngestEvent<TData>` | A received event: `Name`, `Data`, `Id`, `User`, `Timestamp`, `Version`, `Meta`. |
| `EventMeta` | An event's sessions: `Sessions`, its own, where a null id drops an inherited session; `PropagatedSessions`, those it inherits from the run that sends it, which the SDK fills in; `InheritSessions`, false to inherit none. See [Sending events](https://damikun.github.io/inngest-dotnet/guides/sending-events.md#sessions). |
| `SendEventsResult` | `Ids` of the sent events, in order. |
| `SendOptions` | Options for one send: `Environment`, a branch environment to send to instead of the app's. |

## Runs

**`IInngestRunsClient`** reads and cancels runs through the Inngest REST API, with the app's signing key:

| Method | Returns |
| - | - |
| `GetRunAsync(string runId, CancellationToken)` | `Task<FunctionRun>`: the run's status and, once it has ended, its output |
| `GetEventRunsAsync(string eventId, CancellationToken)` | `Task<IReadOnlyList<FunctionRun>>`: the runs an event started, by the event id that sending it returned |
| `CancelRunAsync(string runId, CancellationToken)` | `Task`; a step that is running finishes, and no further steps run |

**`FunctionRun`**, a record: `RunId`, `Status`, `FunctionId` (Inngest's own id of the function, a UUID),
`FunctionVersion`, `StartedAt`, `EndedAt`, `EventId`, `BatchId`, `OriginalRunId`, `Cron`, `EnvironmentId`, `Output`
and `GetOutput<T>()`, which reads the output with the app's serializer options. `Status` is a `FunctionRunStatus`:
`Unknown`, `Scheduled`, `Running`, `Completed`, `Failed` or `Cancelled`.

Inngest caches these answers for a few seconds, a run for 3 and the runs of an event for 15, and only `EndedAt` says
for sure that a run is over. Failures throw `InngestException` with Inngest's message.

## Realtime

**`IInngestRealtime`** publishes, streams and receives realtime messages and mints subscription tokens through the
Inngest API, with the app's signing key. See [Realtime](https://damikun.github.io/inngest-dotnet/guides/realtime.md).

| Method | Returns |
| - | - |
| `PublishAsync<T>(RealtimeTopic topic, T data, CancellationToken)` | `Task`; publishes at once, tagged with the run when called inside one. Text goes out as it is, anything else as JSON. Also for a `RealtimeTopic<T>`. |
| `PublishStreamAsync(RealtimeTopic topic, IAsyncEnumerable<string> chunks, CancellationToken)` | `Task<string>`: all the text, streamed to the topic in one request as it arrives |
| `SubscribeAsync<T>(RealtimeTopic<T> topic, CancellationToken)` | `IAsyncEnumerable<RealtimeMessage<T>>`: the topic's messages from now on, with the data read as a `T` |
| `SubscribeAsync(string channel, IReadOnlyList<string> topics, CancellationToken)` | `IAsyncEnumerable<RealtimeMessage<JsonElement>>` for several topics of a channel |
| `GetSubscriptionTokenAsync(string channel, IReadOnlyList<string> topics, CancellationToken)` | `Task<RealtimeToken>` for a browser to subscribe to these topics of the channel |

**`RealtimeTopic`**, a record `(string Channel, string Name)`, and **`RealtimeTopic<T>`**, the same for data of type
`T`, with `Untyped` for the members that take any topic. **`RealtimeMessage<T>`**, a record `(RealtimeMessageKind Kind,
string Channel, string Topic)` with `Data`, `StreamId`, `Text`, `RunId` and `CreatedAt`; `RealtimeMessageKind` is
`Data`, `StreamStart`, `StreamChunk`, `StreamEnd`, `Run` or `Other`. **`RealtimeToken`**, a record `(string Channel,
IReadOnlyList<string> Topics, string Key)` with `ApiBaseUrl`; serialized with camelCase names, it is the token object
that `useRealtime` and `subscribe` of the TypeScript SDK take.

## Durable endpoints

Package `Inngest.AspNetCore`. The API is marked `[Experimental("INNGEST001")]`, because Inngest calls durable endpoints
beta; [Durable endpoints](https://damikun.github.io/inngest-dotnet/guides/durable-endpoints.md#beta) shows how to suppress the diagnostic.

**`DurableEndpointOptions`**, the argument of `WithDurableEndpoint`:

| Property | Type | Default |
| - | - | - |
| `Id` | `string?` | The method and the route pattern, such as `POST /orders/{id}` |
| `Retries` | `int?` | 3 |
| `AsyncResponse` | `DurableEndpointResponse` | `Redirect` |
| `RedirectUrl` | `Func<DurableEndpointRun, Uri>?` | The output URL on the Inngest API |
| `MaxRequestBodySize` | `int` | 1 MiB; a larger request gets 413 |

**`DurableEndpointResponse`** is what the client gets when Inngest takes the run over: `Redirect`, a 303 to the URL that
answers with the endpoint's response, or `Token`, a 200 with `{"run_id", "token"}`. **`DurableEndpointRun`**, a record
`(string RunId, string Token)`, is what `RedirectUrl` receives.

**`DurableEndpointAttribute`** marks a controller or an action, as `WithDurableEndpoint` does for a minimal API
endpoint. Its constructor is `(string? id = null)`, and its properties are `Retries`, where a negative number means the
default, and `AsyncResponse`. **`DurableEndpointDiagnostics.Id`** is `"INNGEST001"`, the diagnostic that marks the API.

## Errors

| Exception | Meaning |
| - | - |
| `NonRetriableException` | Thrown by your code to fail a step or run without retrying. |
| `RetryAfterException` | Thrown by your code to retry after `Delay` or at `RetryAt`. |
| `StepFailedException` | Thrown by the SDK where a step that failed for good is awaited: `StepId`, `Error`. When the recorded error has a cause, `InnerException` is a `FunctionErrorException` for it. |
| `FunctionErrorException` | An error Inngest recorded, rebuilt as an exception: `Error` holds the recorded name, message and stack, and `InnerException` the recorded cause. |
| `InngestException` | Thrown by the SDK when talking to Inngest fails, with `StatusCode` when there is one. |

## Middleware

`InngestMiddleware` has virtual hooks that do nothing by default: `OnRunStartAsync`, `OnMemoizationEndAsync`,
`OnRunCompleteAsync`, `OnRunErrorAsync`, `OnStepStartAsync`, `OnStepCompleteAsync`, `OnStepErrorAsync`,
`WrapFunctionAsync`, `WrapStepAsync`, `WrapStepHandlerAsync` and `WrapSendEventAsync`. `StepInfo` describes a step:
`Id`, `HashedId`, `Type`, `Memoized`, and `Name`, the display name the code gave it. `Type` is a `StepType`: `Run`,
`Sleep`, `WaitForEvent`, `Invoke`, `SendEvent`, `WaitForSignal`, `SendSignal`, `Fetch`, `AIInfer`, `AIWrap` or
`RealtimePublish`. See
[Middleware](https://damikun.github.io/inngest-dotnet/guides/middleware.md).

**`FunctionMiddleware`** is middleware for one function, listed in `FunctionOptions.Middleware`. It runs inside the
app's middleware, also for the function's failure handler. `FunctionMiddleware.Of<TMiddleware>()` resolves
`TMiddleware` from the call's scope and registers it as scoped unless the app registered it;
`FunctionMiddleware.Of(Func<IServiceProvider, InngestMiddleware>)` calls the factory with the call's scope on every
call.

**`IPayloadTransform`**, implemented by a middleware, rewrites the raw JSON between Inngest and the function:

| Method | Rewrites |
| - | - |
| `TransformEvent(JsonElement @event, Utf8JsonWriter writer)` | A triggering event, before the function's event type is read from it |
| `TransformStepResult(FunctionContext context, StepInfo step, JsonElement result, Utf8JsonWriter writer)` | A memoized result of any step type, before the step's result type is read from it |
| `TransformStepOutput(FunctionContext context, StepInfo step, JsonElement output, Utf8JsonWriter writer)` | What a step handler returned, before Inngest memoizes it |
| `TransformFunctionOutput(FunctionContext context, JsonElement output, Utf8JsonWriter writer)` | The function's output, before it is sent to Inngest |

Each method writes one replacement value and returns `true`, or writes nothing and returns `false` to keep the value;
the defaults keep every value. JSON coming in reaches the transforms in registration order, and JSON going out in
reverse order.

## Connect

Package `Inngest.Connect`. `AddConnect` registers **`IInngestConnection`**, the worker's connection to Inngest:
`State`, and `WaitForStateChangedAsync(InngestConnectionState lastObservedState, CancellationToken)`, which completes
with the state the connection moves to. `InngestConnectionState` is `Connecting`, `Active`, `Reconnecting`, `Closing`
or `Closed`.

`IHealthChecksBuilder.AddInngestConnect(string name = "inngest-connect", HealthStatus? failureStatus = null,
IEnumerable<string>? tags = null)`, in `Microsoft.Extensions.DependencyInjection`, adds a health check that is healthy
only while the connection is `Active`. See [Deploying](https://damikun.github.io/inngest-dotnet/guides/deploying.md).

## Aspire

Package `Inngest.Aspire.Hosting`, for the AppHost project. The resource type is in `Aspire.Hosting.ApplicationModel` and
the extension methods, on `IDistributedApplicationBuilder` and `IResourceBuilder<T>`, are in `Aspire.Hosting`, the
namespaces an AppHost imports. See [Aspire](https://damikun.github.io/inngest-dotnet/guides/aspire.md).

| Member | Purpose |
| - | - |
| `AddInngest(string name, int? port = null, int? connectPort = null)` | Adds the dev server as a container running `inngest dev --no-discovery`, with health checks on `/health` and on the gateway's `/ready` and its UI linked in the dashboard. `port` and `connectPort` fix the host ports of the API and the gateway. The dev server is left out of a manifest. Returns an `IResourceBuilder<InngestServerResource>`. |
| `AddInngestSelfHosted(string name, IResourceBuilder<ParameterResource>? signingKey = null, IResourceBuilder<ParameterResource>? eventKey = null, int? port = null, int? connectPort = null)` | Adds a self-hosted server, `inngest start`, that signs its requests and wants an event key. Keys that are not given are generated and kept in the user secrets of the AppHost. A key that `inngest start` would refuse is reported when the server starts. The server is part of a manifest, with the keys as parameters. |
| `IResourceBuilder<InngestServerResource>.WithApp<TApp>(IResourceBuilder<TApp> app, string path = "/api/inngest", string endpointName = "http")` | Makes the server sync an app that serves functions, at the address its container reaches the endpoint on. Call it once for every app. |
| `IResourceBuilder<TDestination>.WithReference(IResourceBuilder<InngestServerResource> inngest)` | Gives a project or container `INNGEST_DEV` (the dev server) or `INNGEST_DEV=0`, `INNGEST_SIGNING_KEY` and `INNGEST_EVENT_KEY` (the self-hosted server), and for both `INNGEST_BASE_URL`, `INNGEST_API_BASE_URL` and `INNGEST_EVENT_API_BASE_URL` with the server's address, and `INNGEST_CONNECT_GATEWAY_URL` for workers. In a manifest a reference to the dev server adds nothing. |

**`InngestServerResource`**, a `ContainerResource`: `IsSelfHosted`; `SigningKeyParameter` and `EventKeyParameter`, which are
null for the dev server; `PrimaryEndpoint`, the endpoint named `http` that serves the REST API, the event API and the UI;
and `ConnectEndpoint`, the endpoint named `connect` that is the gateway of Connect workers. Its constructor takes the
name and, for a self-hosted server, both keys.

## AWS Lambda

Package `Inngest.AwsLambda`. See [Running on AWS Lambda](https://damikun.github.io/inngest-dotnet/guides/aws-lambda.md).

| Member | Purpose |
| - | - |
| `InngestLambdaHandler(IServiceProvider services)` | Takes the SDK from the service provider of an app that called `AddInngest`, and fails at once when it did not. Build one when the function starts. |
| `HandleAsync(APIGatewayProxyRequest request, ILambdaContext context)` | Serves an event of an API Gateway REST API (payload format 1.0) and returns an `APIGatewayProxyResponse`. |
| `HandleAsync(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)` | Serves an event of an API Gateway HTTP API or a Lambda function URL (payload format 2.0) and returns an `APIGatewayHttpApiV2ProxyResponse`. |

Each invocation runs in a service scope of its own. The cancellation token that functions receive is never cancelled,
and `InngestOptions.Streaming` has no effect, since the response is buffered; building the handler logs a warning when
it is on. The event types are those of `Amazon.Lambda.APIGatewayEvents`, and the package ships no
`JsonSerializerContext` for them.

## Azure Functions

Package `Inngest.AzureFunctions`.

| Member | Purpose |
| - | - |
| `HttpRequestData.ServeInngestAsync()` | Serves the Inngest endpoint from an HTTP-triggered function of the isolated worker: `GET` answers introspection, `PUT` syncs the app and `POST` runs functions. Returns the `HttpResponseData` the function returns. It works with the built-in HTTP model and with the ASP.NET Core integration, where `InngestOptions.Streaming` streams a long call. Requires `services.AddInngest()`. In `Microsoft.Azure.Functions.Worker.Http`. |

The function that calls it is declared in the app. See [Azure Functions](https://damikun.github.io/inngest-dotnet/guides/azure-functions.md).

## OpenTelemetry

Package `Inngest.OpenTelemetry`.

| Member | Purpose |
| - | - |
| `IInngestBuilder.AddInstrumentation()` | Adds the middleware that traces and measures runs. Add it before other middleware, so that their work falls inside its spans. |
| `TracerProviderBuilder.AddInngestInstrumentation()` | Collects the SDK's spans. In `OpenTelemetry.Trace`. |
| `MeterProviderBuilder.AddInngestInstrumentation()` | Collects the SDK's metrics. In `OpenTelemetry.Metrics`. |
| `InngestInstrumentation.ActivitySourceName`, `InngestInstrumentation.MeterName` | `"inngest"`, for `AddSource`, `AddMeter` or a listener of your own. |

The spans are `inngest.execution`, one per call from Inngest, and `inngest.step`, one per step the app executes; a
Connect worker adds `inngest.connect.request` for every request it receives. The metrics are `inngest.run.started`,
`inngest.run.completed`, `inngest.run.failed`, `inngest.retries`, `inngest.execution.duration` and
`inngest.step.duration`, with durations in seconds. The [tracing middleware example](https://damikun.github.io/inngest-dotnet/examples/tracing-middleware.md)
shows how to write such a middleware yourself.

## Encryption

Package `Inngest.Encryption`. See [Encryption](https://damikun.github.io/inngest-dotnet/guides/middleware.md#encryption).

| Member | Purpose |
| - | - |
| `IInngestBuilder.AddEncryption(Action<EncryptionOptions>?)` | Adds the middleware that encrypts event data, step results and function output and decrypts what comes back, in the format of `@inngest/middleware-encryption`. Register it after middleware that add or change events. |

**`EncryptionOptions`**:

| Member | Purpose |
| - | - |
| `Key` | The key that encrypts, and the first one tried when decrypting. Required unless `Service` is set. |
| `FallbackDecryptionKeys` | Keys that only decrypt, for replacing a key. Empty by default. |
| `DecryptOnly` | Reads encrypted data and writes none. `false` by default. |
| `EventEncryptionField` | The top-level field of an event's data that is encrypted when the event is sent. `"encrypted"` by default. |
| `Service` | An `EncryptionService` that encrypts in place of the default strategy. |

**`EncryptionService`**, an abstract class: `Identifier`, which the envelope's `__STRATEGY__` carries;
`Encrypt(ReadOnlySpan<byte> json)`, which returns the text of the `data` field; and `Decrypt(string data)`, which
returns the UTF-8 JSON of the value.

## Sentry

Package `Inngest.Sentry`.

| Member | Purpose |
| - | - |
| `IInngestBuilder.AddSentryReporting(Action<SentryReportingOptions>?)` | Adds the middleware that reports the errors of functions and steps to Sentry. Add it before other middleware, so that their work falls inside its scope. |

**`SentryReportingOptions`**: `OnlyCaptureFinalAttempt` (default `true`), `CaptureStepErrors` (`false`),
`DisableAutomaticFlush` (`false`) and `FlushTimeout` (2 seconds). See [Middleware](https://damikun.github.io/inngest-dotnet/guides/middleware.md#sentry)
for the tags, the trace and the options.

## AI chat clients

Package `Inngest.Extensions.AI`. The extension methods are in `Microsoft.Extensions.AI`. See
[AI chat clients](https://damikun.github.io/inngest-dotnet/guides/ai.md).

| Member | Purpose |
| - | - |
| `ChatClientBuilder.UseInngestSteps(Action<InngestStepsOptions>?)` | Makes each call to the model inside a run a step: the messages and options are its input, the `ChatResponse` as JSON its result, and a replay returns the saved response without calling the model. A streaming call runs the whole stream in its step. Outside a run, and inside a step handler, calls pass through. |
| `ChatClientBuilder.UseInngestFunctionInvocation(Action<FunctionInvokingChatClient>?, Action<InngestStepsOptions>?)` | Adds a `FunctionInvokingChatClient`, as `UseFunctionInvocation` does, whose invocations of tools inside a run are steps named `tool <name>`, with the arguments as input and `{"result": ...}` saved. A tool that throws is retried like any step, except for an `ArgumentException`, which fails its step at once. |
| `ChatOptions.WithInngestStepId(string)` | Names the step of one call, in place of `chat`, for calls that reach the client in no fixed order, such as ones made after a request that finishes at varying times. The steps of its tools carry the name. The client removes the name before the options reach the model. |
| `ChatOptions.WithInngestRealtimeStream(RealtimeTopic)` | Publishes the text of a streaming call to the topic while the model produces it, gathered into chunks of up to 1 KB, at most 1,000 to a stream. Failing to publish does not fail the call. |

**`InngestInferChatClient(Uri endpoint, string model, string? apiKey = null, IReadOnlyDictionary<string, string>? headers = null)`**,
in the `Inngest` namespace, is an `IChatClient` whose calls are inference steps in the OpenAI chat completions format: Inngest
makes the request, and no worker waits for it. It works inside a run only, sends `max_completion_tokens`, and fails for good on an answer that is an error or has no choices. A `strict` additional property of the options, or of a function, asks for strict structured outputs.

**`InngestStepsOptions`**, in the `Inngest` namespace: `RecordInput` (default `true`) records the messages and options of a
model call, or the arguments of a tool call, as the input of its step; without it the step is a plain run step.

## Source generator and analyzers

Part of the `Inngest` package, which carries them in `analyzers/dotnet/cs` for the compiler to load. See
[Source generator and analyzers](https://damikun.github.io/inngest-dotnet/guides/source-generator.md).

| Member | Purpose |
| - | - |
| `IInngestBuilder.AddInngestFunctions()` | An extension method that the generator writes into each project that has turned generation on and has function classes, in the namespace `Inngest`, in a static class named after the assembly. It calls `AddFunction<T>()` for every class with `[InngestFunction]` that derives from `InngestFunction<TEventData>` or `InngestFunction<TEventData, TResult>` and that generated code in the project can create and name, in the order of their names, and returns the builder. `internal` unless the project says `public`. |

The MSBuild properties of the project that the generator and the analyzers read:

| Property | Default | Purpose |
| - | - | - |
| `InngestGenerateFunctionRegistration` | `false` | `true` writes the method and reports the classes it leaves out (`INNGEST001` to `INNGEST005`). Nothing is written or reported until it is set. |
| `InngestFunctionsMethodName` | `AddInngestFunctions` | The name of the generated method. |
| `InngestFunctionsAccessibility` | `internal` | `public` or `internal`. |
| `DisableInngestAnalyzers` | `false` | `true` removes everything the package runs in the compiler, the generator and both analyzers, from the build. |
| `PublishAot`, `PublishTrimmed`, `JsonSerializerIsReflectionEnabledByDefault` | | Read by the JSON analyzer, which is active when reflection-based JSON is off. |

The diagnostics:

| Id | Severity | Reported when |
| - | - | - |
| [`INNGEST001`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest001) | Warning | A class has `[InngestFunction]` but is not a function. |
| [`INNGEST002`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest002) | Warning | An abstract class has `[InngestFunction]`; it is not registered. |
| [`INNGEST003`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest003) | Warning | A generic class has `[InngestFunction]`; it is not registered. |
| [`INNGEST004`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest004) | Warning | A private, `protected`, `private protected` or file-local class, or one inside such a type, has `[InngestFunction]`; it is not registered. |
| [`INNGEST005`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest005) | Warning | A function class has no public constructor; it is not registered. |
| [`INNGEST006`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest006) | Warning | `InngestGenerateFunctionRegistration`, `InngestFunctionsMethodName` or `InngestFunctionsAccessibility` has a value the generator cannot use. |
| [`INNGEST010`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest010) | Warning | The event data or result of a function, or a type a typed step saves (`RunAsync<T>`, `WaitForEventAsync<T>`, `InvokeAsync`, `PublishAsync<T>` and the others), is in no `JsonSerializerContext`, and reflection-based JSON is off. A code fix lists it. |
| [`INNGEST011`](https://damikun.github.io/inngest-dotnet/guides/source-generator.md#inngest011) | Warning | The same type is anonymous, which no context can list. |

## Entity Framework Core outbox

Package `Inngest.EntityFrameworkCore`. See [Transactional outbox](https://damikun.github.io/inngest-dotnet/guides/outbox.md).

| Member | Purpose |
| - | - |
| `ModelBuilder.AddInngestOutbox(Action<EntityTypeBuilder<InngestOutboxMessage>>?)` | Maps the `InngestOutbox` table in the model of a `DbContext`; the callback changes the mapping, for example its name or schema. In `Microsoft.EntityFrameworkCore`. |
| `DbContextOptionsBuilder.UseInngestOutbox()` | Lets saves wake the dispatcher once they have committed, instead of at the next poll. Works wherever the options are built, with or without a service provider. In `Microsoft.EntityFrameworkCore`. |
| `DbContextOptionsBuilder.UseInngestOutbox(IServiceProvider)` | The same, and fails when the options are built if `AddEntityFrameworkOutbox` was not called. In `Microsoft.EntityFrameworkCore`. |
| `DbContext.AddInngestEvent<T>(T data, string? id = null)` | Adds the data as an event named `T.EventName` to the unit of work, serialized with the SDK's serializer options, and returns the row. The id is generated unless given. In `Microsoft.EntityFrameworkCore`. |
| `DbContext.AddInngestEvent(InngestEvent @event)` | The same for an `InngestEvent`, which keeps its `Id`, `User`, `Timestamp`, `Version` and the sessions of `Meta`. In `Microsoft.EntityFrameworkCore`. |

**`InngestOutboxMessage`**, the entity of the table, with all times in UTC: `Id` (the key, and the id the event is sent
under; at most 255 characters, as `Name` and `Version` are), `Name`, `Data`, `User` and `Sessions` (JSON text), `Version`,
`Timestamp` (the event's own time, which the code may set), `CreatedAt` (when the row was added), `NextAttemptAt`,
`Attempts`, `LastError`, `LeaseToken`, `LeaseExpiresAt` and `SentAt`.

**`InngestOutboxOptions`**:

| Member | Purpose |
| - | - |
| `DispatcherEnabled` | Runs the dispatcher in this process. `true` by default. |
| `PollInterval` | How long the dispatcher waits for a wake-up before it looks anyway. 5 seconds by default. |
| `BatchSize` | The most events one claim takes and one request sends. 100 by default. |
| `LeaseDuration`, `SendTimeout` | How long a dispatcher owns the events it claimed (1 minute) and how long one request may take (20 seconds). The lease must be at least twice the timeout. |
| `RetryDelay`, `MaxRetryDelay` | The wait after the first failed attempt, doubled with each failure, and its cap. 5 seconds and 5 minutes by default. |
| `Retention`, `CleanupInterval`, `CleanupBatchSize` | How long sent events stay (1 day), how often older ones are deleted (5 minutes) and how many one `DELETE` removes (1000). |

## SignalR

Package `Inngest.SignalR`, which also brings in `Inngest.AspNetCore`. See [SignalR](https://damikun.github.io/inngest-dotnet/guides/signalr.md).

| Member | Purpose |
| - | - |
| `IServiceCollection.AddInngestSignalR(Action<InngestSignalROptions>)` | Registers the bridge for `InngestRealtimeHub`. Call `AddSignalR` and `AddInngest` as well. In `Microsoft.Extensions.DependencyInjection`. |
| `IServiceCollection.AddInngestSignalR<THub>(Action<InngestSignalROptions>)` | Registers the bridge for a hub of your own, which injects `InngestRealtimeBridge<THub>`. The options are read at once, so a missing `Authorize` fails the app at startup. |
| `IEndpointRouteBuilder.MapInngestRealtimeHub(string pattern)` | Maps `InngestRealtimeHub` and returns its `HubEndpointConventionBuilder`. In `Microsoft.AspNetCore.Builder`. |
| `IEndpointRouteBuilder.MapInngestRealtimeHub(string pattern, Action<HttpConnectionDispatcherOptions>)` | The same, with the options of the connections that `MapHub` takes, such as `CloseOnAuthenticationExpiration`. |

**`InngestRealtimeHub`**, a sealed hub with `Subscribe(string channel, string[] topics)` and `Unsubscribe(string channel,
string[] topics)`.

**`InngestRealtimeBridge<THub>`** forwards the messages to the clients of `THub`. It sends to connections by id, never to
a group, so that instances on a backplane deliver each message once.

| Member | Purpose |
| - | - |
| `SubscribeAsync(HubCallerContext caller, string channel, IReadOnlyList<string> topics, CancellationToken)` | `ValueTask<RealtimeSubscription>`: asks `Authorize`, joins the caller and returns what it joined. Joining again changes nothing. Throws `HubException` when the request is incomplete, refused, over the limit, or the app is stopping. |
| `SubscribeAsync<T>(HubCallerContext caller, RealtimeTopic<T> topic, CancellationToken)` | The same for a typed topic. |
| `Unsubscribe(HubCallerContext caller, string channel, IReadOnlyList<string> topics)` | Takes the caller out of what it joined with these arguments, or with the subscription `Authorize` returned. Leaving what it is not in changes nothing. |
| `Unsubscribe<T>(HubCallerContext caller, RealtimeTopic<T> topic)` | The same for a typed topic. |

**`InngestSignalROptions`**:

| Member | Purpose |
| - | - |
| `Authorize` | `Func<RealtimeSubscribeRequest, ValueTask<RealtimeSubscription?>>`, required: returns the subscription to join, which may have fewer topics or another channel, or null to refuse. |
| `MessageMethod` | The client method that receives each message as a string of JSON. `"Realtime"` by default. |
| `ErrorMethod` | The client method that hears of a subscription the client lost, with the channel and topics it asked for and the reason: `"behind"`, `"send-failed"` or `"inngest-ended"`. The connection stays open. `"RealtimeError"` by default. |
| `MaxSubscriptionsPerConnection` | How many subscriptions one connection may be in, counting each way it asked to join. 32 by default. |
| `MaxQueuedMessagesPerConnection` | How many messages may wait for one connection. A connection that reaches it loses its subscriptions and is told. At least `MaxSubscriptionsPerConnection`. 512 by default. |

**`RealtimeSubscribeRequest(HubCallerContext caller, RealtimeSubscription requested, IServiceProvider services,
CancellationToken cancellationToken = default)`**, with `Caller`, `Requested`, `Services` (a scope of its own for each
request) and `CancellationToken`. The bridge makes one for each client that asks to join, and a test can make one to call
`Authorize` directly.

## Blazor

Package `Inngest.Blazor`, for Blazor Server, WebAssembly and interactive Auto. See
[Realtime in Blazor](https://damikun.github.io/inngest-dotnet/guides/blazor.md).

**`RealtimeView<TData>`**, a component that draws nothing of its own. It subscribes after its first interactive render,
never while prerendering, and unsubscribes when it is disposed.

| Member | Purpose |
| - | - |
| `Topic` | The `RealtimeTopic<TData>` to watch. |
| `Channel`, `Topics` | Several topics of a channel, instead of `Topic`. `TData` must then be `JsonElement`. |
| `MaxMessages` | How many of the newest messages `RealtimeState.Messages` keeps. 100 by default. |
| `OnMessage` | An `EventCallback<RealtimeMessage<TData>>` called for each message, in order. |
| `ThrowOnError` | Hands a failed subscription to the nearest `ErrorBoundary`. `false` by default. |
| `ChildContent` | A `RenderFragment<RealtimeState<TData>>`. |
| `Restart()` | Subscribes again with the messages cleared, such as after a failure. |

**`RealtimeState<TData>`**: `Connection`, `Error`, `Messages`, `Latest`, `LatestByTopic` and `TextByTopic`, the text of
the newest stream of each topic. **`RealtimeConnectionState`** is `Idle`, `Connecting`, `Open`, `Reconnecting`,
`Closed` or `Failed`.

**`IRealtimeSource`** is what the component asks for, and what code outside components can inject:

| Method | Returns |
| - | - |
| `SubscribeAsync<T>(RealtimeTopic<T> topic, CancellationToken)` | `IAsyncEnumerable<RealtimeMessage<T>>`: the topic's messages from now on |
| `SubscribeAsync(string channel, IReadOnlyList<string> topics, CancellationToken)` | `IAsyncEnumerable<RealtimeMessage<JsonElement>>` for several topics of a channel |
| The same two with an `Action<RealtimeConnectionState>? onConnectionChanged` before the token | The same, and a source that can tell calls the callback with `Connecting`, `Open` and `Reconnecting`. An implementation writes these two. |

Each side registers its own source. An app that registers both gets the one registered last.

**`RealtimeRelayOptions`**: `Url`, which maps a channel and its topics to the address of the endpoint, and
`SerializerOptions`, which read the data of typed topics.

## User context

Packages `Inngest.UserContext` and `Inngest.UserContext.AspNetCore`. See [User context](https://damikun.github.io/inngest-dotnet/guides/user-context.md).

| Member | Purpose |
| - | - |
| `IInngestBuilder.AddUserContext(Action<UserContextOptions>?)` | Adds the middleware that attaches the allowed claims of the current user to the events an app sends and restores them in runs, and registers `ICurrentUser` and a scoped `ClaimsPrincipal`. Needs Data Protection, or an `IUserContextProtector`. Register it before middleware that needs the user. |
| `IInngestBuilder.AddUserContextFromHttpContext()` | Package `Inngest.UserContext.AspNetCore`. Makes `HttpContext.User` the user of events sent while serving a request. |
| `InvokeOptions.WithUser(FunctionContext)` | Extension method. Passes the user of the run to the function being invoked. Invocations do not pass through middleware, so they carry the user only when asked. |

**`ICurrentUser`**: `Principal`, a `ClaimsPrincipal` that is anonymous without a user; `IsAuthenticated`; and `IssuedAt`,
when the sender took the claims. It is the user of the run whose code is running, in every scope.

**`UserContextOptions`**:

| Member | Purpose |
| - | - |
| `Claims` | The claim types that are attached, matched exactly. `sub` and `ClaimTypes.NameIdentifier` by default. |
| `Lifetime` | How long after the claims were taken a run can still start with them, judged by the later of its creation and its event's timestamp. Seven days by default. |
| `MaxSize` | The largest size of the payload's JSON in UTF-8 bytes. 4096 by default; a larger user is not attached. |
| `Propagate` | Whether the events a run sends carry its user. `true` by default. |
| `EventFilter` | A `Func<InngestEvent, bool>` that picks the events that carry the user. `null` means all. |
| `OnInvalid` | An `InvalidUserContextAction`: `Anonymous` (default) runs without a user and logs a warning, `FailRun` fails the run. A failure handler never fails on its user. |

**`IUserContextSource`**: `GetUserAsync(CancellationToken)` returns the `ClaimsPrincipal` an event sent outside a run is
sent for, or null. **`IUserContextProtector`**: `Protect(string)` and `TryUnprotect(string, out string?)`, which turn the
JSON of the claims into text for the event and back; ASP.NET Core Data Protection by default.

## Webhooks

Package `Inngest.Webhooks`, which `Inngest.AspNetCore` brings in; `MapInngestWebhook` is in `Inngest.AspNetCore`. See
[Webhooks](https://damikun.github.io/inngest-dotnet/guides/webhooks.md).

**`WebhookProvider`** says how a source of webhooks is verified and how its deliveries become events. Its constructor
`(string name, IWebhookVerifier verifier, Func<WebhookDelivery, InngestEvent?> createEvent)` takes any other source. The
factories:

| Factory | Event |
| - | - |
| `Stripe(string secret, TimeSpan? tolerance = null)` | `stripe/<type>`, the Stripe event as data, its `id` as the id. Also with `IEnumerable<string>` secrets. |
| `GitHub(string secret)` | `github/<X-GitHub-Event>`, the payload as data, `X-GitHub-Delivery` as the id; the name and the id are unsigned headers, and a delivery without the id is a 400. Also with `IEnumerable<string>` secrets. |
| `Shopify(string secret)` | `shopify/<topic>` with dots for slashes, the payload as data, `X-Shopify-Webhook-Id` as the id; the topic and the id are unsigned headers, and a delivery without the id is a 400. Also with `IEnumerable<string>` secrets. |
| `StandardWebhooks(string eventPrefix, string secret, TimeSpan? tolerance = null)` | `<prefix>/<type>`, the `data` of the payload as data, `webhook-id` or `svix-id` as the id. Also with `IEnumerable<string>` secrets. |
| `Hmac(string eventName, HmacWebhookOptions options, string secret)` | The one name, the payload as data, the header `options` names as the id, which a delivery must then have; no id without such a header. Also with `IEnumerable<string>` secrets. |

**`WebhookOptions`**, an immutable record:

| Member | Purpose |
| - | - |
| `MaxBodySize` | The largest body accepted, in bytes; a larger request is answered with 413. 1 MiB by default. |
| `SendTimeout` | How long to wait for Inngest to accept the event before answering 503. Four seconds by default. |
| `Transform` | `Func<WebhookDelivery, InngestEvent, InngestEvent?>`: changes the event before it is sent, or returns null to drop the delivery with 204. A `JsonElement` as the `Data` or `User` it returns is copied before the send. |

**`WebhookDelivery`**, what a `Transform` and a provider's `createEvent` see: `Provider`, `DeliveryId`, `Payload` (a
`JsonElement`, valid while the request is handled), `Services` and `GetHeader(string name)`. Its constructor
`(string provider, string? deliveryId, JsonElement payload, IWebhookRequest? request = null)` is public, to try a
`Transform` in a test.

An event that cannot be sent is answered with a client error, not a 503: 413 when Inngest says it is too large, 400 for
another client error of Inngest, for a refusal of the client and for a payload that cannot be written as JSON. See
[Webhooks](https://damikun.github.io/inngest-dotnet/guides/webhooks.md#what-happens-to-a-request).

**Verifiers.** Each takes a secret or several, so that a secret can be replaced without downtime, and checks a request
with `Verify(IWebhookRequest request, DateTimeOffset now)`, which returns a `WebhookVerification` and never throws for
what a request contains.

| Type | Scheme |
| - | - |
| `StandardWebhooksVerifier` | `webhook-id`, `webhook-timestamp` and `webhook-signature`, or the `svix-` headers: HMAC-SHA256 of `{id}.{timestamp}.{body}`, keyed with the base64 after `whsec_`; only `v1` signatures; a tolerance of five minutes by default |
| `StripeWebhookVerifier` | `Stripe-Signature`, `t=` and `v1=`: HMAC-SHA256 of `{t}.{body}`, keyed with the text of the secret; a tolerance of five minutes by default |
| `GitHubWebhookVerifier` | `X-Hub-Signature-256`, `sha256=` and the hex HMAC-SHA256 of the body; delivery id from `X-GitHub-Delivery`, which the signature does not cover |
| `ShopifyWebhookVerifier` | `X-Shopify-Hmac-Sha256`, the base64 HMAC-SHA256 of the body; delivery id from `X-Shopify-Webhook-Id`, which the signature does not cover |
| `HmacWebhookVerifier` | Any HMAC of the body, with `HmacWebhookOptions` |

**`HmacWebhookOptions`**, an immutable record: `SignatureHeader` (required), `Algorithm` (`HashAlgorithmName`, SHA-256 by
default), `SignatureEncoding` (`HmacSignatureEncoding.Hex` or `Base64`), `SignaturePrefix`, `TimestampHeader`,
`SignedContentPrefix`, `TimestampSeparator`, `Tolerance` and `DeliveryIdHeader`.

To verify another source, implement **`IWebhookVerifier`**. **`IWebhookRequest`** is the request it sees, independent of
the web framework: `Body` (the raw bytes, owned by the host and read in place), `GetHeader(string name)` and `Services`.
**`WebhookVerification`** is a result:
`Valid(string? deliveryId = null)` or `Invalid(string reason)`, with `IsValid`, `DeliveryId` and `Reason`; the default
value is not valid.

## Test host

Package `Inngest.Testing`, namespace `Inngest.Testing`. See [Testing functions](https://damikun.github.io/inngest-dotnet/guides/testing.md).

| Member | Purpose |
| - | - |
| `InngestTestHost(IServiceCollection services)` | Builds the app's services from a copy of the collection, with nothing sent to Inngest. |
| `RunAsync(string functionId, InngestEvent @event, StepMocks mocks, CancellationToken)` | Runs the function until it completes, fails for good or waits on a step without a mock. Also without `mocks`, and with an `IReadOnlyList<InngestEvent>` for a batch. |
| `RunAsync<TFunction>(InngestEvent @event, StepMocks mocks, CancellationToken)` | The same for the function registered as `TFunction`, with the same overloads. |
| `ExecuteStepAsync(string functionId, string stepId, InngestEvent @event, StepMocks mocks, CancellationToken)` | Runs the function until the step has run, or has been reached when Inngest carries it out. Also without `mocks`. |
| `ExecuteStepAsync<TFunction>(string stepId, InngestEvent @event, StepMocks mocks, CancellationToken)` | The same for the function registered as `TFunction`. Also without `mocks`. |
| `SentEvents` | Every event sent through the app's client since the host was created. |
| `SentSignals` | Every signal sent through the app's client since the host was created. None resumes a run. |
| `Published` | Every realtime message published since the host was created, as `PublishedMessage(RealtimeTopic Topic, JsonElement Data)` with `IsStream` for a stream, recorded with all its text. Tokens the host mints are placeholders, and subscribing fails. |
| `Services` | The app's services, as functions resolve them. Disposing the host, which is `IAsyncDisposable` and `IDisposable`, disposes them. |

**`StepMocks`** answers steps by id, and every method returns a new set: `Run<T>(id, T result)`,
`Run<T>(id, Func<int, T> handler)`, `RunThrows(id, exception)`, `Sleep(id)`, `AllSleeps()`,
`WaitForEvent(id, received)`, `WaitForEventTimeout(id)`, `Invoke<T>(id, T result)`,
`Invoke<TFunction, TResult>(id, TResult result)`, `InvokeThrows(id, exception)`, `WaitForSignal<T>(id, T data)`,
`WaitForSignalTimeout(id)`, `SendSignal(id, runId)`, `Fetch(id, statusCode, body, headers)` and
`Infer<T>(id, T response)`. A `Run` mock also answers an AI wrap step.

**`TestRun`**: `RunId`, `FunctionId`, `Status`, `Output`, `GetOutput<T>()`, `Error`, `Steps`, `GetStep(id)`,
`WaitingOn`, `SentEvents`, `SentSignals`, `Published` and `FailureHandler`; `ToString()` describes the outcome. `Status` is a `TestRunStatus`:
`Completed`, `Failed`, `Waiting` or `Stopped`.

**`TestStep`**: `Id`, `HashedId`, `Type`, `Op`, `Options`, `Status`, `Attempts`, `Mocked`, `Output`,
`GetOutput<T>()`, `Error` and `Errors`. `Status` is a `TestStepStatus`: `Pending`, `Completed` or `Failed`.

## Test containers

Package `Inngest.Testing.Containers`, namespace `Inngest.Testing.Containers`. See
[Testing with containers](https://damikun.github.io/inngest-dotnet/guides/testing-with-containers.md). It follows the Testcontainers module
pattern: a builder, a container and a configuration.

**`InngestServerBuilder(string image)`** builds an `InngestServerContainer` for the dev server. There is no default image; the module
is tested with `inngest/inngest:v1.49.0`. It takes an `IImage` as well, and has the methods of every Testcontainers
builder.

| Member | Purpose |
| - | - |
| `WithSelfHosting()` | Runs `inngest start`, the self-hosted server, in place of the dev server. |
| `WithSigningKey(string)`, `WithEventKey(string)` | Sets the keys of the self-hosted server. Keys left out are generated. The signing key must be hexadecimal digits in pairs. |
| `WithAppUrl(Uri)`, `WithAppUrl(string)` | Has the server sync the app at this address when it starts and retry every second until the app answers. A loopback address becomes `host.docker.internal`. |
| `WithConnectGateway(int? port = null)` | Publishes the Connect gateway on the same port inside the container and on the host, and waits for it. A free port when none is given, kept from other processes until the container starts. |
| `WithFastTiming(bool = true)` | The timing flags that make runs finish quickly: a sync retried and a step retried after a second, the queue polled every 50 ms. On by default. |
| `InngestServerBuilder.InngestPort` | The port of the server's API inside the container, 8288. |

**`InngestServerContainer`**, a `DockerContainer`:

| Member | Purpose |
| - | - |
| `GetBaseAddress()` | The address of the server's API as the test reaches it, as a `Uri`. `GetConnectionString()` returns it as text. |
| `Configure(InngestOptions)` | Sets an app's options to use the server: dev mode and the address, or cloud mode with the keys and the address. Replaces addresses set in code. |
| `GetSdkEnvironment()` | The same settings as `INNGEST_*` variables, for a process that inherits others: `INNGEST_DEV` (the dev server's address, or `0` for the self-hosted server), `INNGEST_BASE_URL`, `INNGEST_API_BASE_URL` and `INNGEST_EVENT_API_BASE_URL`, and `INNGEST_SIGNING_KEY` and `INNGEST_EVENT_KEY` for the self-hosted one. |
| `SyncAsync(Uri appUrl, CancellationToken)` | Has the server register the app that listens at this address. A loopback address, or `0.0.0.0` or `[::]`, goes out as `host.docker.internal`; any other address as given. The request names the kind of server (`dev` or `cloud`) and is signed for the self-hosted server. Throws when the app does not register. |
| `IsDevServer`, `SigningKey`, `EventKey`, `ConnectGatewayPort` | Which server it is, the keys of the self-hosted server (null for the dev server) and the gateway's port (null without one). |
| `StartAsync(CancellationToken)` | Starts the container. A server that does not become ready fails with its log in the exception. |

**`InngestServerConfiguration`** holds what the builder keeps for the container: `SelfHosted`, `SigningKey`, `EventKey`,
`AppUrls`, `ConnectGateway`, `ConnectGatewayPort` and `FastTiming`, next to the settings of any container.

## ASP.NET Core test factory

Package `Inngest.Testing.Containers.AspNetCore` for .NET 10, namespace `Inngest.Testing.Containers`. See
[An app under WebApplicationFactory](https://damikun.github.io/inngest-dotnet/guides/testing-with-containers.md#an-app-under-webapplicationfactory).

| Member | Purpose |
| - | - |
| `InngestWebApplicationFactory<TEntryPoint>(InngestServerContainer inngest, string servePath = "/api/inngest")` | A `WebApplicationFactory` that runs the app on Kestrel, on all interfaces and a free port, and applies `inngest.Configure` to the app's options after the app's own. The container must be running when the app starts. |
| `StartAsync(CancellationToken)` | Starts the app and has the container sync it, so the server knows its functions when this returns. |
| `GetAppUrl()` | The address of the app's Inngest endpoint as the test reaches it. |
| `CreateClient()`, `CreateClient(WebApplicationFactoryClientOptions)` | Declared again, so that a client reaches the app at `localhost` and its port, also with options that have no base address. `CreateDefaultClient` gets the same address. |
| `Container` | The container the app is configured for. |

## Templates and tool

Packages `Inngest.Templates` and `dotnet-inngest`. They have no C# API: the first holds `dotnet new` templates and the
second is a .NET tool. See [Templates and tool](https://damikun.github.io/inngest-dotnet/guides/templates-and-tools.md).

**Templates**, installed with `dotnet new install Inngest.Templates`:

| Short name | Project SDK | References |
| - | - | - |
| `inngest-web` | `Microsoft.NET.Sdk.Web` | `Inngest.AspNetCore`, at the version of the template package |
| `inngest-worker` | `Microsoft.NET.Sdk.Worker` | `Inngest.Connect`, at the version of the template package, and `Microsoft.Extensions.Hosting` |

| Option | Templates | Meaning |
| - | - | - |
| `-n`, `--name` | both | The project name, and the app id in kebab case. |
| `-f`, `--framework` | both | `net10.0` (the default) or `net8.0`. |
| `--http-port` | `inngest-web` | The port of the `http` launch profile. A free port from 5000 to 5300 by default. |
| `--no-restore` | both | Skips the restore after the project is made. |

**`dotnet inngest`**, installed with `dotnet tool install --global dotnet-inngest` (the command is `dotnet-inngest`):

| Command | Purpose |
| - | - |
| `dotnet inngest dev [options] [-- <options of inngest dev>]` | Downloads the Inngest CLI release on first use, checks its SHA-256 and runs `inngest dev`. |
| `dotnet inngest --version` | Prints the version of the tool and of the CLI it is pinned to. |
| `dotnet inngest --help` | Prints the usage. |

| Option of `dev` | Meaning |
| - | - |
| `-u`, `--sdk-url <url>` | The app's Inngest endpoint, passed to the CLI. Without it, and without `--no-discovery`, the tool reads the `http` address of the first profile that runs the project in `Properties/launchSettings.json` and appends `/api/inngest`. |
| `--project <path>` | The folder or project file whose launch settings are read. The current folder by default. |
| `--cli-version <version>` | Runs another release of the CLI. Its download is checked against the `checksums.txt` of the release; the pinned version against checksums built into the tool. |

Other options go to `inngest dev` as they are, in order. `INNGEST_DOTNET_CACHE` sets the folder the CLI is kept in. The
exit code is the CLI's; the tool's own are 1 when the CLI cannot be had (a request failed, or a download does not
match its SHA-256), 2 for a command line it cannot read and 130 when Ctrl+C stops a download.
