.NET SDK
The official .NET client for FetchCatch. Two NuGet packages, both public:
| Package | Use it when |
|---|---|
FetchCatch.Client |
Anything .NET — console apps, workers, libraries. No DI dependency. |
FetchCatch.AspNetCore |
ASP.NET Core hosts. Adds AddFetchCatch() for one-line DI registration with IHttpClientFactory. |
Both target net8.0, net10.0, and (for FetchCatch.Client) netstandard2.0 so .NET Framework 4.7.2+ also works.
Install
# In an ASP.NET Core project:
dotnet add package FetchCatch.AspNetCore
# Console / worker / library:
dotnet add package FetchCatch.Client
Authenticate
Create a Tenant API key from Console → Settings → API keys and pass it to the SDK. Treat it like a database password — never commit it.
# environment variable
setx FETCHCATCH_API_KEY "fc_live_..."
// appsettings.json
{
"FetchCatch": {
"BaseUrl": "https://api.fetchcatch.com",
"ApiKey": "fc_live_..."
}
}
Quick start (ASP.NET Core)
// Program.cs
builder.Services.AddFetchCatch(builder.Configuration);
// CheckoutService.cs
public class CheckoutService(IFetchCatchClient fc)
{
public async Task<CheckoutDecision> EvaluateAsync(CheckoutInput input, CancellationToken ct)
{
var result = await fc.EvaluateAsync<CheckoutInput, CheckoutDecision>(
flowSlug: "checkout",
input: input,
options: new EvaluateOptions { CorrelationKey = input.OrderId },
cancellationToken: ct);
return result.Status switch
{
RunStatus.Completed => result.Decision!,
RunStatus.Waiting => throw new CheckoutPausedException(result.RunId, result.WaitingForEvent!),
_ => throw new FetchCatchException(result.Error ?? "Unknown error"),
};
}
}
public record CheckoutInput(string OrderId, decimal Amount, string UserId);
public record CheckoutDecision(bool Approved, string? Reason);
Quick start (console / worker)
using FetchCatch.Client;
using var fc = new FetchCatchClient(new FetchCatchOptions
{
ApiKey = Environment.GetEnvironmentVariable("FETCHCATCH_API_KEY")!,
});
var result = await fc.EvaluateAsync("hello", new { name = "world" });
Console.WriteLine(result.Decision); // JsonElement when you don't specify TDecision
API surface
`EvaluateAsync(slug, input, options?, ct?)`
Calls a published flow. Returns EvaluateResponse<TDecision> with one of three useful outcomes:
Status |
What it means | What to do |
|---|---|---|
Completed |
The flow reached an End node. Decision is populated. |
Use result.Decision. |
Waiting |
The flow paused at a wait_for_event node. WaitingForEvent tells you which event to send. |
Persist result.RunId, render UI, call ResumeAsync later. |
Failed / TimedOut |
Unhandled error or expired wait. Error is populated. |
Treat as a failure. |
Use the helpers IsCompleted, IsWaiting, IsFailed for clean branching.
`ResumeAsync(runId, eventName, payload, ct?)`
Continues a paused run. eventName must match result.WaitingForEvent exactly.
var resumed = await fc.ResumeAsync<UserConfirmation, CheckoutDecision>(
runId: pendingRunId,
eventName: "user_confirmed",
payload: new UserConfirmation { Code = "123456" });
`ListRunsAsync(filter?, ct?)`
Streams matching runs with automatic cursor pagination — you never see a cursor token. The returned IAsyncEnumerable<FlowRunListItem> stops when the server runs out, when MaxItems is reached, or when you cancel.
var lastHour = new RunFilter
{
Status = RunStatus.Failed,
FromUtc = DateTime.UtcNow.AddHours(-1),
MaxItems = 500,
};
await foreach (var run in fc.ListRunsAsync(lastHour, ct))
{
logger.LogWarning("{Flow} {Run}: {Error}", run.FlowSlug, run.Id, run.Error);
}
`GetRunAsync(runId, ct?)`
Single run with its full per-step trace. Returns null for unknown ids (no exception).
`GetRunStatsAsync(fromUtc?, toUtc?, ct?)`
Aggregate KPIs — the exact numbers behind the FetchCatch console dashboard. Default range is the last 24 hours.
var stats = await fc.GetRunStatsAsync(DateTime.UtcNow.AddDays(-7), DateTime.UtcNow);
Console.WriteLine($"{stats.Completed}/{stats.Total} succeeded ({stats.SuccessRate:P0})");
Console.WriteLine($"p50 {stats.P50Ms}ms · p95 {stats.P95Ms}ms · p99 {stats.P99Ms}ms");
foreach (var top in stats.TopFlows.Take(5))
Console.WriteLine($" {top.FlowSlug}: {top.Total} ({top.Failed} failed)");
Error handling
Every non-2xx response maps to a structured exception:
| HTTP | Exception | When |
|---|---|---|
| 400 | FetchCatchValidationException |
Payload didn't match the flow's input schema. |
| 401 / 403 | FetchCatchAuthException |
API key missing, revoked, or wrong workspace. |
| 404 | FetchCatchNotFoundException |
Flow slug, run id, or workspace not found. |
| 429 | FetchCatchRateLimitedException |
Read RetryAfter to schedule retry. |
| 5xx, network | FetchCatchException (base) |
Inspect StatusCode and ResponseBody. Retried automatically by default. |
try
{
await fc.EvaluateAsync<CheckoutInput, CheckoutDecision>("checkout", input);
}
catch (FetchCatchRateLimitedException rl)
{
await Task.Delay(rl.RetryAfter ?? TimeSpan.FromSeconds(5));
// retry…
}
catch (FetchCatchNotFoundException)
{
// The flow slug is wrong, surface it in the build pipeline.
}
Configuration reference
| Option | Default | Notes |
|---|---|---|
BaseUrl |
https://api.fetchcatch.com |
Override for self-hosted / staging. |
ApiKey |
(required) | Tenant API key. |
Timeout |
30s |
Per-request HTTP timeout. Matches the server's evaluate ceiling. |
EnableRetries |
true |
Retry transient 5xx and network errors with exponential backoff. |
MaxRetryAttempts |
3 |
Including the initial attempt. |
UserAgentApp |
null |
Appended to User-Agent. Surfaces in support / rate-limit logs. |
Observability
The SDK exposes an ActivitySource("FetchCatch.Client") — every public method opens a span (fetchcatch.evaluate, fetchcatch.resume, fetchcatch.list_runs, etc.). To export spans via OpenTelemetry:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("FetchCatch.Client").AddOtlpExporter());
Mocking in tests
Inject the IFetchCatchClient interface and swap with your favourite mocking library:
var fc = Substitute.For<IFetchCatchClient>();
fc.EvaluateAsync<CheckoutInput, CheckoutDecision>(default!, default!, default, default)
.ReturnsForAnyArgs(Task.FromResult(new EvaluateResponse<CheckoutDecision>
{
RunId = Guid.NewGuid(),
Status = RunStatus.Completed,
Decision = new CheckoutDecision(Approved: true, Reason: null),
}));
AOT and trimming
FetchCatch.Client works under AOT for the inspection APIs (ListRunsAsync, GetRunAsync, GetRunStatsAsync) which use known wire types. The generic EvaluateAsync<TInput, TDecision> and ResumeAsync<TPayload, TDecision> overloads use reflection-based JSON for caller-supplied types — v0.2 will add JsonTypeInfo<T> overloads for full AOT compatibility. Until then, AOT consumers can use the non-generic EvaluateAsync / ResumeAsync overloads that return EvaluateResponse<JsonElement> and parse the decision themselves with their own source-gen context.
Versioning
The SDK follows semver. The HTTP API (/v1/...) is stable; SDK surface may evolve before 1.0. Pin a version in production:
<PackageReference Include="FetchCatch.Client" Version="0.1.*" />
License
MIT. See github.com/teclogist/fetchcatch.