Skip to content

Community project

Inngest SDK for .NET

Write Inngest functions as plain C# classes in your ASP.NET Core app, with realtime updates and the .NET ecosystem you already use. Inngest runs them; this SDK connects your .NET app to it.

// CheckoutFunction.ExecuteAsyncvar charge = await context.Step.RunAsync("charge",    ct => pay.ChargeAsync(order, ct));await context.Step.RunAsync("send-receipt",    ct => mail.SendReceiptAsync(order, ct));await context.Step.SleepAsync("cooling-off",    TimeSpan.FromMinutes(30));await context.Step.RunAsync("ship",    ct => warehouse.ShipAsync(order, ct));return new { charge.Id };
CheckoutFunction as Inngest runs it, step by step. The charge runs and completes. Send-receipt fails, and Inngest retries that step alone while the charge stays completed; the retry succeeds. The run then sleeps for 30 minutes, and ship runs.
  • .NET 8 and .NET 10
  • ASP.NET Core or Connect
  • Native AOT
  • 3,735 unit tests
  • 351 end-to-end tests
  • Apache 2.0

Built on Inngest

The .NET side of Inngest

Inngest is the platform that runs your functions: it receives events, keeps the state of every run, retries what fails and shows each run in its dashboard. This SDK is how a .NET app takes part. You write the functions in C#, and the SDK serves them to Inngest and sends your events. Inngest's own documentation covers the platform.

How it works shows what this SDK answers to each of Inngest’s calls in a real run.

Code and run

Ordinary C#, durable by default

The function from the top of the page, in full. Each step is a call to RunAsync, SleepAsync or another step tool; the rest is plain C#.

CheckoutFunction.cs
using Inngest;
public sealed record OrderPaid(string OrderId, string CustomerId, decimal Amount);
public sealed record Charge(string Id, decimal Amount);
public sealed class CheckoutFunction(IPayments pay, IMailer mail, IWarehouse warehouse)
: InngestFunction<OrderPaid>
{
public override async Task<object?> ExecuteAsync(
FunctionContext<OrderPaid> context, CancellationToken cancellationToken)
{
var order = context.Event.Data;
var charge = await context.Step.RunAsync("charge",
ct => pay.ChargeAsync(order, ct));
await context.Step.RunAsync("send-receipt",
ct => mail.SendReceiptAsync(order, ct));
await context.Step.SleepAsync("cooling-off",
TimeSpan.FromMinutes(30));
await context.Step.RunAsync("ship",
ct => warehouse.ShipAsync(order, ct));
return new { charge.Id };
}
}
public interface IPayments
{
Task<Charge> ChargeAsync(OrderPaid order, CancellationToken cancellationToken);
}
public interface IMailer
{
Task SendReceiptAsync(OrderPaid order, CancellationToken cancellationToken);
}
public interface IWarehouse
{
Task ShipAsync(OrderPaid order, CancellationToken cancellationToken);
}

RunAsync("charge", ...) is a step. Inngest stores the charge it returns, and later calls get that stored result instead of charging again. If it throws, Inngest retries this step alone.

RunAsync("send-receipt", ...) is a step of its own. If the mail server fails, Inngest retries the receipt, and the charge stays done.

SleepAsync("cooling-off", ...) ends the call. Inngest calls the function again in 30 minutes. Nothing runs or waits in your app meanwhile, and deploys in between do not matter.

RunAsync("ship", ...) runs after the sleep. By then the charge and the receipt come back from Inngest’s store, not from the payment provider or the mail server.

return gives the run its output. The SDK answers 200 and Inngest marks the run completed.

Capabilities

Everything the SDK can do

Steps and flow control, realtime and AI, hosting, integrations and testing. Each card shows the main call and links to its guide.

A step’s result is saved, so a later call gets it back without running the code again.

var paymentId = await context.Step.RunAsync(
"charge", ct => payments.ChargeAsync(
order.CustomerId, order.Total, ct));

Steps guide

