# Getting started with Inngest in ASP.NET Core

> Add the Inngest .NET SDK to an ASP.NET Core app, write a first durable function in C#, run it against the Inngest dev server and watch each step run.

Web page: https://damikun.github.io/inngest-dotnet/guides/getting-started/

In about five minutes you will have an ASP.NET Core app with a durable function running on your machine. When a
user signs up, it sends a welcome email, waits, and sends a second one; if the second email fails, a retry sends
only that one. The dev server shows every step as it happens.

Figure: What you will run: the app sends an event when a user signs up, and the dev server runs the function by calling the app's endpoint once per step. Its UI shows every run.

> **You need**
>
> - The [.NET SDK](https://dotnet.microsoft.com/download), version 8 or 10.
> - A way to run the [Inngest dev server](https://www.inngest.com/docs/local-development): its install script,
>   Homebrew, Node.js or Docker.

> **Start from a template**
>
> `dotnet new install Inngest.Templates` and `dotnet new inngest-web -n Shop` make a project like the one in this guide,
> and `dotnet inngest dev` runs the dev server for it. See [Templates and tool](https://damikun.github.io/inngest-dotnet/guides/templates-and-tools.md).

## 1. Add the SDK

Install `Inngest.AspNetCore` from NuGet. It brings in `Inngest`:

```bash
dotnet add package Inngest.AspNetCore
```

The SDK is split into these packages:

| Package | What it gives you |
| - | - |
| [`Inngest`](https://www.nuget.org/packages/Inngest/) | Functions, steps, triggers, flow control, middleware, the client that sends events, and request signing. A source generator that registers your function classes, which a project turns on, and an analyzer that finds the types a Native AOT app has not listed, come with it. See [Source generator and analyzers](https://damikun.github.io/inngest-dotnet/guides/source-generator.md). |
| [`Inngest.AspNetCore`](https://www.nuget.org/packages/Inngest.AspNetCore/) | `MapInngest`, the endpoint Inngest calls in an ASP.NET Core app, and a health check for readiness probes. |
| [`Inngest.AwsLambda`](https://www.nuget.org/packages/Inngest.AwsLambda/) | `InngestLambdaHandler`, which serves the endpoint from an AWS Lambda function behind API Gateway or a function URL. See [Running on AWS Lambda](https://damikun.github.io/inngest-dotnet/guides/aws-lambda.md). |
| [`Inngest.AzureFunctions`](https://www.nuget.org/packages/Inngest.AzureFunctions/) | `ServeInngestAsync`, the endpoint Inngest calls, served from an HTTP-triggered function of an Azure Functions app. See [Azure Functions](https://damikun.github.io/inngest-dotnet/guides/azure-functions.md). |
| [`Inngest.Blazor`](https://www.nuget.org/packages/Inngest.Blazor/) | A Blazor component that shows realtime messages on the server, in WebAssembly and in interactive Auto. See [Realtime in Blazor](https://damikun.github.io/inngest-dotnet/guides/blazor.md). |
| [`Inngest.Connect`](https://www.nuget.org/packages/Inngest.Connect/) | `AddConnect`, a hosted worker that receives calls over an outbound WebSocket, with no endpoint to expose. See [Connect](https://damikun.github.io/inngest-dotnet/guides/connect.md). |
| [`Inngest.Aspire.Hosting`](https://www.nuget.org/packages/Inngest.Aspire.Hosting/) | An Aspire hosting integration for the AppHost project: runs the Inngest server as a container and gives projects the settings the SDK reads. See [Aspire](https://damikun.github.io/inngest-dotnet/guides/aspire.md). |
| [`Inngest.Testing`](https://www.nuget.org/packages/Inngest.Testing/) | An in-memory test host that runs your functions on the real execution engine. See [Testing functions](https://damikun.github.io/inngest-dotnet/guides/testing.md). |
| [`Inngest.Testing.Containers`](https://www.nuget.org/packages/Inngest.Testing.Containers/) | A Testcontainers module that runs the Inngest dev server or a self-hosted server in Docker for end-to-end tests. See [Testing with containers](https://damikun.github.io/inngest-dotnet/guides/testing-with-containers.md). |
| [`Inngest.Testing.Containers.AspNetCore`](https://www.nuget.org/packages/Inngest.Testing.Containers.AspNetCore/) | A `WebApplicationFactory` that runs an ASP.NET Core app on a real port against that server (.NET 10). See [Testing with containers](https://damikun.github.io/inngest-dotnet/guides/testing-with-containers.md#an-app-under-webapplicationfactory). |
| [`Inngest.OpenTelemetry`](https://www.nuget.org/packages/Inngest.OpenTelemetry/) | Traces and metrics for function calls and steps. |
| [`Inngest.Encryption`](https://www.nuget.org/packages/Inngest.Encryption/) | End-to-end encryption of event data, step results and function output. See [Encryption](https://damikun.github.io/inngest-dotnet/guides/middleware.md#encryption). |
| [`Inngest.Sentry`](https://www.nuget.org/packages/Inngest.Sentry/) | Reports the errors of functions and steps to Sentry. See [Middleware](https://damikun.github.io/inngest-dotnet/guides/middleware.md#sentry). |
| [`Inngest.Extensions.AI`](https://www.nuget.org/packages/Inngest.Extensions.AI/) | Makes the model calls and tool calls of a Microsoft.Extensions.AI `IChatClient` steps of the run. See [AI chat clients](https://damikun.github.io/inngest-dotnet/guides/ai.md). |
| [`Inngest.EntityFrameworkCore`](https://www.nuget.org/packages/Inngest.EntityFrameworkCore/) | A transactional outbox for Entity Framework Core: events saved with your data and sent to Inngest afterwards. See [Transactional outbox](https://damikun.github.io/inngest-dotnet/guides/outbox.md). |
| [`Inngest.Templates`](https://www.nuget.org/packages/Inngest.Templates/) | `dotnet new inngest-web` and `dotnet new inngest-worker`, which make a project like the one below. See [Templates and tool](https://damikun.github.io/inngest-dotnet/guides/templates-and-tools.md). |
| [`dotnet-inngest`](https://www.nuget.org/packages/dotnet-inngest/) | A .NET tool: `dotnet inngest dev` downloads the Inngest CLI and runs the dev server for the app. See [Templates and tool](https://damikun.github.io/inngest-dotnet/guides/templates-and-tools.md#the-dotnet-inngest-command). |
| [`Inngest.SignalR`](https://www.nuget.org/packages/Inngest.SignalR/) | Forwards Inngest realtime messages to the clients of a SignalR hub. See [SignalR](https://damikun.github.io/inngest-dotnet/guides/signalr.md). |
| [`Inngest.UserContext`](https://www.nuget.org/packages/Inngest.UserContext/) | Carries the signed-in user who caused a run into it, as `ICurrentUser`. See [User context](https://damikun.github.io/inngest-dotnet/guides/user-context.md). |
| [`Inngest.UserContext.AspNetCore`](https://www.nuget.org/packages/Inngest.UserContext.AspNetCore/) | Takes the user an event is sent for from `HttpContext.User`. |
| [`Inngest.Webhooks`](https://www.nuget.org/packages/Inngest.Webhooks/) | Verifies the signatures of webhooks from Stripe, GitHub, Shopify, Clerk, Resend and other providers and sends each as an event, without a provider SDK. `Inngest.AspNetCore` brings it in. See [Webhooks](https://damikun.github.io/inngest-dotnet/guides/webhooks.md). |

## 2. Write a function

A function is a class. `[InngestFunction]` gives it an id and `[EventTrigger]` names the event that starts it. Each
`RunAsync` is a step: it runs once and Inngest saves its result. `SleepAsync` pauses the run without holding a thread.

```csharp title="OnboardUser.cs"
using Inngest;

public sealed record UserSignedUp(string UserId, string Email);

[InngestFunction("onboard-user")]
[EventTrigger("user/signed-up")]
public sealed class OnboardUser(IEmailSender email) : 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",
            ct => email.SendAsync(user.Email, "Welcome!", ct));

        await context.Step.SleepAsync("wait-before-tips", TimeSpan.FromSeconds(30));

        await context.Step.RunAsync("send-tips",
            ct => email.SendAsync(user.Email, "Three things to try first", ct));

        return new { user.UserId };
    }
}
```

The email service only logs for now:

```csharp title="EmailSender.cs"
public interface IEmailSender
{
    Task SendAsync(string to, string subject, CancellationToken cancellationToken);
}

public sealed class ConsoleEmailSender(ILogger<ConsoleEmailSender> logger) : IEmailSender
{
    public Task SendAsync(string to, string subject, CancellationToken cancellationToken)
    {
        logger.LogInformation("Sending \"{Subject}\" to {To}", subject, to);
        return Task.CompletedTask;
    }
}
```

## 3. Serve it

`AddInngest` registers the SDK and the function, `MapInngest` maps the endpoint Inngest calls, and `/signup`
sends the event that starts a run:

```csharp title="Program.cs"
using Inngest;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IEmailSender, ConsoleEmailSender>();
builder.Services
    .AddInngest(options => options.AppId = "shop")
    .AddFunction<OnboardUser>();

var app = builder.Build();

app.MapInngest("/api/inngest");

app.MapPost("/signup", async (SignupRequest request, IInngestClient inngest, CancellationToken cancellationToken) =>
{
    var userId = Guid.NewGuid().ToString("N");
    await inngest.SendAsync(new InngestEvent("user/signed-up", new UserSignedUp(userId, request.Email)), cancellationToken);
    return Results.Accepted(value: new { userId });
});

app.Run();

public sealed record SignupRequest(string Email);
```

The function's options are attributes on the class here. They can also be passed in code, as `FunctionOptions` given
to `AddFunction`, which suits values the app reads from configuration;
[Functions and triggers](https://damikun.github.io/inngest-dotnet/guides/functions-and-triggers.md#code-first-or-attributes) shows both forms.

> **Tip**
>
> Keep the app id and the function id stable. Inngest knows this function as `shop-onboard-user`; renaming it
> creates a new function.

## 4. Run it

1. Start the app in dev mode. `INNGEST_DEV=1` points the SDK at the local dev server:

   **bash**

   ```bash
   INNGEST_DEV=1 dotnet run --urls http://localhost:5280
   ```

   **PowerShell**

   ```powershell
   $env:INNGEST_DEV = "1"; dotnet run --urls http://localhost:5280
   ```

2. Start the dev server and point it at the app. Use the install script or Homebrew on macOS and Linux, npx when
   Node.js is installed, Docker when it is not, and Compose when a team shares the setup:

   **Install script**

   ```bash
   curl -sSfL https://cli.inngest.com/install.sh | sh
   inngest dev -u http://localhost:5280/api/inngest
   ```

   **Homebrew**

   ```bash
   brew install inngest/tap/inngest
   inngest dev -u http://localhost:5280/api/inngest
   ```

   **npx**

   ```bash
   npx --ignore-scripts=false inngest-cli@latest dev -u http://localhost:5280/api/inngest
   ```

   **.NET tool**

   ```bash
   dotnet tool install --global dotnet-inngest
   dotnet inngest dev -u http://localhost:5280/api/inngest
   ```

   The 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 from `Properties/launchSettings.json`.

   **Docker**

   ```bash
   docker run --rm -p 8288:8288 -p 8289:8289 inngest/inngest:v1.49.0 inngest dev -u http://host.docker.internal:5280/api/inngest
   ```

   The container reaches the app at `host.docker.internal`, since `localhost` inside it is the container itself. On Linux without Docker Desktop, add `--add-host=host.docker.internal:host-gateway`.

   **Compose**

   ```yaml title="compose.yaml"
   services:
     inngest:
       image: inngest/inngest:v1.49.0
       command: inngest dev -u http://host.docker.internal:5280/api/inngest
       ports:
         - "8288:8288"
         - "8289:8289"
       extra_hosts:
         - "host.docker.internal:host-gateway"
   ```

   ```bash
   docker compose up
   ```

3. Sign up a user:

   ```bash
   curl -X POST http://localhost:5280/signup -H 'Content-Type: application/json' -d '{"email": "ada@example.com"}'
   ```

4. Open `http://localhost:8288` and select **Runs**. The run shows `send-welcome-email`, the sleep, and after
   30 seconds `send-tips`.

## What happened

Inngest called the app four times for that one run, once per step and once more for the output:

Figure: Every call runs `ExecuteAsync` from the top. Steps that finished earlier return their saved results at once, so the welcome email goes out once however many calls pass it.

That is why side effects belong in steps and why step ids must not change while runs are in flight.
[Steps](https://damikun.github.io/inngest-dotnet/guides/steps.md) explains both, and [How it works](https://damikun.github.io/inngest-dotnet/how-it-works.md) lets you step
through a run.

## Next steps

- [Functions and triggers](https://damikun.github.io/inngest-dotnet/guides/functions-and-triggers.md): Event filters, cron schedules and functions as delegates.
- [Sleep and wait for events](https://damikun.github.io/inngest-dotnet/guides/sleep-and-wait-for-events.md): Pause a run until a time or until something happens.
- [Deploying](https://damikun.github.io/inngest-dotnet/guides/deploying.md): Run the app against Inngest Cloud.
- [The sample app](https://github.com/damikun/inngest-dotnet/tree/main/samples/Inngest.Sample): samples/Inngest.Sample: a complete app with several functions.
