---
title: ".NET SDK for ASP.NET Core: install and options · camada"
description: "Install camada in ASP.NET Core with AddCamada() and UseCamada(): the options it takes, what this position sees, and how it fails open."
url: https://www.camada.app/docs/dotnet
last_updated: 2026-10-26
---

Docs

# .NET: ASP.NET Core

`Camada` enforces your snapshot inside a .NET process: your ordered custom rules, then allow, block and challenge, decided in-process, with events shipped off the request path. One package with an ASP.NET Core middleware and three `HttpContext` helpers.

## Install

Not on NuGet yet. From the folder that holds your .csproj: clone it next to the project, then reference it. Next to it, not inside it: an SDK-style project compiles every `.cs` file under its folder.

shell

```sh
git clone https://github.com/camada-app/camada-dotnet ../camada-dotnet
dotnet add reference ../camada-dotnet/src/Camada/Camada.csproj
```

.NET 8. No runtime dependencies beyond the ASP.NET Core shared framework.

## Mount it

Program.cs

```csharp
using Camada.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCamada();      // one engine from the environment, drained on shutdown

var app = builder.Build();
app.UseCamada();                   // FIRST: camada answers before routing, static files, anything
app.MapGet("/", (HttpContext ctx) => Results.Content($"<html><head>{ctx.CamadaScriptTag()}</head>…", "text/html"));
app.Run();
```

`AddCamada()` builds the engine when the container first resolves it — on the first request through the middleware — from the environment:

shell

```sh
export CAMADA_KEY=<ingest_token>.<snap_token>
export CAMADA_INGEST_URL=http://localhost:8787   # dev only; defaults to production ingest
```

Set these as environment variables: export them in the shell that runs dotnet run, add them to "environmentVariables" in Properties/launchSettings.json, or set them in your host's settings. camada reads only the environment, and ASP.NET does not pass appsettings.json, user-secrets or a .env file to it.

That build starts the snapshot poll on a background task and never blocks, so the request that triggered it is answered cold: it passes (fail open), and so does anything that arrives before the first poll lands. To enforce from request 1, resolve the engine at startup and wait for the boot poll:

warm-up

```csharp
// beside using Camada.AspNetCore; at the top of Program.cs
using Camada;
using Camada.Snapshot;

// after var app = builder.Build();
var engine = app.Services.GetRequiredService<CamadaEngine>();   // builds it; the boot poll is already running
if (engine.Snap != null)                                       // null when CAMADA_KEY is unset or CAMADA_DISABLED=1
{
    var deadline = Environment.TickCount64 + 5000;
    while (engine.Snap.Verdict(new MatchInput { Ip = "0.0.0.0" }).Reason == "cold" && Environment.TickCount64 < deadline)
    {
        Thread.Sleep(10);   // bounded: an unreachable analyst leaves it cold, and the app still fails open
    }
}
```

An app that reads its own configuration hands the values in through the options — credentials still travel as an environment map, never as constructor arguments. Outside the generic host, `UseCamada()` alone falls back to `CamadaEngine.Default`, a lazy singleton wired from the process environment; `CamadaEngine.Configure(options)` replaces it.

explicit config

```csharp
using Camada.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCamada(o =>   // instead of AddCamada(); myKey, myIngest: read from your own configuration
{
    o.Env = new Dictionary<string, string?> { ["CAMADA_KEY"] = myKey, ["CAMADA_INGEST_URL"] = myIngest };
    o.ChallengePath = "/api/__camada/challenge";
});
```

