Skip to the content.

CurlDotNet Implementation Patterns for Running curl in C#

These CurlDotNet implementation patterns show how teams run curl commands in C# beyond a single call: adding logging and timings, retrying on transient failures, fanning out across environments, replaying captured commands, and guarding what a command is allowed to do. Every snippet uses the shipped API (Curl.ExecuteAsync, CurlRequestBuilder, CurlSettings, CurlResult) and compiles against the current package.

Install the package first:

dotnet add package CurlDotNet

Snippets assume using CurlDotNet; plus using CurlDotNet.Core; where CurlResult, CurlSettings or CurlRequestBuilder are named.

Pattern 1: Observability Bootstrap

Why it is useful: Log status, size and timings for every request without wrapping the body of each call.

using CurlDotNet;
using CurlDotNet.Core;
using Microsoft.Extensions.Logging;

async Task<CurlResult> ExecuteLoggedAsync(string command, ILogger logger)
{
    var result = await Curl.ExecuteAsync(command);
    logger.LogInformation("curl {Command} -> {Status} in {TotalMs} ms ({Bytes} bytes)",
        command, result.StatusCode, result.Timings?.Total, result.Body?.Length ?? 0);
    return result;
}

var r = await ExecuteLoggedAsync("curl https://httpbin.org/get", logger);

CurlResult.Timings exposes NameLookup, Connect, AppConnect, PreTransfer, Redirect, StartTransfer and Total in milliseconds for HTTP requests.

Pattern 2: Environment Matrix Runner

Why it is useful: Run the same health check against dev, staging and production and see drift immediately.

using CurlDotNet;

record TargetEnvironment(string Name, string Url, string Token);

async Task<Dictionary<string, bool>> RunMatrixAsync(IEnumerable<TargetEnvironment> envs)
{
    var results = new Dictionary<string, bool>();
    foreach (var env in envs)
    {
        var r = await Curl.ExecuteAsync(
            $"curl --max-time 10 {env.Url}/health -H 'Authorization: Bearer {env.Token}'");
        results[env.Name] = r.IsSuccess;
    }
    return results;
}

For independent environments, build the command strings and pass them to Curl.ExecuteManyAsync to run them in parallel.

Pattern 3: Secrets from the Environment

Why it is useful: Keep tokens out of source. The parser expands $VAR, ${VAR}, %VAR% and $env:VAR from the process environment, exactly like a shell would.

using CurlDotNet;

// API_TOKEN is read from the environment at execution time; nothing is interpolated in C#.
var result = await Curl.ExecuteAsync(
    "curl https://httpbin.org/bearer -H 'Authorization: Bearer $API_TOKEN'");
Console.WriteLine(result.StatusCode);

If the token comes from a vault client instead, fetch it first and interpolate it into the string; never log the resulting command.

Pattern 4: Profiles with CurlSettings

Why it is useful: Give different callers (support tooling, batch importers) different timeouts and retry budgets without changing the curl strings.

using CurlDotNet;
using CurlDotNet.Core;

static class Profiles
{
    public static CurlSettings SupportAgent => new CurlSettings()
        .WithTimeout(10).WithHeader("X-Actor", "support");

    public static CurlSettings BatchImporter => new CurlSettings()
        .WithTimeout(60).WithHeader("X-Actor", "batch");
}

var result = await Curl.ExecuteAsync("curl https://httpbin.org/get", Profiles.BatchImporter);
Console.WriteLine(result.StatusCode);

CurlSettings also offers WithConnectTimeout, WithFollowRedirects, WithInsecure, WithRetries, WithHeaders, WithCancellation, WithProgress, WithProxy, WithUserAgent, WithCookies (a CookieContainer sent and updated on every hop, so a session carries over between calls), WithRedirectHandler, WithAutoDecompression and WithBufferSize. The engine applies every one of them, to every URL of a multi-URL command, and a value set in the settings overrides the same option in the command.

Pattern 5: Contract-Locked Responses

Why it is useful: Fail loudly when an API changes shape, before bad data reaches production.

using System.Text.Json;
using CurlDotNet;
using CurlDotNet.Core;

