Zwedze.Aetherweave.Http
0.2.5
dotnet add package Zwedze.Aetherweave.Http --version 0.2.5
NuGet\Install-Package Zwedze.Aetherweave.Http -Version 0.2.5
<PackageReference Include="Zwedze.Aetherweave.Http" Version="0.2.5" />
<PackageVersion Include="Zwedze.Aetherweave.Http" Version="0.2.5" />
<PackageReference Include="Zwedze.Aetherweave.Http" />
paket add Zwedze.Aetherweave.Http --version 0.2.5
#r "nuget: Zwedze.Aetherweave.Http, 0.2.5"
#:package Zwedze.Aetherweave.Http@0.2.5
#addin nuget:?package=Zwedze.Aetherweave.Http&version=0.2.5
#tool nuget:?package=Zwedze.Aetherweave.Http&version=0.2.5
Aetherweave.Http
Clean, type-safe HttpClient configuration with built-in profiling, content tracing, and error handling.
Features
- Configuration-Based Setup - Configure HttpClients from appsettings.json with IOptions validation
- Built-in Profiling - Automatic request timing and performance tracking
- Content Tracing - Log response content for debugging
- Error Handling - Custom error handlers for failed HTTP requests
- Chainable API - Fluent builder pattern for adding handlers
- Startup Validation - Configuration validated at application startup
- Type-Safe - Strongly-typed clients with dependency injection
Installation
dotnet add package Zwedze.Aetherweave.Http
Quick Start
1. Define Your HTTP Client Interface
public interface IOrderServiceClient
{
Task<Order> GetOrderAsync(int orderId, CancellationToken ct);
Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct);
}
public sealed class OrderServiceClient(HttpClient httpClient) : IOrderServiceClient
{
public async Task<Order> GetOrderAsync(int orderId, CancellationToken ct)
{
var response = await httpClient.GetAsync($"/api/orders/{orderId}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Order>(ct)
?? throw new InvalidOperationException("Order not found");
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct)
{
var response = await httpClient.PostAsJsonAsync("/api/orders", request, ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Order>(ct)
?? throw new InvalidOperationException("Failed to create order");
}
}
2. Configure in appsettings.json
{
"Aetherweave": {
"HttpClients": {
"OrderService": {
"BaseAddress": "https://api.orders.example.com",
"Timeout": "00:00:30",
"EnableProfiling": true,
"EnableContentTracing": false,
"MaxContentLogSize": 10000
}
}
}
}
3. Register in Program.cs
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService");
4. Use in Your Application
public sealed class OrderController(IOrderServiceClient orderClient) : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id, CancellationToken ct)
{
var order = await orderClient.GetOrderAsync(id, ct);
return Ok(order);
}
}
Configuration Options
HttpClientOptions
| Property | Type | Default | Required | Description |
|---|---|---|---|---|
BaseAddress |
string |
- | ✅ | Base URL for the HTTP client (must be absolute URI) |
Timeout |
TimeSpan |
00:00:30 |
❌ | Request timeout (must be > 0) |
EnableProfiling |
bool |
false |
❌ | Enable request timing and performance logging |
EnableContentTracing |
bool |
false |
❌ | Enable response content logging |
MaxContentLogSize |
int |
10000 |
❌ | Maximum bytes to log (content truncated if larger) |
Multiple Clients Configuration
{
"Aetherweave": {
"HttpClients": {
"OrderService": {
"BaseAddress": "https://api.orders.example.com",
"Timeout": "00:00:30",
"EnableProfiling": true,
"EnableContentTracing": false
},
"PaymentService": {
"BaseAddress": "https://api.payments.example.com",
"Timeout": "00:01:00",
"EnableProfiling": false,
"EnableContentTracing": true,
"MaxContentLogSize": 5000
},
"InventoryService": {
"BaseAddress": "https://api.inventory.example.com",
"Timeout": "00:00:15",
"EnableProfiling": true,
"EnableContentTracing": true
}
}
}
}
// Register all clients
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration, "OrderService");
services.AddAetherweaveHttpClient<IPaymentServiceClient, PaymentServiceClient>(
configuration, "PaymentService");
services.AddAetherweaveHttpClient<IInventoryServiceClient, InventoryServiceClient>(
configuration, "InventoryService");
Built-in Handlers
Profiling Handler
Automatically logs request timing when EnableProfiling is true:
[2025-12-20 10:30:45] HTTP GET https://api.orders.example.com/api/orders/123 completed in 245ms with status 200
Configuration:
{
"OrderService": {
"BaseAddress": "https://api.orders.example.com",
"EnableProfiling": true
}
}
Content Tracing Handler
Logs response content when EnableContentTracing is true:
[2025-12-20 10:30:45] HTTP GET https://api.orders.example.com/api/orders/123 returned 200 (1234 bytes): {"orderId":123,"total":99.99,...}
Configuration:
{
"OrderService": {
"BaseAddress": "https://api.orders.example.com",
"EnableContentTracing": true,
"MaxContentLogSize": 5000
}
}
Security Warning: ⚠️ Never enable EnableContentTracing in production with sensitive data (passwords, credit cards,
etc.)!
Authentication
This package is purely generic HttpClient plumbing — it has no knowledge of any authentication flow.
For auth, use one of the dedicated Aetherweave Security packages, each of which builds on top of
AddAetherweaveHttpClient/WithHandler/WithErrorHandler above:
| Case | Package |
|---|---|
| Protecting your own API by validating incoming JWTs | Zwedze.Aetherweave.Security.Jwt |
| Backend-to-backend — your service calling other APIs as itself (OAuth2 client credentials) | Zwedze.Aetherweave.Security.ClientCredentials |
| Interactive user login for a Blazor WebAssembly UI (Authorization Code + PKCE) | Zwedze.Aetherweave.Security.Oidc |
Advanced Usage
Adding Custom Handlers
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService")
.WithHandler<CorrelationIdHandler>()
.WithHandler<RetryPolicyHandler>();
Custom handler example:
public sealed class CorrelationIdHandler(ICorrelationIdAccessor correlationIdAccessor) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Add("X-Correlation-Id", correlationIdAccessor.CorrelationId);
return await base.SendAsync(request, cancellationToken);
}
}
// Register
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService")
.WithHandler<CorrelationIdHandler>();
Custom Error Handling
public sealed class OrderServiceErrorHandler(ILogger<OrderServiceErrorHandler> logger) : IHttpErrorHandler
{
public async Task HandleError(
HttpRequestMessage request,
HttpResponseMessage response,
HttpStatusCode statusCode)
{
var content = await response.Content.ReadAsStringAsync();
logger.LogError(
"Order service request to {Uri} failed with {StatusCode}: {Content}",
request.RequestUri,
statusCode,
content);
throw statusCode switch
{
HttpStatusCode.NotFound => new OrderNotFoundException(content),
HttpStatusCode.BadRequest => new InvalidOrderException(content),
HttpStatusCode.Unauthorized => new UnauthorizedException(),
_ => new OrderServiceException($"Request failed with status {statusCode}")
};
}
}
// Register
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService")
.WithErrorHandler<OrderServiceErrorHandler>();
Combining Multiple Handlers
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService")
.WithHandler<CorrelationIdHandler>()
.WithHandler<RetryPolicyHandler>()
.WithErrorHandler<OrderServiceErrorHandler>();
Handler execution order:
- ProfilingHandler (starts timer) - built-in, outermost
- ContentTracingHandler (logs response) - built-in
- CorrelationIdHandler (adds header)
- RetryPolicyHandler (retries on failure)
- HttpErrorResponseHandler (custom error handling)
- → Actual HTTP request →
- HttpErrorResponseHandler (processes errors)
- RetryPolicyHandler (retry if needed)
- CorrelationIdHandler
- ContentTracingHandler (logs response content)
- ProfilingHandler (logs timing)
Environment-Specific Configuration
appsettings.Development.json:
{
"Aetherweave": {
"HttpClients": {
"OrderService": {
"BaseAddress": "https://dev-api.orders.example.com",
"Timeout": "00:05:00",
"EnableProfiling": true,
"EnableContentTracing": true,
"MaxContentLogSize": 50000
}
}
}
}
appsettings.Production.json:
{
"Aetherweave": {
"HttpClients": {
"OrderService": {
"BaseAddress": "https://api.orders.example.com",
"Timeout": "00:00:30",
"EnableProfiling": false,
"EnableContentTracing": false
}
}
}
}
Using with Polly for Resilience
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService")
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));
Integration with Aetherweave.Application
Command Handler with HTTP Client
public sealed class CreateOrderHandler(
IOrderServiceClient orderServiceClient,
IUnitOfWorkFactory uowFactory,
IOrderRepository orderRepository) : ICommandHandler<CreateOrderCommand, Guid>
{
public async Task<ResponseWrapper<Guid>> Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
await using var uow = uowFactory.CreateTransactional();
try
{
// Create order via external API
var externalOrder = await orderServiceClient.CreateOrderAsync(
new CreateOrderRequest(request.Items),
cancellationToken);
// Save locally
var order = new Order(
Id<Order>.From(externalOrder.Id),
Code<Order>.From(externalOrder.OrderNumber));
await orderRepository.AddAsync(order, cancellationToken);
await uow.SaveChanges(cancellationToken);
await uow.Commit(cancellationToken);
return ResponseWrapper.Ok(order.Id);
}
catch (OrderServiceException ex)
{
return ResponseWrapper.Fail<Guid>(
ErrorFactory.Create("EXTERNAL_SERVICE_ERROR", ex.Message));
}
}
}
Best Practices
✅ DO
Use interfaces for HTTP clients:
// Good public interface IOrderServiceClient { ... } public sealed class OrderServiceClient : IOrderServiceClient { ... } // Bad public sealed class OrderServiceClient { ... } // No interfaceConfigure separate clients for different services:
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(...); services.AddAetherweaveHttpClient<IPaymentServiceClient, PaymentServiceClient>(...);Use configuration for environment-specific settings:
// appsettings.Development.json { "EnableContentTracing": true } // appsettings.Production.json { "EnableContentTracing": false }Set appropriate timeouts:
{ "Timeout": "00:00:30" // 30 seconds for quick APIs "Timeout": "00:05:00" // 5 minutes for long-running operations }Use custom error handlers for domain-specific errors:
.WithErrorHandler<OrderServiceErrorHandler>()
❌ DON'T
Don't hardcode URLs in client implementations:
// Bad var response = await httpClient.GetAsync("https://hardcoded-url.com/api"); // Good - use BaseAddress from config var response = await httpClient.GetAsync("/api/orders");Don't enable content tracing with sensitive data:
// BAD in production! { "PaymentService": { "EnableContentTracing": true // Could log credit cards! } }Don't use the same client name for different services:
// Bad - both use "ApiClient" services.AddAetherweaveHttpClient<IOrderClient, OrderClient>(config, "ApiClient"); services.AddAetherweaveHttpClient<IPaymentClient, PaymentClient>(config, "ApiClient");Remember to validate configuration at startup:
// Configuration automatically validated with .ValidateOnStart() // Will fail fast if BaseAddress is missing or invalid
Error Handling
ConfigurationNotFoundException
Thrown when the configuration section is not found:
try
{
services.AddAetherweaveHttpClient<IOrderClient, OrderClient>(
configuration,
"NonExistentClient");
}
catch (ConfigurationNotFoundException ex)
{
// Configuration section 'Aetherweave:HttpClients:NonExistentClient' not found.
// Ensure your appsettings.json contains the required configuration.
}
Validation Errors
Configuration validated at startup with detailed error messages:
Unhandled exception. Microsoft.Extensions.Options.OptionsValidationException:
DataAnnotation validation failed for 'HttpClientOptions' members: 'Timeout'
with the error: 'Timeout must be greater than zero'.
Performance Considerations
Profiling Overhead
When EnableProfiling is true:
- Minimal overhead (~1-2ms per request)
- Only measures elapsed time
- Safe for production use
Content Tracing Overhead
When EnableContentTracing is true:
- Significant overhead (reads entire response into memory)
- Doubles memory usage for response
- Not recommended for production
- Use only for debugging/development
Handler Order Optimization
Handlers execute in order of registration:
// Optimal order for performance
services.AddAetherweaveHttpClient<IClient, Client>(config, "Client")
.WithHandler<CacheHandler>() // Check cache first
.WithHandler<CorrelationIdHandler>() // Then tag the request
.WithHandler<RetryPolicyHandler>(); // Retry last
Migration from HttpClientFactory
Before (raw HttpClientFactory):
services.AddHttpClient("OrderService", client =>
{
client.BaseAddress = new Uri("https://api.orders.com");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Usage
public class OrderService(IHttpClientFactory httpClientFactory)
{
public async Task<Order> GetOrderAsync(int id)
{
var client = httpClientFactory.CreateClient("OrderService");
var response = await client.GetAsync($"/api/orders/{id}");
// ...
}
}
After (Aetherweave):
// appsettings.json
{
"Aetherweave": {
"HttpClients": {
"OrderService": {
"BaseAddress": "https://api.orders.com",
"Timeout": "00:00:30"
}
}
}
}
// Program.cs
services.AddAetherweaveHttpClient<IOrderServiceClient, OrderServiceClient>(
configuration,
"OrderService");
// Usage
public sealed class OrderService(IOrderServiceClient orderClient)
{
public async Task<Order> GetOrderAsync(int id)
{
return await orderClient.GetOrderAsync(id);
}
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.10)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- Zwedze.Aetherweave.Core (>= 0.2.5)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Zwedze.Aetherweave.Http:
| Package | Downloads |
|---|---|
|
Zwedze.Aetherweave.Security.Oidc
Manage OIDC integration for api security. |
|
|
Zwedze.Aetherweave.Security.ClientCredentials
Backend-to-backend OAuth2 client-credentials authentication for Aetherweave typed HttpClients. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.5 | 109 | 9/3/2026 |
| 0.2.5-pre.2 | 72 | 9/3/2026 |
| 0.2.5-pre.1 | 77 | 8/13/2026 |
| 0.2.5-extend-db-registratio... | 62 | 8/31/2026 |
| 0.2.4 | 128 | 8/13/2026 |
| 0.2.4-pre.1 | 74 | 8/13/2026 |
| 0.2.3 | 119 | 8/6/2026 |
| 0.2.3-pre.1 | 71 | 8/6/2026 |
| 0.2.2 | 127 | 8/5/2026 |
| 0.2.2-pre.1 | 82 | 7/30/2026 |
| 0.2.1 | 118 | 7/29/2026 |
| 0.2.1-pre.1 | 71 | 7/29/2026 |
| 0.2.0 | 116 | 7/25/2026 |
| 0.1.2-pre.3 | 80 | 7/25/2026 |
| 0.1.2-pre.1 | 62 | 7/24/2026 |
| 0.1.2-http-auth.1 | 55 | 7/23/2026 |
| 0.1.1 | 114 | 6/16/2026 |
| 0.1.0-alpha.12 | 61 | 6/16/2026 |
| 0.0.1-pre.14 | 81 | 6/16/2026 |
| 0.0.1-pre.13 | 66 | 6/16/2026 |