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 };- .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#.
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.
Write workflows
Section titled “Write workflows”Steps that run once
Section titled “Steps that run once”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));Sleep and wait for events
Section titled “Sleep and wait for events”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));Parallel steps
Section titled “Parallel steps”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))));Fan-out over a list
Section titled “Fan-out over a list”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 });Invoke other functions
Section titled “Invoke other functions”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"));Scoped steps
Section titled “Scoped steps”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));Event and cron triggers
Section titled “Event and cron triggers”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")]Flow control
Section titled “Flow control”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")]Errors and retries
Section titled “Errors and retries”Every step retries on its own. Exceptions stop the retries or ask for a delay.
throw new RetryAfterException( "Rate limited.", TimeSpan.FromMinutes(1));Realtime and AI
Section titled “Realtime and AI”Realtime
Section titled “Realtime”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");AI chat clients
Section titled “AI chat clients”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();Blazor
Section titled “Blazor”Show realtime messages in Blazor Server, WebAssembly and interactive Auto with the RealtimeView component.
services.AddInngest( options => options.AppId = "shop");services.AddInngestRealtimeServerSource();SignalR
Section titled “SignalR”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");Run it anywhere
Section titled “Run it anywhere”Workers without an endpoint
Section titled “Workers without an endpoint”With Connect, a worker holds an outbound WebSocket to Inngest and needs no public URL.
inngest.AddConnect(connect => connect.MaxWorkerConcurrency = 20);Durable endpoints Experimental
Section titled “Durable endpoints Experimental”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();Azure Functions
Section titled “Azure Functions”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 functionreturn await request.ServeInngestAsync();AWS Lambda
Section titled “AWS Lambda”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);Native AOT
Section titled “Native AOT”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;Integrations
Section titled “Integrations”Send events
Section titled “Send events”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));Webhooks
Section titled “Webhooks”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));Transactional outbox
Section titled “Transactional outbox”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);User context
Section titled “User context”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();Middleware
Section titled “Middleware”Observe and wrap function runs, steps and event sends, for logging, metrics, tracing and error mapping.
inngest.AddMiddleware<TimingMiddleware>();Encryption
Section titled “Encryption”Encrypt event data, step results and function output, in the format of the TypeScript SDK’s encryption middleware.
inngest.AddEncryption(options => options.Key = encryptionKey);Sentry
Section titled “Sentry”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();OpenTelemetry
Section titled “OpenTelemetry”Trace every call and step, and measure runs, retries and durations.
using OpenTelemetry.Trace;inngest.AddInstrumentation();tracing.AddInngestInstrumentation();Test and ship
Section titled “Test and ship”Testing
Section titled “Testing”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 with containers
Section titled “Testing with containers”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();Aspire
Section titled “Aspire”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);Source generator and analyzers
Section titled “Source generator and analyzers”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();Templates and the dotnet inngest tool
Section titled “Templates and the dotnet inngest tool”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.
dotnet new inngest-web -n Shopdotnet tool install --global dotnet-inngestdotnet inngest devTested
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.
-
Install the SDK from NuGet.
Inngest.AspNetCorebrings inInngest:Terminal window dotnet add package Inngest.AspNetCore -
Write the function, a class with one method. Its attributes give it an id and name the event that starts it. Each
RunAsynccall is a step: it runs once and Inngest saves its result.SleepAsyncpauses 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 };}} -
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(); -
Set
INNGEST_DEV=1, which points the SDK at the local dev server, and start the app withdotnet run --urls http://localhost:5280. Then run the Inngest dev server:Terminal window curl -sSfL https://cli.inngest.com/install.sh | shinngest dev -u http://localhost:5280/api/inngestTerminal window brew install inngest/tap/inngestinngest dev -u http://localhost:5280/api/inngestTerminal window npx --ignore-scripts=false inngest-cli@latest dev -u http://localhost:5280/api/inngestTerminal window dotnet tool install --global dotnet-inngestdotnet inngest dev -u http://localhost:5280/api/inngestThe tool downloads the Inngest CLI once, checks its SHA-256 and runs it. In a project with a launch profile, leave out
-u: the tool reads the address fromProperties/launchSettings.json.Terminal window docker run --rm -p 8288:8288 -p 8289:8289 inngest/inngest:v1.49.0 inngest dev -u http://host.docker.internal:5280/api/inngestThe container reaches the app at
host.docker.internal, sincelocalhostinside it is the container itself. On Linux without Docker Desktop, add--add-host=host.docker.internal:host-gateway.compose.yaml services:inngest:image: inngest/inngest:v1.49.0command: inngest dev -u http://host.docker.internal:5280/api/inngestports:- "8288:8288"- "8289:8289"extra_hosts:- "host.docker.internal:host-gateway"Terminal window docker compose up -
Send a
user/signed-upevent. The dev server takes any event key in the URL. The run appears athttp://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.