async Task<T> ExecuteWithContractAsync<T>(CurlRequestBuilder builder, Func<JsonElement, bool> check)
{
    var result = (await builder.ExecuteAsync()).EnsureSuccess(); // throws on 4xx/5xx
    using var doc = JsonDocument.Parse(result.Body);
    if (!check(doc.RootElement))
        throw new InvalidOperationException("Response violates contract");
    return result.ParseJson<T>();
}

var builder = CurlRequestBuilder.Get("https://api.github.com/repos/curl/curl");
var repo = await ExecuteWithContractAsync<Dictionary<string, object>>(builder, e => e.TryGetProperty("name", out _));

EnsureSuccess() throws CurlDotNet.Core.CurlHttpException carrying StatusCode, ResponseBody and ResponseHeaders.

Pattern 6: Replay Harness

Why it is useful: Replay captured curl commands from a JSON file to reproduce an incident deterministically.

using System.Text.Json;
using CurlDotNet;

record ReplayEnvelope(string Command, string? ExpectedBodySubstring);

var envelopes = JsonSerializer.Deserialize<List<ReplayEnvelope>>(await File.ReadAllTextAsync("replay.json"))!;
foreach (var env in envelopes)
{
    var r = await Curl.ExecuteAsync(env.Command);
    if (!r.IsSuccess)
        Console.WriteLine($"Replay failed: {r.StatusCode} for {env.Command}");
    else if (env.ExpectedBodySubstring is { Length: > 0 } needle && !r.Body.Contains(needle))
        Console.WriteLine($"Replay mismatch for {env.Command}");
}

Run Curl.Validate(env.Command) first to reject malformed entries without sending anything.

Pattern 7: Feature-Flagged Commands

Why it is useful: Switch between a control and a candidate command (different headers, flags or endpoints) from a flag provider.

using CurlDotNet;
using CurlDotNet.Core;

async Task<CurlResult> ExecuteFlaggedAsync(Func<bool> flag)
{
    var control = "curl https://httpbin.org/get -H 'X-Tier: control'";
    var candidate = "curl https://httpbin.org/get -H 'X-Tier: candidate' --compressed";
    return await Curl.ExecuteAsync(flag() ? candidate : control);
}

Pattern 8: Tenant-Aware Routing

Why it is useful: Pick base URL and credentials per tenant while keeping one request shape.

using CurlDotNet.Core;

record TenantSettings(string TenantId, string BaseUrl, string ApiKey);

Task<CurlResult> ExecuteForTenantAsync(TenantSettings tenant, string relativePath) =>
    CurlRequestBuilder.Get($"{tenant.BaseUrl}{relativePath}")
        .WithHeader("X-Tenant", tenant.TenantId)
        .WithBearerToken(tenant.ApiKey)
        .WithTimeout(TimeSpan.FromSeconds(15))
        .ExecuteAsync();

var result = await ExecuteForTenantAsync(
    new TenantSettings("acme", "https://httpbin.org", "key"), "/bearer");

Call .ToCurlCommand() on the builder to log the equivalent curl string.

Pattern 9: Evidence Bundles

Why it is useful: Serialize command, status, headers and body so auditors can see exactly what ran.

using System.Text.Json;
using CurlDotNet;

async Task CaptureAsync(string workflow, string operatorId, string command, string outputPath)
{
    var r = await Curl.ExecuteAsync(command);
    var bundle = new { workflow, operatorId, command, r.StatusCode, r.Headers, r.Body,
                       timestamp = DateTimeOffset.UtcNow };
    await File.WriteAllTextAsync(outputPath,
        JsonSerializer.Serialize(bundle, new JsonSerializerOptions { WriteIndented = true }));
}

Strip Authorization from command before writing if the bundle leaves your system.

Pattern 10: Reading Rate-Limit Headers

Why it is useful: Track remaining quota from response headers and pause before the server starts returning 429.

using CurlDotNet;

var result = await Curl.ExecuteAsync("curl https://api.github.com/rate_limit");
var remaining = int.Parse(result.GetHeader("X-RateLimit-Remaining") ?? "0");
var resetAt = DateTimeOffset.FromUnixTimeSeconds(long.Parse(result.GetHeader("X-RateLimit-Reset") ?? "0"));

if (remaining == 0)
    Console.WriteLine($"Quota exhausted; resets at {resetAt:u}");

CurlResult.Headers is case-insensitive; GetHeader returns null when the header is absent.

