A lightweight .NET HTTP utility for consuming REST APIs with either a simple static helper API or a fluent request builder.
KGSoft.TinyHttpClient wraps common HttpClient tasks such as sending requests, applying headers, reading response content, deserializing JSON, handling typed responses, logging, and running authentication callbacks.
Targets .NET 10.
Install from NuGet:
dotnet add package KGSoft.TinyHttpClientNuGet package:
https://www.nuget.org/packages/KGSoft.TinyHttpClient/
- Static helper API for quick requests
- Fluent request builder for readable request composition
- Typed responses via
Response<T> - Automatic JSON deserialization
- Raw response body access as both
stringandbyte[] - Query string support with URL encoding
- JSON request body support
- Form-encoded request support
- Custom
HttpContentsupport - Global and per-request header configuration
- Per-request Polly retry support
- Configurable per-request JSON serialization settings
- Reused
HttpClientwith configurable pooled connection lifetime, idle timeout, and request timeout - Logging support
- Pre-request sync/async hooks for token acquisition or refresh
- 401 sync/async callbacks for authentication expiry flows
Use the Helper class when you want concise one-line HTTP calls.
var response = await Helper.GetAsync("https://example.com/api/users/1");
if (response.IsSuccess)
{
Console.WriteLine(response.Message);
}var response = await Helper.GetAsync<User>("https://example.com/api/users/1");
if (response.IsSuccess)
{
User user = response.Result;
}var body = JsonConvert.SerializeObject(new
{
first_name = "Jane",
last_name = "Doe"
});
var response = await Helper.PostAsync("https://example.com/api/users", body);var body = JsonConvert.SerializeObject(new
{
first_name = "Jane",
last_name = "Doe"
});
var response = await Helper.PostAsync<User>("https://example.com/api/users", body);
if (response.IsSuccess)
{
User user = response.Result;
}Use HttpRequestBuilder when you want requests to read naturally and compose options per request.
var response = await new HttpRequestBuilder()
.Get("https://example.com/api/users/1")
.MakeRequestAsync<User>();You can also provide the URI to the builder constructor and call the HTTP verb without a URI:
var response = await new HttpRequestBuilder("https://example.com/api/users/1")
.Get()
.MakeRequestAsync<User>();Or set the URI fluently:
var response = await new HttpRequestBuilder()
.WithUri("https://example.com/api/users/1")
.Get()
.MakeRequestAsync<User>();var response = await new HttpRequestBuilder()
.Get("https://example.com/api/users")
.AddQueryParam("page", "1")
.AddQueryParam("search", "jane doe")
.MakeRequestAsync<UserSearchResult>();var response = await new HttpRequestBuilder()
.Post("https://example.com/api/users")
.WithBody(new
{
first_name = "Jane",
last_name = "Doe"
})
.MakeRequestAsync<User>();var response = await new HttpRequestBuilder("https://example.com/api/users/1")
.Patch()
.WithBody(new
{
first_name = "Jane"
})
.MakeRequestAsync<User>();var headResponse = await new HttpRequestBuilder("https://example.com/api/users/1")
.Head()
.MakeRequestAsync();
var optionsResponse = await new HttpRequestBuilder()
.Options("https://example.com/api/users")
.MakeRequestAsync();var response = await new HttpRequestBuilder()
.Post("https://example.com/oauth/token")
.AddFormParam("grant_type", "client_credentials")
.AddFormParam("client_id", "my-client-id")
.AddFormParam("client_secret", "my-client-secret")
.MakeRequestAsync<TokenResponse>();var response = await new HttpRequestBuilder()
.Get("https://example.com/api/users/1")
.WithHeader("X-Correlation-ID", correlationId)
.WithBearerToken(accessToken)
.MakeRequestAsync<User>();Calling WithHeader for the same header name replaces the previous value.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var response = await new HttpRequestBuilder()
.Get("https://example.com/api/users/1")
.WithCancellationToken(cts.Token)
.MakeRequestAsync<User>();var response = await new HttpRequestBuilder("https://example.com/upload")
.Post()
.WithContent(() => new ByteArrayContent(fileBytes))
.MakeRequestAsync();Prefer the Func<HttpContent> overload when using retry policies, because it creates fresh content for each retry attempt.
var response = await new HttpRequestBuilder("https://example.com/api/users/1")
.Get()
.WithHttpClient(httpClient)
.MakeRequestAsync<User>();When WithHttpClient is not used, requests use the shared pooled client managed by HttpConfig.
var response = await new HttpRequestBuilder("https://example.com/api/users")
.Post()
.WithBody(user)
.WithJsonSerializerSettings(new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
})
.MakeRequestAsync<User>();var retryPolicy = new ResiliencePipelineBuilder<Response>()
.AddRetry(new RetryStrategyOptions<Response>
{
ShouldHandle = new PredicateBuilder<Response>()
.HandleResult(response => !response.IsSuccess),
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1)
})
.Build();
var response = await new HttpRequestBuilder()
.Get("https://example.com/api/users/1")
.WithRetry(retryPolicy)
.MakeRequestAsync<User>();Builders are intended to compose and send one request. Create a new HttpRequestBuilder for each request so headers, body, content, and retry policy state do not carry over unexpectedly.
var response = await new HttpRequestBuilder("https://example.com/api/users/1")
.Get()
.WithLogger(logger)
.MakeRequestAsync<User>();WithLogger accepts either KGSoft.TinyHttpClient.Logging.ILogger or Microsoft.Extensions.Logging.ILogger, including ILogger<T>.
WithLogger logs all request activity by default. Use WithLogScope to limit that request to failed responses:
var response = await new HttpRequestBuilder("https://example.com/api/users/1")
.Get()
.WithLogger(logger)
.WithLogScope(Enums.LogScope.OnlyFailedRequests)
.MakeRequestAsync<User>();Requests return either Response or Response<T>.
public class Response
{
public HttpStatusCode StatusCode { get; set; }
public bool IsSuccess { get; set; }
public string Message { get; set; }
public byte[] Content { get; set; }
}
public class Response<T> : Response
{
public T Result { get; set; }
}Message contains the response body as a string.
Content contains the response body as bytes.
Result contains the deserialized response body when using Response<T>.
HttpConfig can be used to configure defaults shared by requests.
HttpConfig.MediaTypeHeader = Constants.ApplicationJson;
HttpConfig.RequestTimeoutSeconds = 100;
HttpConfig.HttpClientPoolLifetimeMinutes = 5;
HttpConfig.HttpClientPoolIdleMinutes = 2;HttpConfig.DefaultAuthHeader =
new AuthenticationHeaderValue("Bearer", accessToken);HttpConfig.CustomHeaders["X-App-Version"] = "1.0.0";var config = new HeaderConfig
{
AuthHeader = new AuthenticationHeaderValue("Bearer", accessToken),
CustomHeaders = new Dictionary<string, string>
{
{ "X-Correlation-ID", correlationId }
}
};
var response = await Helper.GetAsync<User>(
"https://example.com/api/users/1",
config: config);You can run logic before each request. This is useful for acquiring or refreshing tokens.
HttpConfig.PreRequestAuthAsyncFunc = async () =>
{
var accessToken = await tokenProvider.GetAccessTokenAsync();
HttpConfig.DefaultAuthHeader =
new AuthenticationHeaderValue("Bearer", accessToken);
};The hook runs before request headers are applied, so changes made inside the hook affect the current request.
You can register callbacks that run when a response returns 401 Unauthorized.
HttpConfig.UnauthorizedResultAction = () =>
{
Console.WriteLine("Unauthorized response received.");
};Or use an async callback:
HttpConfig.UnauthorizedResultAsyncFunc = async () =>
{
await authService.RefreshSignInAsync();
};Implement ILogger and assign it to HttpConfig.Logger.
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
HttpConfig.Logger = new ConsoleLogger();
HttpConfig.LogScope = Enums.LogScope.AllRequests;Supported log scopes:
Enums.LogScope.OnlyFailedRequests
Enums.LogScope.AllRequestsKGSoft.TinyHttpClient intentionally keeps the API small. It is useful when you want a lightweight wrapper around HttpClient without adopting a larger REST client framework.
For more examples, see the test project in this repository.