Skip to the content.

How CurlDotNet works

CurlDotNet works by parsing a curl command string into a CurlOptions object in C#, then sending that request with .NET’s own HttpClient (or FtpWebRequest and the file system for ftp:// and file://). There is no native libcurl, no curl.exe, no Process.Start and no P/Invoke: it is managed code on every target framework.

dotnet add package CurlDotNet

The pipeline

"curl -X POST -H ... -d ... https://..."
        |
        v
  CommandParser.Parse       (CurlDotNet.Core.CommandParser)
        |   1. join line continuations: \  `  ^
        |   2. drop the leading "curl"
        |   3. expand $VAR, ${VAR}, %VAR%, $env:VAR (not inside single quotes)
        |   4. split into arguments using shell quoting rules
        |   5. map each flag onto a property (curl 8.7.1's full option table,
        |      -K config files, --variable, .netrc)
        |   6. expand URL globs and split URLs / --next groups into transfers
        v
  CurlOptions               (URL, method, headers, data, auth, proxy, timeouts, output...;
        |                    further transfers in AdditionalTransfers)
        |
        v
  CurlEngine                picks a handler from the URL scheme
        |-- http / https -> HttpHandler  (HttpClient)
        |-- ftp / ftps   -> FtpHandler   (FtpWebRequest)
        |-- file         -> FileHandler  (System.IO)
        v
  CurlResult                (StatusCode, Headers, Body or BinaryData, OutputFiles, Timings)

CurlRequestBuilder and LibCurl build a CurlOptions directly and skip the parser. Curl.ExecuteAsync(CurlOptions) takes one too.

Parsing like a shell

The parser is modelled on curl’s own argument handling (src/tool_getparam.c) and on how shells split words:

You can see the result without sending anything:

using CurlDotNet;

var check = Curl.Validate("curl -X PUT -H 'X-Id: 7' -d 'a=1' https://httpbin.org/put");
Console.WriteLine(check.IsValid);                  // True
Console.WriteLine(check.ParsedOptions.Method);     // PUT
Console.WriteLine(check.ParsedOptions.Headers["X-Id"]);  // 7

Sending with HttpClient

Text or binary?

The body is always read as bytes first. CurlDotNet then decides whether to decode it as text from the Content-Type (with a registry of known binary types such as Office Open XML, PDF and ZIP) plus a byte-level sniff, and a missing Content-Type counts as binary. Text goes to result.Body; everything else goes to result.BinaryData, unchanged. -o writes whichever one applies.

Errors as curl exit codes

.NET exceptions are mapped to curl’s error codes by what actually failed: a name that does not resolve becomes CurlCouldntResolveHostException (6), a refused connection CurlCouldntConnectException (7), a rejected certificate CurlPeerFailedVerificationException (60), a timeout CurlOperationTimeoutException (28), an empty reply CurlGotNothingException (52), and so on. HTTP status codes are data, not exceptions, as with the curl CLI; -f turns 400 and above into CurlHttpReturnedErrorException (22). A cancelled CancellationToken surfaces as OperationCanceledException. See error handling and the exit code reference.

Code generation

Curl.ToHttpClient, ToFetch, ToPythonRequests and ToPowershellCode run the same parser and print code from the resulting CurlOptions instead of sending it. See convert curl to C#.

Packaging

One assembly targeting netstandard2.0, net472, net48, net8.0, net9.0 and net10.0. JSON goes through System.Text.Json, or Newtonsoft.Json on netstandard2.0. On netstandard2.0 and .NET Framework, CurlDotNet works from HttpClient’s parsed headers instead of the raw bytes, so a few wire-level features (--request-target, removing Host, blank headers, --raw, -p tunnels and a separate --connect-timeout) are not available there. MIT licensed, by Jacob Mellor, sponsored by Iron Software. The source for each stage is in src/CurlDotNet/Core/ in the repository.