CurlDotNet Usage Guide
This comprehensive guide covers common usage scenarios for CurlDotNet, from basic requests to advanced patterns.
Table of Contents
- Getting Started
- Basic HTTP Operations
- Working with JSON
- Authentication
- File Operations
- Advanced Patterns
- Error Handling
- Performance Optimization
- Testing and Mocking
- Troubleshooting
Getting Started
Installation
dotnet add package CurlDotNet
Basic Setup
using CurlDotNet;
using CurlDotNet.Core;
// Simplest usage - direct curl command
var result = await Curl.ExecuteAsync("curl https://api.example.com");
// Using convenience methods
var result = await Curl.GetAsync("https://api.example.com");
Basic HTTP Operations
GET Requests
// Simple GET
var result = await Curl.GetAsync("https://api.example.com/users");
// GET with query parameters
var result = await Curl.GetAsync("https://api.example.com/users?page=1&limit=10");
// GET with headers
var result = await CurlRequestBuilder.Get("https://api.example.com/users")
.WithHeader("Accept", "application/json")
.WithHeader("X-API-Version", "2.0")
.ExecuteAsync();
// The same as a curl command
var same = await Curl.ExecuteAsync(
"curl -H 'Accept: application/json' -H 'X-API-Version: 2.0' https://api.example.com/users");
Every request carries User-Agent: curl/8.7.1 and Accept: */* unless you set your own, exactly like curl.
POST Requests
// POST with JSON body: serialize an object...
var result = await Curl.PostJsonAsync("https://api.example.com/users",
new { name = "John Doe", email = "john@example.com" });
// ...or send JSON text with curl's --json flag (sets Content-Type and Accept)
var result2 = await Curl.ExecuteAsync(
@"curl --json '{""name"":""John Doe"",""email"":""john@example.com""}' https://api.example.com/users");
// POST with form data (application/x-www-form-urlencoded)
var result3 = await Curl.PostAsync("https://api.example.com/login", "username=john&password=secret");
// POST with custom headers
var result4 = await CurlRequestBuilder.Post("https://api.example.com/users")
.WithJson(new { name = "John Doe" })
.WithHeader("X-Request-ID", Guid.NewGuid().ToString())
.ExecuteAsync();
PUT Requests
// PUT to update resource
var result = await CurlRequestBuilder.Put("https://api.example.com/users/123")
.WithJson(new { name = "Jane Doe", email = "jane@example.com" })
.ExecuteAsync();
// PUT a file's bytes, as curl -T does (the file name is appended when the URL ends in /)
var upload = await Curl.ExecuteAsync(
"curl -T document.pdf -H 'Content-Type: application/pdf' https://api.example.com/files/");
DELETE Requests
// Simple DELETE
var result = await Curl.ExecuteAsync("curl -X DELETE https://api.example.com/users/123");
// DELETE with confirmation header
var result2 = await CurlRequestBuilder.Delete("https://api.example.com/users/123")
.WithHeader("X-Confirm-Delete", "true")
.ExecuteAsync();
PATCH Requests
// PATCH for partial updates
var patchData = "[{\"op\":\"replace\",\"path\":\"/email\",\"value\":\"new@example.com\"}]";
var result = await CurlRequestBuilder.Patch("https://api.example.com/users/123")
.WithData(patchData)
.WithHeader("Content-Type", "application/json-patch+json")
.ExecuteAsync();
Working with JSON
Parsing JSON Responses
using System.Text.Json;
// Basic JSON parsing
var result = await Curl.GetAsync("https://api.example.com/user/123");
var json = JsonDocument.Parse(result.Body);
var userName = json.RootElement.GetProperty("name").GetString();
// Deserialize to strongly-typed objects
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var result2 = await Curl.GetAsync("https://api.example.com/user/123");
var user = result2.ParseJson<User>(); // case-insensitive: "name" binds to Name
ParseJson<T>() matches property names case-insensitively (camelCase JSON binds to PascalCase properties); snake_case keys still need [JsonPropertyName]. It throws InvalidOperationException for an empty or malformed body.
Sending JSON Data
// Using anonymous objects
var data = new
{
name = "John Doe",
email = "john@example.com",
age = 30
};
var result = await Curl.PostJsonAsync("https://api.example.com/users", data);
// Using strongly-typed objects
var user = new User { Name = "Jane Doe", Email = "jane@example.com" };
var result2 = await Curl.PostJsonAsync("https://api.example.com/users", user);
Working with JSON Arrays
// Parse JSON array
var result = await Curl.GetAsync("https://api.example.com/users");
var users = result.ParseJson<List<User>>();
foreach (var user in users)
{
Console.WriteLine($"{user.Name} - {user.Email}");
}
// Send JSON array
var newUsers = new[]
{
new User { Name = "User1", Email = "user1@example.com" },
new User { Name = "User2", Email = "user2@example.com" }
};
var bulk = await Curl.PostJsonAsync("https://api.example.com/users/bulk", newUsers);
Authentication
Basic Authentication
// Using curl command syntax
var result = await Curl.ExecuteAsync("curl -u username:password https://api.example.com/secure");
// Using builder
var result2 = await CurlRequestBuilder.Get("https://api.example.com/secure")
.WithBasicAuth("username", "password")
.ExecuteAsync();
// Using Authorization header
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("username:password"));
var result3 = await CurlRequestBuilder.Get("https://api.example.com/secure")
.WithHeader("Authorization", $"Basic {credentials}")
.ExecuteAsync();
// Digest, NTLM and --anyauth are negotiated in-library, like curl
var digest = await Curl.ExecuteAsync("curl --digest -u username:password https://api.example.com/secure");
// Credentials from ~/.netrc instead of the command line
var netrc = await Curl.ExecuteAsync("curl -n https://api.example.com/secure");
A wrong password comes back as a normal result with StatusCode == 401. Credentials are not forwarded when a redirect goes to another host, unless you pass --location-trusted.
Bearer Token Authentication
// Using the builder
var result = await CurlRequestBuilder.Get("https://api.example.com/secure")
.WithBearerToken("your-token-here")
.ExecuteAsync();
// Using curl's --oauth2-bearer flag
var result2 = await Curl.ExecuteAsync("curl --oauth2-bearer your-token-here https://api.example.com/secure");
// Reusable authenticated client
public class AuthenticatedApiClient
{
private readonly string _token;
public AuthenticatedApiClient(string token)
{
_token = token;
}
public async Task<CurlResult> GetAsync(string endpoint)
{
return await CurlRequestBuilder.Get($"https://api.example.com{endpoint}")
.WithBearerToken(_token)
.ExecuteAsync();
}
}
API Key Authentication
// API key in header
var result = await CurlRequestBuilder.Get("https://api.example.com/data")
.WithHeader("X-API-Key", "your-api-key")
.ExecuteAsync();
// API key in query parameter
var result2 = await Curl.GetAsync("https://api.example.com/data?api_key=your-api-key");
OAuth 2.0 Flow
// Step 1: Get access token (token endpoints take a form-encoded POST)
var tokenResult = await Curl.ExecuteAsync(@"curl https://auth.example.com/token \
-d grant_type=client_credentials \
-d client_id=your-client-id \
-d client_secret=your-client-secret");
using var tokenResponse = JsonDocument.Parse(tokenResult.EnsureSuccess().Body);
var accessToken = tokenResponse.RootElement.GetProperty("access_token").GetString();
// Step 2: Use access token
var result = await CurlRequestBuilder.Get("https://api.example.com/protected")
.WithBearerToken(accessToken)
.ExecuteAsync();
File Operations
Downloading Files
// Download to memory: binary content is in BinaryData, never decoded as text
var result = await Curl.GetAsync("https://example.com/file.pdf");
if (result.IsBinary)
File.WriteAllBytes("downloaded.pdf", result.BinaryData); // or result.SaveToFile("downloaded.pdf")
// Download straight to disk, as curl -o does (Body stays empty)
var saved = await Curl.ExecuteAsync("curl -o largefile.zip https://example.com/largefile.zip");
// Resume an interrupted download from the size of the existing file
var resumed = await Curl.ExecuteAsync("curl -C - -o largefile.zip https://example.com/largefile.zip");
For a progress display, pass a callback with CurlSettings.WithProgress. It is called once per network read of an http/https body with the percentage (-1 when the length is unknown), the bytes received and the total; throwing from it aborts the download with CurlAbortedByCallbackException (exit code 42). -C - resume, --retry, --limit-rate and --speed-limit are the other tools for long downloads.
var settings = new CurlSettings()
.WithProgress((percent, received, total) =>
Console.Write(percent < 0 ? $"\r{received:N0} bytes" : $"\r{percent:F0}%"));
var big = await Curl.ExecuteAsync("curl -o largefile.zip https://example.com/largefile.zip", settings);
Uploading Files
// Upload file as binary: --data-binary sends the bytes unchanged
var result = await Curl.ExecuteAsync(
"curl --data-binary @document.pdf -H 'Content-Type: application/pdf' https://api.example.com/upload");
// The builder equivalent
var fileBytes = File.ReadAllBytes("document.pdf");
var result2 = await CurlRequestBuilder.Post("https://api.example.com/upload")
.WithBinaryData(fileBytes)
.WithHeader("Content-Type", "application/pdf")
.ExecuteAsync();
// Multipart form upload: curl builds the boundary, Content-Disposition and file Content-Type
var result3 = await Curl.ExecuteAsync(
"curl -F 'name=John Doe' -F 'file=@document.pdf' https://api.example.com/upload");
A missing upload file throws CurlReadErrorException (curl exit code 26) before anything is sent.
Working with Streams
// The response is fully buffered; ToStream() wraps it for stream-based APIs
var result = await Curl.GetAsync("https://api.example.com/stream");
using var stream = result.ToStream();
using var reader = new StreamReader(stream);
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
Console.WriteLine($"Received: {line}");
}
Advanced Patterns
Retry Logic
curl’s --retry is built in: it retries timeouts and HTTP 408/429/500/502/503/504 with back-off that doubles from 1 second, honours Retry-After, and stops at --retry-max-time. CurlSettings.WithRetries is the .NET shortcut and retries any failure. See curl –retry in C#.
var result = await Curl.ExecuteAsync("curl --retry 3 --retry-max-time 60 https://api.example.com/data");
var result2 = await Curl.ExecuteAsync("curl https://api.example.com/data",
new CurlSettings().WithRetries(count: 3, delayMs: 1000));
When you need a custom policy, write the loop yourself:
public async Task<CurlResult> ExecuteWithRetry(string url, int maxRetries = 3)
{
for (int i = 0; i <= maxRetries; i++)
{
try
{
var result = await Curl.GetAsync(url);
if (result.IsSuccess)
return result;
if (i < maxRetries && IsRetryableStatusCode(result.StatusCode))
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); // Exponential backoff
continue;
}
return result;
}
catch (CurlTimeoutException) when (i < maxRetries)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
throw new Exception($"Failed after {maxRetries} retries");
}
private bool IsRetryableStatusCode(int statusCode)
{
return statusCode == 429 || statusCode >= 500;
}
Rate Limiting
public class RateLimitedClient
{
private readonly SemaphoreSlim _semaphore;
private readonly TimeSpan _resetInterval;
private DateTime _nextReset;
public RateLimitedClient(int requestsPerInterval, TimeSpan interval)
{
_semaphore = new SemaphoreSlim(requestsPerInterval);
_resetInterval = interval;
_nextReset = DateTime.UtcNow.Add(interval);
}
public async Task<CurlResult> ExecuteAsync(string url)
{
await _semaphore.WaitAsync();
try
{
if (DateTime.UtcNow > _nextReset)
{
_nextReset = DateTime.UtcNow.Add(_resetInterval);
_semaphore.Release(Math.Min(_semaphore.CurrentCount + 1, 10));
}
return await Curl.GetAsync(url);
}
finally
{
_ = Task.Delay(_resetInterval).ContinueWith(_ => _semaphore.Release());
}
}
}
Parallel Requests
// Execute multiple requests in parallel
var urls = new[]
{
"https://api.example.com/data1",
"https://api.example.com/data2",
"https://api.example.com/data3"
};
var tasks = urls.Select(url => Curl.GetAsync(url));
var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
Console.WriteLine($"Status: {result.StatusCode}, Body length: {result.Body.Length}");
}
// Parallel with cancellation: a cancelled token throws OperationCanceledException
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var cancellable = urls.Select(url => Curl.ExecuteAsync($"curl {url}", cts.Token));
var cancellableResults = await Task.WhenAll(cancellable);
Several URLs inside one command (curl URL1 URL2 or a glob such as data[1-3]) also all run, but sequentially, as curl does; see multiple URLs and globbing.
Connection Pooling
The static Curl API shares one internal HttpClient, so connections are pooled and reused across calls automatically; there is nothing to create or dispose. Commands with their own transport options (a proxy, -k, --resolve and similar) get a dedicated client configured for them. For a reusable client with shared defaults, use LibCurl:
using CurlDotNet.Lib;
using var api = new LibCurl()
.WithBearerToken(token)
.WithTimeout(TimeSpan.FromSeconds(30));
var data = await api.GetAsync("https://api.example.com/data");
Error Handling
Basic Error Handling
using CurlDotNet.Exceptions;
try
{
// -f makes HTTP 400+ throw CurlHttpReturnedErrorException (curl exit code 22)
var result = await Curl.ExecuteAsync("curl -f -m 30 https://api.example.com/data");
// Process successful result
}
catch (CurlHttpReturnedErrorException httpEx)
{
Console.WriteLine($"HTTP Error {httpEx.StatusCode}: {httpEx.Message}");
if (httpEx.IsRateLimited)
{
var retryAfter = httpEx.GetRetryAfter();
Console.WriteLine($"Rate limited. Retry after: {retryAfter}");
}
}
catch (CurlOperationTimeoutException timeoutEx)
{
Console.WriteLine($"Request timed out: {timeoutEx.Message}"); // -m expired, exit code 28
}
catch (CurlException curlEx)
{
Console.WriteLine($"Curl error: {curlEx.Message}");
foreach (var suggestion in curlEx.Suggestions)
{
Console.WriteLine($" - {suggestion}");
}
}
Custom Error Handling
public class ApiErrorHandler
{
public async Task<T> ExecuteWithHandling<T>(
Func<Task<CurlResult>> request,
Func<CurlResult, T> processResult)
{
try
{
var result = await request();
if (!result.IsSuccess)
{
throw result.StatusCode switch
{
401 => new UnauthorizedException("Authentication required"),
403 => new ForbiddenException("Access denied"),
404 => new NotFoundException("Resource not found"),
429 => new RateLimitException("Too many requests"),
>= 500 => new ServerException("Server error"),
_ => new ApiException($"Request failed: {result.StatusCode}")
};
}
return processResult(result);
}
catch (CurlException ex)
{
throw new ApiException("Network error", ex);
}
}
}
Performance Optimization
Caching Responses
public class CachedApiClient
{
private readonly MemoryCache _cache;
public CachedApiClient()
{
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 100
});
}
public async Task<string> GetCachedAsync(string url)
{
if (_cache.TryGetValue(url, out string cached))
{
return cached;
}
var result = await Curl.GetAsync(url);
if (result.IsSuccess)
{
_cache.Set(url, result.Body, new MemoryCacheEntryOptions
{
Size = 1,
SlidingExpiration = TimeSpan.FromMinutes(5)
});
}
return result.Body;
}
}
Request Compression
// Ask for a compressed response and let CurlDotNet decode it (gzip, deflate, br)
var result = await Curl.ExecuteAsync("curl --compressed https://api.example.com/data");
// result.Body is decoded; result.Headers keeps Content-Encoding exactly as the server sent it
// Send a gzip-compressed request body yourself
var compressed = await CurlRequestBuilder.Post("https://api.example.com/data")
.WithBinaryData(CompressString(jsonData))
.WithHeader("Content-Type", "application/json")
.WithHeader("Content-Encoding", "gzip")
.ExecuteAsync();
private byte[] CompressString(string text)
{
var bytes = Encoding.UTF8.GetBytes(text);
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Optimal))
{
gzip.Write(bytes, 0, bytes.Length);
}
return output.ToArray();
}
Testing and Mocking
Unit Testing with CurlDotNet
[TestClass]
public class ApiClientTests
{
[TestMethod]
public async Task GetUser_ReturnsValidUser()
{
// Arrange
var expectedUser = new User { Id = 1, Name = "Test User" };
// Act
var result = await Curl.GetAsync("https://jsonplaceholder.typicode.com/users/1");
var user = result.ParseJson<User>(); // case-insensitive, so "id" binds to Id
// Assert
Assert.IsNotNull(user);
Assert.AreEqual(1, user.Id);
Assert.IsFalse(string.IsNullOrEmpty(user.Name));
}
}
Integration Testing
[TestClass]
[TestCategory("Integration")]
public class ApiIntegrationTests
{
private string _baseUrl = "https://api.example.com";
[TestMethod]
public async Task CreateAndDeleteUser_Workflow()
{
// Create user
var createResult = await Curl.PostJsonAsync($"{_baseUrl}/users", new { name = "Test User" });
Assert.AreEqual(201, createResult.StatusCode);
var userId = JsonDocument.Parse(createResult.Body)
.RootElement.GetProperty("id").GetInt32();
// Verify user exists
var getResult = await Curl.GetAsync($"{_baseUrl}/users/{userId}");
Assert.AreEqual(200, getResult.StatusCode);
// Delete user
var deleteResult = await Curl.ExecuteAsync($"curl -X DELETE {_baseUrl}/users/{userId}");
Assert.AreEqual(204, deleteResult.StatusCode);
// Verify user deleted
var verifyResult = await Curl.GetAsync($"{_baseUrl}/users/{userId}");
Assert.AreEqual(404, verifyResult.StatusCode);
}
}
Troubleshooting
Common Issues and Solutions
Connection Timeouts
// There is no timeout unless you set one; set a generous one for slow endpoints
var result = await CurlRequestBuilder.Get("https://slow-api.example.com/data")
.WithConnectTimeout(TimeSpan.FromSeconds(10))
.WithTimeout(TimeSpan.FromMinutes(5))
.ExecuteAsync();
// Same as: curl --connect-timeout 10 -m 300 https://slow-api.example.com/data
SSL Certificate Issues
// Disable SSL verification (development only!)
var result = await CurlRequestBuilder.Get("https://self-signed.example.com")
.Insecure()
.ExecuteAsync();
// Same as: curl -k https://self-signed.example.com
On .NET 5 and later --cacert/--capath trust a private CA, --cert/--key send a client certificate and --pinnedpubkey pins the server key; on .NET Standard 2.0 and .NET Framework those options are listed in CurlOptions.UnsupportedOptions and certificates are checked against the operating system’s trust store. See SSL certificate errors.
Proxy Configuration
// Use proxy server
var result = await CurlRequestBuilder.Get("https://api.example.com/data")
.WithProxy("http://proxy.company.com:8080", "username", "password")
.ExecuteAsync();
// Same as: curl -x http://proxy.company.com:8080 -U username:password https://api.example.com/data
The http_proxy, https_proxy, all_proxy and NO_PROXY environment variables are honoured as in curl.
Debugging Requests
// Enable verbose output for debugging: the > / < log is at the start of result.Body
var result = await CurlRequestBuilder.Get("https://api.example.com/data")
.Verbose()
.ExecuteAsync();
// Full wire dump to a file, and curl's write-out summary
var traced = await Curl.ExecuteAsync(
"curl --trace-ascii trace.txt -w '\\n%{http_code} %{time_total}s' https://api.example.com/data");
See curl -w in C# for every write-out variable.
Logging
public class LoggingApiClient
{
private readonly ILogger<LoggingApiClient> _logger;
public LoggingApiClient(ILogger<LoggingApiClient> logger)
{
_logger = logger;
}
public async Task<CurlResult> ExecuteAsync(string url)
{
_logger.LogInformation("Executing request to {Url}", url);
var stopwatch = Stopwatch.StartNew();
try
{
var result = await Curl.GetAsync(url);
_logger.LogInformation(
"Request completed: {StatusCode} in {ElapsedMs}ms",
result.StatusCode,
stopwatch.ElapsedMilliseconds
);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex,
"Request failed after {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds
);
throw;
}
}
}
Best Practices
- Always dispose resources: Use
usingstatements or implementIDisposable - Handle errors gracefully: Catch specific exception types
- Use timeouts: There is no default timeout, so set
-m(orCurl.DefaultMaxTimeSeconds) to prevent hanging requests - Validate inputs: Check URLs and parameters before sending
- Log requests: Track API usage and errors
- Use connection pooling: Reuse HTTP connections
- Retry transient failures:
--retry NorCurlSettings.WithRetries - Respect rate limits: Implement throttling
- Secure credentials: Never hardcode secrets
- Test thoroughly: Unit and integration tests