SimpleHttpClient 5.1.0
dotnet add package SimpleHttpClient --version 5.1.0
NuGet\Install-Package SimpleHttpClient -Version 5.1.0
<PackageReference Include="SimpleHttpClient" Version="5.1.0" />
<PackageVersion Include="SimpleHttpClient" Version="5.1.0" />
<PackageReference Include="SimpleHttpClient" />
paket add SimpleHttpClient --version 5.1.0
#r "nuget: SimpleHttpClient, 5.1.0"
#:package SimpleHttpClient@5.1.0
#addin nuget:?package=SimpleHttpClient&version=5.1.0
#tool nuget:?package=SimpleHttpClient&version=5.1.0
SimpleHttpClient
An easy-to-use .NET wrapper for HttpClient. No extension methods, included interfaces for easy unit test mocking, and straightforward properties for easier debugging (the response body is available as a string, byte array, and/or a typed object). It also supports streaming responses, with built-in helpers for reading line-delimited streams and Server-Sent Events.
Contents
Installation
SimpleHttpClient is available on NuGet and can installed through the NuGet Package Manager or by running
nuget install SimpleHttpClient
The package targets netstandard2.0 (for .NET Framework and older runtimes) and net8.0. On modern runtimes it uses SocketsHttpHandler with a pooled connection lifetime to keep DNS fresh; on netstandard2.0 it periodically rotates the underlying HttpClient to achieve the same.
Basic Usage
With Dependency Injection
SimpleHttpClient is designed to be used with dependency injection in order to avoid pitfalls that come with using an HttpClient:
In Program.cs:
// Register SimpleHttpClient with the ServiceCollection
await Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSimpleHttpClient();
})
.Build()
.RunAsync();
Then, inject ISimpleClientFactory and create a client with the host you want to call. This is the
preferred approach: each consumer gets its own client, so there's no shared, mutable Host to
collide over.
public class YourClientClass
{
private readonly ISimpleClient client;
// Retrieve an ISimpleClientFactory through dependency injection
public YourClientClass(ISimpleClientFactory clientFactory)
{
// Create a client for the host you'll be calling
client = clientFactory.CreateClient("https://api.sampleapis.com");
}
public async Task<string> MakeRequest()
{
// Pass the path you want to call into the SimpleRequest constructor
var request = new SimpleRequest("/coffee/hot");
// Call MakeRequest on the client, passing your request, and get your response back
var response = await client.MakeRequest(request);
return response.StringBody;
}
}
You can also inject ISimpleClient directly and set its Host (it's registered as transient, so
each consumer gets its own instance):
public YourClientClass(ISimpleClient client)
{
client.Host = "https://api.sampleapis.com";
}
Without Dependency Injection
If you're using SimpleHttpClient without dependency injection, you can just create an instance of SimpleClient:
public class YourClientClass
{
private readonly SimpleClient client;
public YourClientClass()
{
// Pass the host you'll be calling into the SimpleClient constructor
client = new SimpleClient("https://api.sampleapis.com");
}
public async Task<string> MakeRequest()
{
// Pass the path you want to call into the SimpleRequest constructor
var request = new SimpleRequest("/coffee/hot");
// Call MakeRequest on the client, passing your request, and get your response back
var response = await client.MakeRequest(request);
return response.StringBody;
}
}
NOTE: Although SimpleClient implements IDisposable, it should NOT be created inside a using block, but instead should be disposed with the class that uses it.
Typed Responses
You can also call MakeRequest with a type to deserialize the response body into that type:
public async Task<SomeResponseObject> MakeRequest()
{
// Pass the path you want to call into the SimpleRequest constructor
var request = new SimpleRequest("/get");
// Call MakeRequest<T> on the client, passing your request, and get your response back
var response = await client.MakeRequest<SomeResponseObject>(request);
return response.Body;
}
The untyped StringBody and ByteBody are still available on a typed response. If deserialization fails, response.Body will be null and the thrown exception is available on response.SerializationException.
Requests
A SimpleRequest defaults to a GET. Pass an HttpMethod to change it:
var request = new SimpleRequest("/post", HttpMethod.Post);
Query String Parameters
Query string parameters can be added directly to the path, via the QueryStringParameters dictionary, or both. Values in QueryStringParameters take precedence over duplicates in the path:
var request = new SimpleRequest("/get?param1=value1");
request.QueryStringParameters.Add("param2", "value2");
Form URL Encoded Parameters
Add application/x-www-form-urlencoded parameters via the FormUrlEncodedParameters dictionary. When present, these take precedence over any body set on the request:
var request = new SimpleRequest("/post", HttpMethod.Post);
request.FormUrlEncodedParameters.Add("param1", "value1");
request.FormUrlEncodedParameters.Add("param2", "value2");
Headers
Headers set on the request are merged with the client's DefaultHeaders (request headers win on conflicts):
// Sent with every request made by this client
client.DefaultHeaders["Authorization"] = "Bearer <token>";
// Sent with just this request
var request = new SimpleRequest("/get");
request.Headers["X-Custom-Header"] = "value";
Request Bodies
Pass an object as the body and it will be serialized using the client's serializer (JSON by default):
var request = new SimpleRequest("/post", HttpMethod.Post, new
{
param1 = "value1",
param2 = "value2",
});
Alternatively, set request.StringBody to send a pre-serialized string body. You can control the content type and encoding via request.ContentType and request.ContentEncoding.
After a request is sent, the request reflects what was actually sent: for an object Body, request.StringBody holds the serialized payload, and request.ContentType holds the resolved content type — handy for logging and debugging. An object Body is the source of truth and is re-serialized on every send (so changing Body and re-sending the same request sends the new value); a string body is sent as-is.
Note: On .NET Framework, sending a
GETrequest with a body isn't supported — itsHttpClientis backed byHttpWebRequest, which disallows it — and SimpleClient surfaces this as aNotSupportedExceptionwith an explanatory message. This works fine on modern runtimes (net8.0+); if you need to target .NET Framework, send the body with aPOST/PUT/etc. instead.
Streaming Responses
For responses you want to consume as they arrive — for example Server-Sent Events (SSE) or large downloads — use MakeStreamRequest. Unlike MakeRequest, it does not buffer the body into memory; it returns the live network stream as soon as the response headers are available.
var request = new SimpleRequest("/stream");
// The response holds the connection open, so dispose it when you're done (a using block is ideal).
using var response = await client.MakeStreamRequest(request);
if (!response.IsSuccessful)
{
// response.StatusCode and response.Headers are available immediately
}
// response.Body is the raw, unbuffered network stream
using var reader = new StreamReader(response.Body);
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
// Process each line as it arrives
Console.WriteLine(line);
}
A few things to keep in mind:
- Dispose the response. The underlying
HttpResponseMessageand connection are held open until you dispose the returnedISimpleStreamResponse. Ausingblock is the simplest way to guarantee this. MakeStreamRequestaccepts aCancellationToken. Pass one to cancel sending the request, waiting for the headers, and reading the stream (e.g. when a user aborts mid-stream). The token is observed by reads too, even through aStreamReaderthat gives you no place to pass it — so theawait reader.ReadLineAsync()loop above stops promptly when the token fires. Async reads honor it even mid-read; synchronous reads observe it between reads, so to abort a synchronous read already blocked on the socket, dispose the response. A caller-requested cancellation surfaces as anOperationCanceledException; a timeout still surfaces as aTimeoutException.- The body is yours to frame.
SimpleStreamResponse.Bodyis a plainStream. For common cases there are helpers (below) that read it as lines or Server-Sent Events; otherwise you can frame it however you like.
Reading lines and Server-Sent Events
ISimpleStreamResponse has two methods that cover the most common streaming formats. Both are IAsyncEnumerable<T>, so you consume them with await foreach, and both honor a CancellationToken:
// Line-delimited streams (NDJSON, plain text, etc.)
await foreach (var line in response.ReadLinesAsync(cancellationToken))
{
Console.WriteLine(line);
}
// Server-Sent Events (text/event-stream)
await foreach (var sse in response.ReadServerSentEventsAsync(cancellationToken))
{
// sse.Data, sse.EventType, sse.Id, sse.Retry
Console.WriteLine(sse.Data);
}
ReadServerSentEventsAsync parses the SSE wire format per the WHATWG specification: events are separated by blank lines, multiple data: lines are joined with newlines, and comment/keep-alive lines (starting with :) are skipped. It deliberately does not handle application-specific conventions — most notably it does not treat any sentinel value specially and does not deserialize the payload — so those stay in your hands:
using var response = await client.MakeStreamRequest(request, cancellationToken);
await foreach (var sse in response.ReadServerSentEventsAsync(cancellationToken))
{
if (sse.Data == "end") // a sentinel some APIs send to mark the end - your convention, not the library's
{
break;
}
var chunk = client.Serializer.Deserialize<MyChunk>(sse.Data);
// ...handle chunk
}
This keeps SimpleHttpClient general-purpose: the SSE framing is a web standard (the same format EventSource consumes in browsers), while which sentinel terminates the stream and how each data payload is shaped are specific to the API you're calling.
Configuration
Timeouts
The client Timeout defaults to 30 seconds and can be overridden per-request. Set the value to -1 to disable the timeout. A request that exceeds its timeout throws a TimeoutException:
client.Timeout = 60; // 60 seconds for all requests on this client
var request = new SimpleRequest("/slow");
request.TimeoutOverride = 120; // 120 seconds for just this request
For streaming requests, the timeout applies to receiving the response headers — not to how long you spend reading the stream.
Additional Successful Status Codes
By default, IsSuccessful is true for any 2xx status code. You can mark additional status codes as successful on the client (applies to all requests) and/or per-request:
client.AdditionalSuccessfulStatusCodes.Add(HttpStatusCode.NotFound);
var request = new SimpleRequest("/get");
request.AdditionalSuccessfulStatusCodes.Add(HttpStatusCode.NotAcceptable);
Custom Serializers
SimpleHttpClient ships with JSON (default) and XML serializers, and uses the serializer to both serialize request bodies and deserialize typed responses. Set one on the client, or override it per-request:
client.Serializer = new SimpleHttpDefaultXmlSerializer();
var request = new SimpleRequest("/get");
request.SerializerOverride = new SimpleHttpDefaultJsonSerializer();
You can supply your own serializer by implementing ISimpleHttpSerializer.
JSON serialization
The default JSON serializer (SimpleHttpDefaultJsonSerializer) is backed by System.Text.Json. It serializes with camelCase names, omits null values, writes indented output, and deserializes case-insensitively. For smoother interop it also reads numbers from JSON strings (e.g. "123") and tolerates trailing commas and comments while reading. The equivalent SimpleHttpSystemTextJsonSerializer is also available for callers who reference it explicitly.
Note: the defaults above cover the most common cases, but deserialization is strict in two ways they don't soften:
- Non-public parameterless constructors aren't used — add a public constructor or a
[JsonConstructor].- Wrong-shape values aren't coerced — a field that's sometimes a string and sometimes an object (and similar) will throw. For such fields, attach a custom
JsonConverterto the property.If you need different behavior wholesale, implement
ISimpleHttpSerializerwith your own serializer and set it on the client.
Logging
You can log requests and responses by setting the LogRequest and LogResponse delegates (called immediately before a request is sent and immediately after a response is received), or by providing an ISimpleHttpLogger:
client.LogRequest = (url, request) => Console.WriteLine($"--> {request.Method} {url}");
client.LogResponse = (response) => Console.WriteLine($"<-- {response.StatusCode}");
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Http (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.1)
- System.Text.Json (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 5.1.0 | 50 | 5/31/2026 | |
| 5.0.0 | 57 | 5/30/2026 | |
| 4.2.0 | 71 | 5/30/2026 | |
| 4.1.0 | 3,674 | 4/18/2024 | |
| 4.0.0 | 292 | 4/25/2023 | |
| 3.0.0 | 267 | 4/23/2023 | |
| 2.1.1 | 805 | 4/17/2023 | |
| 2.1.0 | 262 | 4/17/2023 | |
| 2.0.0 | 294 | 4/16/2023 | |
| 1.1.0 | 283 | 4/16/2023 | |
| 1.0.10 | 359 | 2/28/2023 | |
| 1.0.9 | 350 | 2/17/2023 | |
| 1.0.8 | 343 | 2/17/2023 | |
| 1.0.7 | 333 | 2/17/2023 | |
| 1.0.6 | 341 | 2/17/2023 | |
| 1.0.5 | 343 | 2/17/2023 | |
| 1.0.4 | 350 | 2/17/2023 | |
| 1.0.3 | 349 | 2/17/2023 | |
| 1.0.2 | 344 | 2/16/2023 | |
| 1.0.1 | 338 | 2/16/2023 |