Pause for days, or until an event arrives, without holding a thread or a worker.

var activated = await context.Step
.WaitForEventAsync<UserActivated>(
"activation", TimeSpan.FromDays(3));

Sleep and wait guide

Start steps together and await them with Task.WhenAll; Inngest runs each in its own call.

await Task.WhenAll(userIds.Select(userId =>
context.Step.RunAsync($"load-{userId}",
ct => profiles.LoadAsync(userId, ct))));

Parallel steps guide

Run a step for each item of a list, with a limit on how many run at once and the results back in order.

await context.Step.MapAsync("charge", due,
(i, ct) => billing.ChargeAsync(i.Id, ct),
new FanOutOptions { MaxParallelism = 5 });

Fan-out guide

Run another function as a step and get its typed result back, with its own retries and history.

var score = await context.Step
.InvokeAsync<ScoreAccount, AccountScore>(
"score-acme", new ScoreRequest("acme"));

Invoking functions guide

Run a step in a dependency injection scope of its own, so a DbContext carries no state from one step to the next.

await context.Step
.RunScopedAsync<IInventory>("reserve",
(inv, ct) => inv.ReserveAsync(id, ct));

Scoped steps guide

Start a function from a typed event, a filtered event or a cron schedule, with its options as attributes or in code.

[EventTrigger<OrderPlaced>(
If = "event.data.total >= 500")]
[CronTrigger("0 6 * * 1-5", Timezone = "UTC")]

Functions and triggers guide

Concurrency, throttling, rate limits, debounce, batching, priority and singletons are attributes on the function.

[Throttle(10, "1m")]
[Concurrency(1, Key = "event.data.accountId")]
[Debounce("30s", Key = "event.data.accountId")]

Flow control guide

Every step retries on its own. Exceptions stop the retries or ask for a delay.

throw new RetryAfterException(
"Rate limited.", TimeSpan.FromMinutes(1));

Errors and retries guide

Publish updates and streams of text from a run or from any code, and relay them to browsers or receive them in .NET.

var status = new RealtimeTopic<string>(
$"order:{orderId}", "status");
await realtime.PublishAsync(status, "Charged");

Realtime guide

Run the model calls and tool calls of an IChatClient as steps, so a retry or a replay never repeats them.

using Microsoft.Extensions.AI;
builder.UseInngestFunctionInvocation()
.UseInngestSteps();

AI chat clients guide

Show realtime messages in Blazor Server, WebAssembly and interactive Auto with the RealtimeView component.

services.AddInngest(
options => options.AppId = "shop");
services.AddInngestRealtimeServerSource();

Blazor guide

Forward realtime messages to the clients of a SignalR hub, and decide who may join which channel.

services.AddInngestSignalR(options =>
options.Authorize = authorize);
app.MapInngestRealtimeHub("/hubs/inngest");

SignalR guide

With Connect, a worker holds an outbound WebSocket to Inngest and needs no public URL.

inngest.AddConnect(connect =>
connect.MaxWorkerConcurrency = 20);

Connect guide

Run steps in an ASP.NET Core endpoint, answer the caller directly, and hand the run to Inngest when a step has to wait or fails. Inngest calls it beta, so the API may still change.

app.UseInngestDurableEndpoints();
app.MapPost("/pay", (IStepTools step) =>
step.RunAsync("charge", () => "paid"))
.WithDurableEndpoint();

Durable endpoints guide

Serve the endpoint from an HTTP-triggered function of an Azure Functions app in the isolated worker model.

using Microsoft.Azure.Functions.Worker.Http;
// In an HTTP-triggered function
return await request.ServeInngestAsync();

Azure Functions guide

Serve the endpoint from a Lambda function behind API Gateway or a function URL, as a managed function or a Native AOT executable.

var handler = new InngestLambdaHandler(sp);
return await handler.HandleAsync(
request, context);

AWS Lambda guide

Publish trimmed or as a Native AOT binary by giving the SDK source-generated JSON metadata for your types.