Quickstart: [ASP.NET Core](https://www.camada.app/docs/quickstart/aspnet).

## The options you will use

| Option | Default | Meaning |
| --- | --- | --- |
| `Env` | `the process environment` | Where CAMADA_KEY and friends are read from (the process one is read per request, for the kill switch). Pass a dictionary to configure in code. |
| `Transport` | `HttpClient` | The delegate that reaches the analyst; tests hand in an in-process fake. |
| `RefreshS` | `server-steered` | Poll cadence in seconds. Unset, the tenant config's poll_seconds steers it; set, it is pinned. |
| `Challenge` | `true` | Serve the proof-of-work page for challenge verdicts (CAMADA_CHALLENGE=0 switches it off too). |
| `ChallengePath` | `/__camada/challenge` | Where that page posts its solution. |
| `SnapshotVersion` | `5` | 4 drops your ordered custom rules; 3 the allow and challenge sections too. |
| `ScriptPath` | `/_cam/b.js` | Where the first-party beacon script is served. |
| `FpPath` | `/_cam/fp` | Where that script posts the beacon. Keep it in the same directory as ScriptPath: the script derives one from the other. |

Environment: `CAMADA_KEY`, `CAMADA_INGEST_URL`, `CAMADA_SNAPSHOT_URL`, `CAMADA_TRUSTED_PROXY`, `CAMADA_SERVERLESS=1` (a per-request staleness check, no timer), `CAMADA_CHALLENGE=0`, and the kill switch `CAMADA_DISABLED=1`, checked per request (set at boot, no timer starts at all). The [docs index](https://www.camada.app/docs) lists what each one means.

## What it does per request

1. Keeps the snapshot fresh off the request path: a background task on a `PeriodicTimer` at the cadence your tenant config sets (gzip on the wire, 304 when nothing changed), asking for snapshot v5, the container that carries your ordered custom rules. `CAMADA_SERVERLESS=1` checks staleness per request instead.
2. Resolves the client from the socket peer (`Connection.RemoteIpAddress`), combined with `X-Forwarded-For` only under your trusted-proxy setting. A raw forwarded header is never trusted without one. The SDK never reads the peer through the forwarded-headers middleware: if you add `UseForwardedHeaders()`, keep `UseCamada()` before it, so camada judges the raw header itself.
3. Runs your ordered custom rules, then the allow, block and challenge lists. A blocked request gets **403** with `x-block-reason` (and `x-block-rule` when a rule decided) before your app runs. The event still ships, marked as an SDK block so it is not confused with your own 403s.
4. A challenged browser gets the proof-of-work page; an API call reaches your app, and its response carries `x-camada-challenge: 1` for your frontend to act on. A request whose IP cannot be resolved is let through. See [challenge](https://www.camada.app/docs/challenge).
5. Serves the beacon endpoints, then runs your app with `x-rid` and the `_sfp` session cookie on its response (stamped from `Response.OnStarting`, so a handler that clears headers still carries them), the context in `HttpContext.Items["camada"]`, and when the response is done ships one batched, redacted event with its real status, latency and the matched route pattern. An exception in your app ships as `st: 500` and propagates unchanged.

## Add the beacon

Bots that never run JavaScript are the cheapest to catch. Put the tag in the `<head>` of the pages you render and the middleware does the rest:

beacon

```csharp
// Program.cs, above app.Run(): Mount it already maps this route
app.MapGet("/", (HttpContext ctx) =>
    Results.Content($"<html><head>{ctx.CamadaScriptTag()}</head><body>…</body></html>", "text/html"));
```

`ctx.CamadaScriptTag()` returns a `<script src="/_cam/b.js?r=…" async>` tag carrying this request's event id, so the beacon is joined to the page view it came from. The middleware serves the script at `GET /_cam/b.js` (an embedded resource, cacheable for an hour) and relays `POST /_cam/fp` onto the event batch, stamped with the client address camada resolved — never the one the body claims. Both endpoints sit behind the verdict, so a blocked client gets 403 there too. Turn the beacon off in the project's settings and the tag becomes empty and both endpoints fall through to your app.

## Report what your app knows

The wire shows a `POST /login`; only your handler knows whether it failed. Tell camada from the handler:

outcomes

```csharp
// Program.cs, above app.Run(); LoginForm (with an Email) and SignIn are your own form type and sign-in check
app.MapPost("/login", async (HttpContext ctx, LoginForm form) =>
{
    var ok = await SignIn(form);
    if (!ok) ctx.CamadaTrack("login_failed", user: form.Email);   // hashed in-process; never throws
    return ok ? Results.Redirect("/") : Results.Json(new { error = "invalid" }, statusCode: 401);
});
```

`ctx.CamadaTrack(event, user: …)` ships the outcome joined to this request's event and session. The user identifier is hashed in-process with your ingest token before it is queued — the raw value never leaves the process. It never throws; on a request the middleware did not run for it still ships the event, with no rid, sid or ip to join it to. The event name is free-form; the analyst's rules read this vocabulary:

| Event | When |
| --- | --- |
| `login_failed / login_succeeded` | a credential check settled |
| `signup` | an account was created |
| `password_reset` | a reset was requested |
| `mfa_failed` | a second factor was rejected |
| `payment_failed / payment_succeeded` | a charge settled |
| `coupon_failed` | a promo code was rejected |

A route you want to gate yourself can serve the challenge on demand; the answer is an `IResult` to return from the handler:

gating a route

```csharp
// Program.cs, above app.Run(): the proof-of-work page (or 403 JSON) until this browser holds a valid _cch, then null
app.MapGet("/export", (HttpContext ctx) => ctx.CamadaServeChallenge() ?? BigFile());   // BigFile(): your response, an IResult
```

## What this position sees

The same position as `@camada/node`, in a .NET process: the final status and latency, the session, the route pattern, the beacon's browser signals and your app's outcomes. The event carries the header order as `IHeaderDictionary` reports it, which is not the wire's order under Kestrel — camada knows what this tap can see from the position itself and never scores the absence of header order as evidence against a request. No ASN, country or TLS fingerprint reaches the middleware, so those conditions fail open in-app and the analyst resolves ASN and country itself. `matches` patterns are JS regexes run on `Regex` in ECMAScript mode (retried on the default engine when that mode refuses a spelling; one neither accepts never matches here while it does at the edge), each under a 50 ms timeout that reads as no match. What a rule can key on is on [rules and list entries](https://www.camada.app/docs/rules).

## Deploying it

- Every process polls its own snapshot (about 5 MB resident) and flushes its own batches; the tenant's `poll_seconds` keeps the cadence honest across a fleet.
- Pending events drain on the host's `ApplicationStopping` within half a second, and every engine also drains on `ProcessExit` (what a bare `CamadaEngine.Default` relies on). No signal handlers are installed — an app owns its own shutdown — so a process killed outright may drop its last batch.
- Serverless hosts: `CAMADA_SERVERLESS=1`. A cold invocation fails open and catches up on the next one.
- The poll is a plain background task, not an `IHostedService`: it needs no host to run under and starts with the engine.

## It fails open

Without a key the middleware is **inert**: one log line, no requests, no enforcement, so an unprovisioned environment behaves exactly as if camada were not installed. Beyond that, a dead ingest (logged at most once a minute, through `ILogger` under `AddCamada()` or stderr otherwise — the message only, never a stack trace), a corrupt snapshot or a bug in the package costs you the telemetry, never the request — camada can never turn one of your responses into a 5xx. The whole stance is on the [security](https://www.camada.app/security) page.

## Sitemap

[Every page on this site](https://www.camada.app/sitemap.md)