Pattern 11: Retries for Transient Failures

Why it is useful: Transient failures (timeouts, 429, 503) should not fail a whole job. curl’s --retry handles the common case: it retries timeouts and HTTP 408/429/500/502/503/504 with doubling back-off, honours Retry-After, and --retry-max-time caps the total. CurlSettings.WithRetries(count, delayMs) retries any failure. See curl –retry in C#.

using CurlDotNet;
using CurlDotNet.Core;

var result = await Curl.ExecuteAsync("curl -m 10 --retry 3 https://httpbin.org/status/503");

var viaSettings = await Curl.ExecuteAsync("curl -m 10 https://httpbin.org/get",
    new CurlSettings().WithRetries(count: 3, delayMs: 500));

For a custom policy, such as retrying only some status codes, a small loop still works:

using CurlDotNet;
using CurlDotNet.Core;

async Task<CurlResult> RetryOnStatusAsync(string command, int attempts = 3)
{
    CurlResult result = null!;
    for (var attempt = 1; attempt <= attempts; attempt++)
    {
        result = await Curl.ExecuteAsync(command, new CurlSettings().WithTimeout(10));
        if (result.StatusCode is not (429 or 502 or 503 or 504)) return result;
        await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
    }
    return result;
}

A result also offers await result.Retry() to re-run the same command once more.

Pattern 12: Shadow (Dark-Launch) Calls

Why it is useful: Call the current and candidate API versions side by side and compare before cutting over.

using CurlDotNet;

var results = await Curl.ExecuteManyAsync(
    "curl https://httpbin.org/anything/v1/orders",
    "curl https://httpbin.org/anything/v2/orders");

var (v1, v2) = (results[0], results[1]);
Console.WriteLine($"v1:{v1.StatusCode} v2:{v2.StatusCode} same body: {v1.Body == v2.Body}");

Pattern 13: Cache Warmers and Health Probes

Why it is useful: Touch a list of URLs on a schedule with HEAD requests and report status codes, replacing a bash loop.

using CurlDotNet;

async Task WarmAsync(IEnumerable<string> urls, CancellationToken token)
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
    do
    {
        foreach (var url in urls)
        {
            var r = await Curl.ExecuteAsync($"curl -I --max-time 5 {url}", token);
            Console.WriteLine($"{url}: {r.StatusCode}");
        }
    } while (await timer.WaitForNextTickAsync(token));
}

Cancelling the token while a request is in flight throws the standard OperationCanceledException (as does PeriodicTimer.WaitForNextTickAsync), so one catch (OperationCanceledException) around WarmAsync handles shutdown.

Pattern 14: Policy Checks Before Execution

Why it is useful: Reject commands that violate policy (for example --insecure outside development) before anything is sent.

using CurlDotNet;
using CurlDotNet.Core;

Task<CurlResult> ExecuteWithPolicyAsync(string command)
{
    var validation = Curl.Validate(command);
    if (!validation.IsValid)
        throw new ArgumentException(validation.Error, nameof(command));
    if (command.Contains("--insecure") || command.Contains(" -k "))
        throw new InvalidOperationException("Insecure TLS is not allowed");
    return Curl.ExecuteAsync(command);
}

Pattern 15: Onboarding Katas

Why it is useful: Self-verifying xUnit tests give new engineers hands-on curl practice with immediate feedback.

using CurlDotNet;
using Xunit;

public class Kata01_GetRequest
{
    [Fact]
    public async Task Should_Fetch_Public_Data()
    {
        var result = await Curl.ExecuteAsync("curl https://api.github.com");
        Assert.True(result.IsSuccess);
        Assert.Contains("current_user_url", result.Body);
    }
}

Pattern 16: Generating Client Code from a Command

Why it is useful: Hand a working curl command to another team in their language, or show the equivalent HttpClient code during a migration.

using CurlDotNet;

var command = "curl -X POST https://httpbin.org/post -H 'Accept: application/json' -d 'a=1'";
Console.WriteLine(Curl.ToHttpClient(command));     // C# HttpClient
Console.WriteLine(Curl.ToPythonRequests(command)); // Python requests
Console.WriteLine(Curl.ToFetch(command));          // JavaScript fetch
Console.WriteLine(Curl.ToPowershellCode(command)); // PowerShell