Code Examples
Complete, working examples for common scenarios
Basic GET Request
Fetch data from a REST API with type-safe error handling.
using System.Net.Http.Json;
using RestClient.Net;
using Urls;
// Define models
record Post(int UserId, int Id, string Title, string Body);
record ApiError(string Message);
// Type aliases for clean pattern matching
using OkPost = Outcome.Result<Post, Outcome.HttpError<ApiError>>
.Ok<Post, Outcome.HttpError<ApiError>>;
using ErrorPost = Outcome.Result<Post, Outcome.HttpError<ApiError>>
.Error<Post, Outcome.HttpError<ApiError>>;
using ResponseErrorPost = Outcome.HttpError<ApiError>.ErrorResponseError;
using ExceptionErrorPost = Outcome.HttpError<ApiError>.ExceptionError;
// Make the request
using var httpClient = new HttpClient();
var result = await httpClient.GetAsync(
url: "https://jsonplaceholder.typicode.com/posts/1".ToAbsoluteUrl(),
deserializeSuccess: async (content, ct) =>
await content.ReadFromJsonAsync<Post>(ct) ?? throw new Exception("Null"),
deserializeError: async (content, ct) =>
await content.ReadFromJsonAsync<ApiError>(ct) ?? new ApiError("Unknown")
);
// Handle all cases
var message = result switch
{
OkPost(var post) => $"Title: {post.Title}",
ErrorPost(ResponseErrorPost(var err, var status, _)) => $"Error {status}: {err.Message}",
ErrorPost(ExceptionErrorPost(var ex)) => $"Exception: {ex.Message}",
};
Console.WriteLine(message);
POST Request with Body
Create a new resource with a JSON request body.
record CreatePostRequest(string Title, string Body, int UserId);
var newPost = new CreatePostRequest("My Title", "My content", 1);
var result = await httpClient.PostAsync(
url: "https://jsonplaceholder.typicode.com/posts".ToAbsoluteUrl(),
body: newPost,
serializeRequest: body => JsonContent.Create(body),
deserializeSuccess: async (content, ct) =>
await content.ReadFromJsonAsync<Post>(ct) ?? throw new Exception("Null"),
deserializeError: async (content, ct) =>
await content.ReadFromJsonAsync<ApiError>(ct) ?? new ApiError("Unknown")
);
var message = result switch
{
OkPost(var post) => $"Created post with ID: {post.Id}",
ErrorPost(ResponseErrorPost(var err, var status, _)) => $"Failed: {status}",
ErrorPost(ExceptionErrorPost(var ex)) => $"Exception: {ex.Message}",
};
Using IHttpClientFactory
Proper HttpClient usage in ASP.NET Core applications.
// Program.cs - Register the client
builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(30);
});
// UserService.cs - Use the client
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
);
}
}
Retry Policy with Polly
Add automatic retries for transient failures.
// 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
);
});
Authentication Handler
Automatically add authentication tokens to requests.
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);
var response = await base.SendAsync(request, cancellationToken);
// Refresh token if expired
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
token = await tokenService.RefreshTokenAsync(cancellationToken);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
response = await base.SendAsync(request, cancellationToken);
}
return response;
}
}
// Program.cs
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("api")
.AddHttpMessageHandler<AuthenticationHandler>();
Status Code Specific Handling
Handle different HTTP status codes differently.
var result = await httpClient.GetUserAsync(userId);
var message = result switch
{
OkUser(var user) => $"Found: {user.Name}",
// Not Found - user doesn't exist
ErrorUser(ResponseErrorUser(_, HttpStatusCode.NotFound, _)) =>
"User not found. Please check the ID.",
// Unauthorized - need to log in
ErrorUser(ResponseErrorUser(_, HttpStatusCode.Unauthorized, _)) =>
"Please log in to view this user.",
// Forbidden - not allowed
ErrorUser(ResponseErrorUser(_, HttpStatusCode.Forbidden, _)) =>
"You don't have permission to view this user.",
// Rate limited
ErrorUser(ResponseErrorUser(_, HttpStatusCode.TooManyRequests, var response)) =>
{
var retryAfter = response.Headers.RetryAfter?.Delta;
return $"Too many requests. Try again in {retryAfter?.TotalSeconds ?? 60} seconds.";
},
// Server error
ErrorUser(ResponseErrorUser(var err, var status, _)) when (int)status >= 500 =>
"The server is experiencing issues. Please try again later.",
// Other API errors
ErrorUser(ResponseErrorUser(var err, var status, _)) =>
$"API Error {(int)status}: {err.Message}",
// Network/timeout errors
ErrorUser(ExceptionErrorUser(TaskCanceledException ex))
when ex.CancellationToken.IsCancellationRequested =>
"Request was cancelled.",
ErrorUser(ExceptionErrorUser(TaskCanceledException)) =>
"Request timed out. Please try again.",
ErrorUser(ExceptionErrorUser(HttpRequestException)) =>
"Network error. Please check your connection.",
ErrorUser(ExceptionErrorUser(var ex)) =>
$"Unexpected error: {ex.Message}",
};
Chaining Multiple Requests
Chain dependent API calls with proper error propagation.
// Get user, then get their orders, then get order details
public async Task<Result<OrderDetails, HttpError<ApiError>>> GetUserOrderDetailsAsync(
string userId,
CancellationToken ct)
{
// First, get the user
var userResult = await httpClient.GetUserAsync(userId, ct);
return await userResult switch
{
OkUser(var user) => await GetOrdersForUserAsync(user, ct),
ErrorUser(var error) => new Result<OrderDetails, HttpError<ApiError>>
.Error(error),
};
}
private async Task<Result<OrderDetails, HttpError<ApiError>>> GetOrdersForUserAsync(
User user,
CancellationToken ct)
{
var ordersResult = await httpClient.GetOrdersAsync(user.Id, ct);
return ordersResult switch
{
OkOrders(var orders) => new Result<OrderDetails, HttpError<ApiError>>
.Ok(new OrderDetails(user, orders)),
ErrorOrders(var error) => new Result<OrderDetails, HttpError<ApiError>>
.Error(error),
};
}
Parallel Requests
Make multiple independent requests in parallel.
public async Task<Dashboard> GetDashboardAsync(string userId, CancellationToken ct)
{
// Start all requests in parallel
var userTask = httpClient.GetUserAsync(userId, ct);
var ordersTask = httpClient.GetOrdersAsync(userId, ct);
var notificationsTask = httpClient.GetNotificationsAsync(userId, ct);
// Wait for all to complete
await Task.WhenAll(userTask, ordersTask, notificationsTask);
var userResult = await userTask;
var ordersResult = await ordersTask;
var notificationsResult = await notificationsTask;
// Combine results
return (userResult, ordersResult, notificationsResult) switch
{
(OkUser(var user), OkOrders(var orders), OkNotifications(var notifications)) =>
new Dashboard(user, orders, notifications),
(ErrorUser(var e), _, _) =>
throw new Exception($"Failed to load user: {e}"),
(_, ErrorOrders(var e), _) =>
throw new Exception($"Failed to load orders: {e}"),
(_, _, ErrorNotifications(var e)) =>
throw new Exception($"Failed to load notifications: {e}"),
};
}