Typed client for FetchCatch.Client and FetchCatch.AspNetCore.

.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.