I come from the SaaS world. Over the last half year I worked with client, and I introduced Inngest. It was super helpful there, and I think Inngest has big potential in the .NET world.
So I built an SDK for it second time to opensource latest feature to .net eco.
inngest-dotnet is a community Inngest SDK for .NET. It speaks the same protocol as Inngest's official SDKs, supports Inngest as it is, and follows .NET conventions: dependency injection, IOptions, ILogger and CancellationToken.
A function is a class
using Inngest; public sealed record HelloRequested(string Name) : IInngestEventData { public static string EventName => "app/hello.requested"; } [InngestFunction("say-hello")] [EventTrigger<HelloRequested>] public sealed class SayHello : InngestFunction<HelloRequested> { public override async Task<object?> ExecuteAsync( FunctionContext<HelloRequested> context, CancellationToken cancellationToken) { var greeting = await context.Step.RunAsync("compose-greeting", () => $"Hello, {context.Event.Data.Name}!"); return new { greeting }; } } Register it, and map the endpoint Inngest calls:
var inngest = builder.Services .AddInngest(options => options.AppId = "shop") .AddFunction<SayHello>(); app.MapInngest("/api/inngest"); The snippets below go inside ExecuteAsync, or next to this registration, where inngest is the builder that AddInngest returns.
Write workflows
Steps that run once. A step's result is saved, so a later call gets it back instead of running the code again.
var paymentId = await context.Step.RunAsync("charge", ct => payments.ChargeAsync(order.CustomerId, order.Total, ct)); Sleep and wait for events. Pause for minutes or days, or until an event arrives, without holding a thread or a worker.
await context.Step.SleepAsync("cool-off", TimeSpan.FromMinutes(30)); var activated = await context.Step.WaitForEventAsync<UserActivated>( "activation", TimeSpan.FromDays(3)); Parallel steps. Start steps together and await them with Task.WhenAll; Inngest runs each in its own call.
await Task.WhenAll(userIds.Select(id => context.Step.RunAsync($"load-{id}", ct => profiles.LoadAsync(id, ct)))); Fan-out over a list. A step per item, with a limit on how many run at once and the results back in order.
await context.Step.MapAsync("charge", due, (invoice, ct) => billing.ChargeAsync(invoice.Id, ct), new FanOutOptions { MaxParallelism = 5 }); Invoke other functions. Run another function as a step and get its typed result back.
var score = await context.Step.InvokeAsync<ScoreAccount, AccountScore>( "score-acme", new ScoreRequest("acme")); Scoped steps. A step with 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", (inventory, ct) => inventory.ReserveAsync(orderId, ct)); Event and cron triggers. Start a function from a typed event, a filtered event or a cron schedule.
[EventTrigger<OrderPlaced>(If = "event.data.total >= 500")] [CronTrigger("0 6 * * 1-5", Timezone = "UTC")] 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. Every step retries on its own. NonRetriableException stops the retries, and RetryAfterException sets the delay.
throw new RetryAfterException("Rate limited.", TimeSpan.FromMinutes(1)); Realtime and AI
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. The model calls and tool calls of a Microsoft.Extensions.AI IChatClient run as steps, so a retry or a replay never repeats them.
using Microsoft.Extensions.AI; builder.Services.AddChatClient(model) .UseInngestFunctionInvocation() .UseInngestSteps(); Blazor. Show realtime messages in Blazor Server, WebAssembly and interactive Auto with the RealtimeView component.
builder.Services.AddInngestRealtimeServerSource(); SignalR. Forward realtime messages to the clients of a SignalR hub, and decide who may join which channel.
builder.Services.AddInngestSignalR(options => options.Authorize = authorize); app.MapInngestRealtimeHub("/hubs/inngest"); Run it anywhere
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). 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 the feature beta, so the API may still change.
app.UseInngestDurableEndpoints(); app.MapPost("/pay", (IStepTools step) => step.RunAsync("charge", () => "paid")) .WithDurableEndpoint(); Azure Functions. Serve the endpoint from an HTTP-triggered function in the isolated worker model.
using Microsoft.Azure.Functions.Worker.Http; return await request.ServeInngestAsync(); AWS Lambda. Serve it from a Lambda function behind API Gateway or a function URL, as a managed function or a Native AOT executable.
var handler = new InngestLambdaHandler(services); return await handler.HandleAsync(request, context); 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 ShopEventJson : JsonSerializerContext; Integrations
Send events with IInngestClient, from application code or from a function as a step, with ids for deduplication and delayed starts.
await client.SendAsync(new OrderPlaced("o-1001", "c-7", 49.90m)); 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. 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. 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. Observe and wrap function runs, steps and event sends, for logging, metrics, tracing and error mapping.
inngest.AddMiddleware<TimingMiddleware>(); 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. With Sentry set up as usual, report the errors of functions and steps, tagged with the app, the function, the run and the event.
inngest.AddSentryReporting(); OpenTelemetry. Trace every call and step, and measure runs, retries and durations.
using OpenTelemetry.Trace; inngest.AddInstrumentation(); tracing.AddInngestInstrumentation(); Test and ship
Testing. Run a function in memory on the real execution engine, with mocked steps.
using Inngest.Testing; var mocks = new StepMocks().Run("charge", "ok"); var run = await host.RunAsync("checkout", orderPlaced, mocks); 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(); await server.StartAsync(); Aspire. Run the Inngest dev server as a container of your AppHost, with its UI linked in the Aspire dashboard.
var devServer = builder.AddInngest("inngest"); builder.AddProject<Projects.Shop>("shop") .WithReference(devServer).WaitFor(devServer); Source generator and analyzers. Turn on the generator, and one call registers every function class of the project. An analyzer finds the types a Native AOT app has not listed.
inngest.AddInngestFunctions(); Try it
dotnet new install Inngest.Templates dotnet new inngest-web -n Shop --http-port 5280 cd Shop && dotnet run In a second terminal in the same folder, start the Inngest dev server:
dotnet tool install --global dotnet-inngest dotnet inngest dev Then send an event with curl -X POST http://localhost:5280/hello/Ada and watch the run at http://localhost:8288.
Links
- 📖 Docs: https://damikun.github.io/inngest-dotnet/
- 💻 GitHub: https://github.com/damikun/inngest-dotnet
- 📦 NuGet: https://www.nuget.org/packages/Inngest
damikun / inngest-dotnet
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. .NET 8 and 10, Native AOT.
Feedback is very welcome, from .NET developers and from the Inngest team: open an issue on GitHub or leave a comment here. If you've wanted Inngest in .NET, a ⭐ on GitHub helps others find it.
What would you build with it?

