Advanced Usage
This guide covers advanced patterns for production use of RestClient.Net.
Using IHttpClientFactory
Always use IHttpClientFactory in production to avoid socket exhaustion:
// Program.cs
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
// In your service
public class UserService(IHttpClientFactory httpClientFactory)
{
public async Task<Result<User, HttpError<ApiError>>> GetUserAsync(
string userId,
CancellationToken ct = default)
{
var client = httpClientFactory.CreateClient("api");
return await client.GetAsync(
url: $"/users/{userId}".ToAbsoluteUrl(),
deserializeSuccess: Deserializers.Json<User>,
deserializeError: Deserializers.Error,
cancellationToken: ct
);
}
}
Typed Clients
Create strongly-typed clients for better organization:
// UserApiClient.cs
public class UserApiClient(HttpClient httpClient)
{
public Task<Result<User, HttpError<ApiError>>> GetUserAsync(
string userId,
CancellationToken ct = default) =>
httpClient.GetAsync(
url: $"/users/{userId}".ToAbsoluteUrl(),
deserializeSuccess: Deserializers.Json<User>,
deserializeError: Deserializers.Error,
cancellationToken: ct
);
public Task<Result<User, HttpError<ApiError>>> CreateUserAsync(
CreateUserRequest request,
CancellationToken ct = default) =>
httpClient.PostAsync(
url: "/users".ToAbsoluteUrl(),
body: request,
serializeRequest: body => JsonContent.Create(body),
deserializeSuccess: Deserializers.Json<User>,
deserializeError: Deserializers.Error,
cancellationToken: ct
);
}
// Program.cs
builder.Services.AddHttpClient<UserApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
Retry Policies with Polly
Add retry logic for transient failures:
using Microsoft.Extensions.Http.Resilience;
// Program.cs
builder.Services.AddHttpClient("api")
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromMilliseconds(500);
options.Retry.UseJitter = true;
options.Retry.ShouldHandle = args => ValueTask.FromResult(
args.Outcome.Exception is not null ||
args.Outcome.Result?.StatusCode >= HttpStatusCode.InternalServerError
);
});
Custom Retry Strategy
For more control:
builder.Services.AddHttpClient("api")
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: (retryAttempt, response, context) =>
{
// Check for Retry-After header
if (response.Result?.Headers.RetryAfter?.Delta is { } delta)
{
return delta;
}
// Exponential backoff
return TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
},
onRetryAsync: (outcome, timespan, retryAttempt, context) =>
{
Console.WriteLine($"Retry {retryAttempt} after {timespan}");
return Task.CompletedTask;
}
)
);
Authentication Handler
Create a delegating handler for authentication:
public class AuthenticationHandler(ITokenService tokenService) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await tokenService.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return await base.SendAsync(request, cancellationToken);
}
}
// Program.cs
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("api")
.AddHttpMessageHandler<AuthenticationHandler>();
Token Refresh Handler
Automatically refresh expired tokens:
public class TokenRefreshHandler(
ITokenService tokenService,
ILogger<TokenRefreshHandler> logger) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var token = await tokenService.GetAccessTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
logger.LogInformation("Token expired, refreshing...");
token = await tokenService.RefreshTokenAsync(cancellationToken);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
}
Request/Response Logging
Log all HTTP traffic:
public class LoggingHandler(ILogger<LoggingHandler> logger) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var requestId = Guid.NewGuid().ToString("N")[..8];
logger.LogInformation(
"[{RequestId}] {Method} {Uri}",
requestId,
request.Method,
request.RequestUri
);
var stopwatch = Stopwatch.StartNew();
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
logger.LogInformation(
"[{RequestId}] {StatusCode} in {ElapsedMs}ms",
requestId,
(int)response.StatusCode,
stopwatch.ElapsedMilliseconds
);
return response;
}
}
Circuit Breaker
Prevent cascading failures:
builder.Services.AddHttpClient("api")
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (result, duration) =>
{
Console.WriteLine($"Circuit broken for {duration}");
},
onReset: () =>
{
Console.WriteLine("Circuit reset");
}
)
);
Custom Serialization
Use different serializers:
System.Text.Json with Options
public static class Deserializers
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
public static async Task<T> Json<T>(HttpContent content, CancellationToken ct)
where T : class =>
await content.ReadFromJsonAsync<T>(Options, ct)
?? throw new InvalidOperationException($"Failed to deserialize {typeof(T).Name}");
}
Newtonsoft.Json
public static async Task<T> NewtonsoftJson<T>(HttpContent content, CancellationToken ct)
{
var json = await content.ReadAsStringAsync(ct);
return JsonConvert.DeserializeObject<T>(json)
?? throw new InvalidOperationException($"Failed to deserialize {typeof(T).Name}");
}
Caching Responses
Cache successful responses:
public class CachingService(
IMemoryCache cache,
IHttpClientFactory httpClientFactory)
{
public async Task<Result<User, HttpError<ApiError>>> GetUserAsync(
string userId,
CancellationToken ct = default)
{
var cacheKey = $"user:{userId}";
if (cache.TryGetValue<User>(cacheKey, out var cached))
{
return new Result<User, HttpError<ApiError>>.Ok(cached);
}
var client = httpClientFactory.CreateClient("api");
var result = await client.GetAsync(
url: $"/users/{userId}".ToAbsoluteUrl(),
deserializeSuccess: Deserializers.Json<User>,
deserializeError: Deserializers.Error,
cancellationToken: ct
);
if (result is Result<User, HttpError<ApiError>>.Ok(var user))
{
cache.Set(cacheKey, user, TimeSpan.FromMinutes(5));
}
return result;
}
}
Request Correlation
Add correlation IDs for distributed tracing:
public class CorrelationHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var correlationId = httpContextAccessor.HttpContext?
.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
request.Headers.Add("X-Correlation-ID", correlationId);
return base.SendAsync(request, cancellationToken);
}
}
Testing
Mock HttpClient
public class UserServiceTests
{
[Fact]
public async Task GetUser_ReturnsUser_WhenFound()
{
// Arrange
var handler = new MockHttpMessageHandler(request =>
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = JsonContent.Create(new User("1", "John"))
});
var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.example.com")
};
var service = new UserService(client);
// Act
var result = await service.GetUserAsync("1");
// Assert
Assert.IsType<OkUser>(result);
}
}
Best Practices Summary
- Always use IHttpClientFactory in production
- Configure timeouts explicitly
- Add retry policies for transient errors
- Use circuit breakers to prevent cascading failures
- Implement authentication handlers for token management
- Log requests and responses for debugging
- Add correlation IDs for distributed tracing
- Cache expensive operations when appropriate
- Test with mock handlers for reliability
Next Steps
- OpenAPI Generator - Generate clients automatically
- MCP Server - Claude Code integration
- API Reference - Complete documentation