DEV Community

Cover image for A community Inngest SDK for .NET: durable workflows as plain C# classes
Dalibor Kundrat
Dalibor Kundrat

Posted on Edited on AI-assisted

A community Inngest SDK for .NET: durable workflows as plain C# classes

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 };
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it, and map the endpoint Inngest calls:

var inngest = builder.Services
    .AddInngest(options => options.AppId = "shop")
    .AddFunction<SayHello>();

app.MapInngest("/api/inngest");
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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))));
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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")]
Enter fullscreen mode Exit fullscreen mode

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")]
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

builder.Services.AddInngestRealtimeServerSource();
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

inngest.AddMiddleware<TimingMiddleware>();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

using OpenTelemetry.Trace;

inngest.AddInstrumentation();
tracing.AddInngestInstrumentation();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Try it

dotnet new install Inngest.Templates
dotnet new inngest-web -n Shop --http-port 5280
cd Shop && dotnet run
Enter fullscreen mode Exit fullscreen mode

In a second terminal in the same folder, start the Inngest dev server:

dotnet tool install --global dotnet-inngest
dotnet inngest dev
Enter fullscreen mode Exit fullscreen mode

Then send an event with curl -X POST http://localhost:5280/hello/Ada and watch the run at http://localhost:8288.

Links

GitHub logo 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?

Top comments (0)