using System.Text.Json.Serialization;
[JsonSerializable(typeof(OrderPlaced))]
partial class OrderJson : JsonSerializerContext;

Native AOT guide

Send events from application code with IInngestClient or from a function as a step, with ids for deduplication and delayed starts.

await inngest.SendAsync(
new OrderPlaced("o-1001", "c-7", 49.90m));

Sending events guide

Verify the signature of a webhook from Stripe, GitHub, Shopify, Clerk or Resend, and send it as an event whose id is the delivery id.

app.MapInngestWebhook("/webhooks/stripe",
WebhookProvider.Stripe(secret));

Webhooks guide

Save an event in the same Entity Framework Core transaction as your data, and a hosted dispatcher sends it to Inngest afterwards.

using Microsoft.EntityFrameworkCore;
db.AddInngestEvent(placed);
await db.SaveChangesAsync(cancellationToken);

Transactional outbox guide

Carry the signed-in user who caused a run into it, as ICurrentUser or a ClaimsPrincipal, protected with ASP.NET Core Data Protection.

inngest.AddUserContext(options =>
options.Claims = ["sub", "role"]);
inngest.AddUserContextFromHttpContext();

User context guide

Observe and wrap function runs, steps and event sends, for logging, metrics, tracing and error mapping.

inngest.AddMiddleware<TimingMiddleware>();

Middleware guide

Encrypt event data, step results and function output, in the format of the TypeScript SDK’s encryption middleware.

inngest.AddEncryption(options =>
options.Key = encryptionKey);

Encryption guide

Report the errors of functions and steps to Sentry, tagged with the app, the function, the run and the event.

builder.WebHost.UseSentry(o => o.Dsn = dsn);
inngest.AddSentryReporting();

Sentry guide

Trace every call and step, and measure runs, retries and durations.

using OpenTelemetry.Trace;
inngest.AddInstrumentation();
tracing.AddInngestInstrumentation();

OpenTelemetry reference

Run a function in memory on the real execution engine with mocked steps, or against a fake IStepTools.

using Inngest.Testing;
var mock = new StepMocks().Run("charge", "ok");
var run = await host.RunAsync("pay", e, mock);

Testing guide

Start the Inngest dev server or a self-hosted server in Docker from your tests, and run your app against it.

using Inngest.Testing.Containers;
var server = new InngestServerBuilder(
"inngest/inngest:v1.49.0").Build();

Testing with containers guide

Run the Inngest dev server as a container of your AppHost, with its UI linked in the Aspire dashboard.

var inngest = builder.AddInngest("inngest");
builder.AddProject<Projects.Shop>("shop")
.WithReference(inngest).WaitFor(inngest);

Aspire guide

Turn on a generator that registers every function class of a project with one call, and find the types a Native AOT app has not listed.

inngest.AddInngestFunctions();

Source generator guide

Start an app with dotnet new inngest-web or inngest-worker, and run the Inngest dev server for it from its folder with dotnet inngest dev.

Terminal window
dotnet new inngest-web -n Shop
dotnet tool install --global dotnet-inngest
dotnet inngest dev

Templates and tool guide

Tested

Tested against the real Inngest server

Triggers, steps, sleeps, waits, flow control, retries, middleware, key rotation and Connect run end to end against Inngest's own server image, in dev mode and in signed cloud mode.

  • 3,735unit tests, including concurrency stress tests with thousands of invocations at once
  • 351end-to-end tests against the Inngest server in Docker, in dev mode and signed cloud mode
  • 8,000 / 8,000runs completed correctly under sustained load, in dev mode and again in cloud mode
  • Flatmemory over 8 million engine invocations and 1.6 million signed HTTP calls

Ships for .NET 8 and .NET 10, runs under Native AOT, serves over HTTP or a Connect WebSocket, and checks every request's signature, with a fallback key for rotation. Read the readiness report.

Coming next: more guides, checkpointing on by default, durable endpoints out of experimental and a validation middleware package.See the roadmap.

Quick start

Your first durable function in five minutes

Install the SDK, write a function and register it, start the dev server, and watch a run go step by step on your machine.

  1. Install the SDK from NuGet. Inngest.AspNetCore brings in Inngest:

    Terminal window
    dotnet add package Inngest.AspNetCore
  2. Write the function, a class with one method. Its attributes give it an id and name the event that starts it. Each RunAsync call is a step: it runs once and Inngest saves its result. SleepAsync pauses the run without holding a thread:

    OnboardUser.cs
    using Inngest;
    public sealed record UserSignedUp(string UserId, string Email);
    [InngestFunction("onboard-user")]
    [EventTrigger("user/signed-up")]
    public sealed class OnboardUser : InngestFunction<UserSignedUp>
    {
    public override async Task<object?> ExecuteAsync(
    FunctionContext<UserSignedUp> context, CancellationToken cancellationToken)
    {
    var user = context.Event.Data;
    await context.Step.RunAsync("send-welcome-email",
    () => Console.WriteLine($"Sending a welcome email to {user.Email}"));
    await context.Step.SleepAsync("wait-a-moment", TimeSpan.FromSeconds(10));
    await context.Step.RunAsync("send-tips",
    () => Console.WriteLine($"Sending tips to {user.Email}"));
    return new { user.UserId };
    }
    }
  3. Register the function and map the endpoint Inngest calls:

    Program.cs
    using Inngest;
    var builder = WebApplication.CreateBuilder(args);
    builder.Services
    .AddInngest(options => options.AppId = "shop")
    .AddFunction<OnboardUser>();
    var app = builder.Build();
    app.MapInngest("/api/inngest");
    app.Run();
  4. Set INNGEST_DEV=1, which points the SDK at the local dev server, and start the app with dotnet run --urls http://localhost:5280. Then run the Inngest dev server:

    Terminal window
    curl -sSfL https://cli.inngest.com/install.sh | sh
    inngest dev -u http://localhost:5280/api/inngest
  5. Send a user/signed-up event. The dev server takes any event key in the URL. The run appears at http://localhost:8288, and the app prints the welcome line, then the tips 10 seconds later:

    Terminal window
    curl -X POST http://localhost:8288/e/test -d '{"name": "user/signed-up", "data": {"userId": "u1", "email": "ada@example.com"}}'

Getting started goes through the same steps in detail.

FAQ

Questions engineers ask first

Is this an official Inngest SDK?

No. inngest-dotnet is a community SDK and is not affiliated with Inngest Inc. It speaks the same protocol as the official Inngest SDKs, and its end-to-end tests run against the Inngest server in dev and cloud mode.

What do I need to run it?

.NET 8 or .NET 10 and an Inngest server: Inngest Cloud, a self-hosted Inngest, or the dev server on your machine. Functions are served from an ASP.NET Core endpoint, from Azure Functions or AWS Lambda, or from a Connect worker.

Does Inngest have to reach my app over the internet?

With MapInngest, Inngest calls a public HTTPS endpoint in your app. With Connect, the app opens a WebSocket to Inngest instead and needs no inbound port.

Is it ready for production?

The serve protocol, steps, flow control, signing, middleware and Connect are implemented and verified: 3,735 unit tests, 351 end-to-end tests against the Inngest server in Docker, and load and memory checks in dev and cloud mode. The readiness report sums them up. The packages are on NuGet as 1.0.0. Durable endpoints are the one feature still marked experimental.

Does it work with Native AOT?

Yes. The SDK uses source-generated JSON for its own types; add a JsonSerializerContext for yours, as described in Native AOT and trimming.

Learn more

Docs for this SDK and for Inngest

For tools that read the docs there are llms.txt and llms-full.txt, and every page has a Markdown copy at its address with .md in place of the trailing slash.

inngest-dotnet is a community Inngest SDK for .NET. It is not affiliated with or endorsed by Inngest Inc.