Flurl
Fluent and user-friendly URL building and HTTP client library for .NET. Features intuitive API through method chaining, excellent testability, JSON integration, and error handling capabilities. HttpClient-based supporting modern async communication patterns.
GitHub Overview
tmenier/Flurl
Fluent URL builder and testable HTTP client for .NET
Topics
Star History
Library
Flurl
Overview
Flurl is "a fluent URL builder and testable HTTP client for .NET" developed as a modern HTTP library gaining attention in the .NET ecosystem. With the concept of "fluent and chainable syntax," it provides an intuitive API that integrates URL building and HTTP communication. Comprehensively supporting features necessary for web API integration such as JSON processing, authentication, testing capabilities, and query parameter handling, it has established itself as a next-generation HTTP client that works across diverse platforms through .NET Standard compliance.
Details
Flurl 2025 edition has established a solid position as an innovative library proposing a new paradigm for .NET HTTP clients. Unlike traditional HttpClient, its fluent API design enables writing a series of processes from URL construction to HTTP request execution with intuitive method chaining. Supporting a wide range of platforms including .NET Framework, .NET Core, Xamarin, and UWP, it achieves high productivity in modern .NET development. The design specialized for testing capabilities, making HTTP communication mocking extremely easy in unit tests, is a major distinguishing feature not found in other libraries.
Key Features
- Fluent API Design: Readable and chainable method chaining syntax
- Integrated URL Building: Unified builder pattern for URLs and HTTP requests
- Comprehensive JSON Processing: Automatic serialization and deserialization
- Excellent Testing Capabilities: Design specialized for mocking and testing
- Multi-platform Support: Wide environment support through .NET Standard compliance
- Flexible Authentication System: Easy implementation of OAuth, Bearer Token, and custom authentication
Pros and Cons
Pros
- Improved code readability and maintainability through fluent API syntax
- Intuitive development experience with integrated URL building and HTTP requests
- Extremely easy HTTP communication mocking in unit tests
- Significantly improved development efficiency through automated JSON processing
- Wide platform support through .NET Standard compliance
- Rich HTTP methods and header customization features
Cons
- Learning cost for developers unfamiliar with fluent APIs
- Debate over string extension method usage (though optional)
- Visibility challenges due to dependency hiding in large projects
- Functional limitations compared to HttpClient (some low-level controls)
- Need for careful consideration in enterprise adoption due to being relatively new
- Flexibility limitations when requiring complex HTTP configurations
Reference Pages
Code Examples
Installation and Basic Setup
# Install Flurl HTTP client (includes URL building features)
Install-Package Flurl.Http
# For URL building only
Install-Package Flurl
# Using .NET CLI
dotnet add package Flurl.Http
# Verification in Package Manager Console
Get-Package Flurl.Http
Basic HTTP Requests (GET/POST/PUT/DELETE)
using Flurl;
using Flurl.Http;
// Basic GET request
var users = await "https://api.example.com/users"
.GetJsonAsync<List<User>>();
Console.WriteLine($"Retrieved users count: {users.Count}");
// GET request with query parameters
var filteredUsers = await "https://api.example.com/users"
.SetQueryParams(new {
page = 1,
limit = 10,
sort = "created_at",
status = "active"
})
.GetJsonAsync<ApiResponse<User>>();
// Dynamic URL construction and GET request
var userId = 123;
var userDetail = await "https://api.example.com"
.AppendPathSegment("users")
.AppendPathSegment(userId)
.SetQueryParam("include", "profile,settings")
.GetJsonAsync<UserDetail>();
Console.WriteLine($"User name: {userDetail.Name}");
// POST request (sending JSON)
var newUser = new User
{
Name = "John Doe",
Email = "[email protected]",
Age = 30,
Department = "Development"
};
var createdUser = await "https://api.example.com/users"
.WithHeader("Authorization", "Bearer your-jwt-token")
.PostJsonAsync(newUser)
.ReceiveJson<User>();
Console.WriteLine($"Created user ID: {createdUser.Id}");
// PUT request (data update)
var updatedData = new { Name = "Jane Doe", Email = "[email protected]" };
var updatedUser = await $"https://api.example.com/users/{userId}"
.WithOAuthBearerToken("your-oauth-token")
.PutJsonAsync(updatedData)
.ReceiveJson<User>();
// DELETE request
var deleteResponse = await $"https://api.example.com/users/{userId}"
.WithHeader("Authorization", "Bearer your-token")
.DeleteAsync();
if (deleteResponse.IsSuccessStatusCode)
{
Console.WriteLine("User deleted successfully");
}
// Form data submission
var loginResult = await "https://api.example.com/login"
.PostUrlEncodedAsync(new {
username = "testuser",
password = "secret123",
remember_me = true
})
.ReceiveJson<LoginResponse>();
// File upload
var uploadResult = await "https://api.example.com/upload"
.WithOAuthBearerToken("your-token")
.PostMultipartAsync(mp => {
mp.AddFile("file", "/path/to/document.pdf", "application/pdf");
mp.AddString("category", "documents");
mp.AddString("public", "false");
})
.ReceiveJson<UploadResponse>();
Console.WriteLine($"Upload completed: {uploadResult.FileId}");
Advanced Configuration and Customization (Headers, Authentication, Timeout, etc.)
using Flurl;
using Flurl.Http;
using Flurl.Http.Configuration;
// Custom header configuration
var response = await "https://api.example.com/data"
.WithHeaders(new {
User_Agent = "MyApp/1.0 (Flurl Client)",
Accept = "application/json",
Accept_Language = "en-US,ja-JP",
X_API_Version = "v2",
X_Request_ID = "req-12345"
})
.GetJsonAsync<ApiData>();
// Authentication configuration (multiple patterns)
// Bearer Token authentication
var bearerData = await "https://api.example.com/protected"
.WithOAuthBearerToken("your-jwt-token")
.GetJsonAsync<ProtectedData>();
// Basic authentication
var basicAuthData = await "https://api.example.com/private"
.WithBasicAuth("username", "password")
.GetJsonAsync<PrivateData>();
// Custom header authentication
var apiKeyData = await "https://api.example.com/secure"
.WithHeader("X-API-Key", "your-api-key")
.WithHeader("X-Client-Id", "client-123")
.GetJsonAsync<SecureData>();
// Timeout configuration
try
{
var slowData = await "https://api.example.com/slow-endpoint"
.WithTimeout(TimeSpan.FromSeconds(30)) // 30-second timeout
.GetJsonAsync<SlowData>();
}
catch (FlurlHttpTimeoutException)
{
Console.WriteLine("Request timed out");
}
// Cookie usage
var session = await "https://api.example.com/login"
.WithCookies(out var cookies) // Create cookie container
.PostJsonAsync(new { username = "user", password = "pass" })
.ReceiveJson<LoginResult>();
// Subsequent requests with the same cookie session
var userData = await "https://api.example.com/user-data"
.WithCookies(cookies) // Use previous cookies
.GetJsonAsync<UserData>();
// SSL configuration and client certificates
var secureClient = new FlurlClient("https://secure-api.example.com")
.Configure(settings => {
settings.HttpClientFactory = new DefaultHttpClientFactory();
// Client certificate configuration is done through HttpClientHandler
});
var secureData = await secureClient
.Request("data")
.GetJsonAsync<SecureApiData>();
// Proxy configuration
FlurlHttp.Configure(settings => {
settings.HttpClientFactory = new DefaultHttpClientFactory();
// Proxy configuration is done through HttpClientHandler
});
// Global configuration customization
FlurlHttp.Configure(settings => {
settings.Timeout = TimeSpan.FromSeconds(60); // Default timeout
settings.AllowedHttpStatusRange = "200-299,404"; // Allowed status range
});
// Client-specific configuration
var apiClient = new FlurlClient("https://api.example.com")
.Configure(settings => {
settings.Timeout = TimeSpan.FromSeconds(30);
settings.BeforeCall = call => {
Console.WriteLine($"Call started: {call.Request.Url}");
};
settings.AfterCall = call => {
Console.WriteLine($"Call completed: {call.Response.StatusCode}");
};
});
// Rate limiting support
var rateLimitedData = await "https://api.example.com/rate-limited"
.WithHeader("X-RateLimit-User", "user123")
.OnError(call => {
if (call.Response?.StatusCode == 429) // Too Many Requests
{
var retryAfter = call.Response.Headers.FirstOrDefault("Retry-After");
Console.WriteLine($"Rate limited: retry after {retryAfter} seconds");
}
})
.GetJsonAsync<RateLimitedData>();
Error Handling and Retry Functionality
using Flurl;
using Flurl.Http;
using Polly;
using Polly.Extensions.Http;
// Comprehensive error handling
async Task<T> SafeApiCall<T>(string url) where T : class
{
try
{
return await url
.WithTimeout(30)
.GetJsonAsync<T>();
}
catch (FlurlHttpTimeoutException)
{
Console.WriteLine("Timeout error: Request did not complete within time limit");
return null;
}
catch (FlurlHttpException ex)
{
Console.WriteLine($"HTTP error: {ex.StatusCode}");
if (ex.StatusCode == 401)
{
Console.WriteLine("Authentication error: Please check your token");
}
else if (ex.StatusCode == 403)
{
Console.WriteLine("Permission error: Access denied");
}
else if (ex.StatusCode == 404)
{
Console.WriteLine("Not found: Resource does not exist");
}
else if (ex.StatusCode == 429)
{
Console.WriteLine("Rate limit: Please wait before retrying");
}
else if (ex.StatusCode >= 500)
{
Console.WriteLine("Server error: Problem on server side");
var errorDetails = await ex.GetResponseStringAsync();
Console.WriteLine($"Error details: {errorDetails}");
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
return null;
}
}
// Usage example
var userData = await SafeApiCall<UserData>("https://api.example.com/users/123");
if (userData != null)
{
Console.WriteLine($"User name: {userData.Name}");
}
// Flurl's built-in error handling
var result = await "https://api.example.com/data"
.OnError(call => {
Console.WriteLine($"Error occurred: {call.Exception.Message}");
Console.WriteLine($"URL: {call.Request.Url}");
Console.WriteLine($"Status: {call.Response?.StatusCode}");
// Detailed error logging
LogApiError(call);
})
.OnErrorAsync(async call => {
// Asynchronous error handling
await LogApiErrorAsync(call);
})
.GetJsonAsync<ApiData>();
// Advanced retry functionality using Polly library
private static readonly HttpPolicyWrap<HttpResponseMessage> RetryPolicy =
Policy.WrapAsync(
// Retry with exponential backoff
HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2, 4, 8 seconds
onRetry: (outcome, timespan, retryCount, context) => {
Console.WriteLine($"Retry {retryCount}: after {timespan} seconds");
}),
// Circuit breaker
HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) => {
Console.WriteLine($"Circuit breaker opened: for {duration} seconds");
},
onReset: () => {
Console.WriteLine("Circuit breaker reset");
})
);
// HTTP client configuration using Polly policy
FlurlHttp.Configure(settings => {
settings.HttpClientFactory = new PollyHttpClientFactory(RetryPolicy);
});
// Manual retry implementation
async Task<T> RetryApiCall<T>(string url, int maxRetries = 3) where T : class
{
var delay = TimeSpan.FromSeconds(1);
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
return await url.GetJsonAsync<T>();
}
catch (FlurlHttpException ex) when (
ex.StatusCode >= 500 ||
ex.StatusCode == 429 ||
ex.StatusCode == 408) // Retryable status codes
{
if (attempt == maxRetries)
{
Console.WriteLine($"Maximum attempts reached: {ex.StatusCode}");
throw;
}
Console.WriteLine($"Attempt {attempt} failed. Retrying in {delay.TotalSeconds} seconds...");
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff
}
}
return null;
}
// Detailed status code handling
var response = await "https://api.example.com/status-check"
.AllowHttpStatus("200-299,404") // Treat 404 as success too
.GetAsync();
switch (response.StatusCode)
{
case 200:
var data = await response.GetJsonAsync<ApiData>();
Console.WriteLine($"Success: {data}");
break;
case 401:
Console.WriteLine("Authentication error: New token required");
await RefreshAuthToken();
break;
case 403:
Console.WriteLine("Permission error: Contact administrator");
break;
case 404:
Console.WriteLine("Resource not found (normal case)");
break;
case 429:
var retryAfter = response.Headers.FirstOrDefault("Retry-After");
Console.WriteLine($"Rate limited: wait {retryAfter} seconds");
break;
default:
Console.WriteLine($"Unexpected status: {response.StatusCode}");
break;
}
// Custom exception handling class
public class ApiClient
{
private readonly string _baseUrl;
private readonly string _apiKey;
public ApiClient(string baseUrl, string apiKey)
{
_baseUrl = baseUrl;
_apiKey = apiKey;
}
public async Task<T> GetAsync<T>(string endpoint) where T : class
{
try
{
return await _baseUrl
.AppendPathSegment(endpoint)
.WithHeader("X-API-Key", _apiKey)
.WithTimeout(30)
.GetJsonAsync<T>();
}
catch (FlurlHttpException ex)
{
await HandleApiException(ex);
throw;
}
}
private async Task HandleApiException(FlurlHttpException ex)
{
var errorResponse = await ex.GetResponseStringAsync();
// Logging
Console.WriteLine($"API Error: {ex.StatusCode} - {errorResponse}");
// Notify external monitoring system
await NotifyMonitoringSystem(ex);
}
private async Task NotifyMonitoringSystem(FlurlHttpException ex)
{
// Implementation for notifying monitoring system
await Task.CompletedTask;
}
}
Concurrent Processing and Performance Optimization
using Flurl;
using Flurl.Http;
using System.Collections.Concurrent;
// Parallel fetching of multiple URLs
async Task<List<T>> FetchMultipleEndpoints<T>(IEnumerable<string> endpoints) where T : class
{
var semaphore = new SemaphoreSlim(5, 5); // Maximum 5 parallel
var results = new ConcurrentBag<T>();
var tasks = endpoints.Select(async endpoint => {
await semaphore.WaitAsync();
try
{
var data = await endpoint.GetJsonAsync<T>();
results.Add(data);
}
catch (Exception ex)
{
Console.WriteLine($"Error {endpoint}: {ex.Message}");
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
return results.ToList();
}
// Usage example
var endpoints = new[]
{
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments",
"https://api.example.com/categories"
};
var allData = await FetchMultipleEndpoints<ApiData>(endpoints);
Console.WriteLine($"Retrieved data count: {allData.Count}");
// Pagination-aware complete data fetching
async Task<List<T>> FetchAllPages<T>(string baseUrl, string accessToken) where T : class
{
var allItems = new List<T>();
var page = 1;
const int pageSize = 100;
while (true)
{
try
{
var response = await baseUrl
.SetQueryParams(new { page, per_page = pageSize })
.WithOAuthBearerToken(accessToken)
.GetJsonAsync<PagedResponse<T>>();
if (response.Items == null || !response.Items.Any())
break;
allItems.AddRange(response.Items);
Console.WriteLine($"Page {page} retrieved: {response.Items.Count} items");
if (!response.HasMore || response.Items.Count < pageSize)
break;
page++;
// API load reduction
await Task.Delay(100);
}
catch (Exception ex)
{
Console.WriteLine($"Error on page {page}: {ex.Message}");
break;
}
}
Console.WriteLine($"Total items retrieved: {allItems.Count}");
return allItems;
}
// Efficient batch processing
async Task<BatchResult<T>> ProcessBatch<T>(IEnumerable<BatchRequest> requests) where T : class
{
var results = new ConcurrentDictionary<string, T>();
var errors = new ConcurrentDictionary<string, Exception>();
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
};
await Parallel.ForEachAsync(requests, parallelOptions, async (request, ct) => {
try
{
var result = await request.Url
.WithHeaders(request.Headers)
.WithTimeout(request.Timeout ?? TimeSpan.FromSeconds(30))
.GetJsonAsync<T>(cancellationToken: ct);
results.TryAdd(request.Id, result);
}
catch (Exception ex)
{
errors.TryAdd(request.Id, ex);
}
});
return new BatchResult<T>
{
Successful = results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Failed = errors.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
TotalProcessed = requests.Count()
};
}
// Cached client with caching functionality
public class CachedApiClient
{
private readonly MemoryCache _cache;
private readonly string _baseUrl;
public CachedApiClient(string baseUrl)
{
_baseUrl = baseUrl;
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = 100
});
}
public async Task<T> GetWithCache<T>(string endpoint, TimeSpan? cacheExpiry = null) where T : class
{
var cacheKey = $"{_baseUrl}/{endpoint}";
if (_cache.TryGetValue(cacheKey, out T cachedResult))
{
Console.WriteLine($"Cache hit: {cacheKey}");
return cachedResult;
}
var result = await _baseUrl
.AppendPathSegment(endpoint)
.GetJsonAsync<T>();
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiry ?? TimeSpan.FromMinutes(5),
Size = 1
};
_cache.Set(cacheKey, result, cacheOptions);
Console.WriteLine($"Cache added: {cacheKey}");
return result;
}
}
// Streaming processing (for large files)
async Task DownloadLargeFile(string fileUrl, string localPath, IProgress<long> progress = null)
{
using var response = await fileUrl
.WithTimeout(TimeSpan.FromMinutes(30))
.GetStreamAsync();
using var fileStream = File.Create(localPath);
var buffer = new byte[8192];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await response.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
Console.WriteLine($"Download completed: {localPath} ({totalBytesRead:N0} bytes)");
}
// Download with progress usage example
var progress = new Progress<long>(bytesDownloaded => {
Console.WriteLine($"Download progress: {bytesDownloaded:N0} bytes");
});
await DownloadLargeFile(
"https://api.example.com/files/large-dataset.zip",
"/tmp/dataset.zip",
progress
);
Framework Integration and Test Implementation
using Flurl;
using Flurl.Http;
using Flurl.Http.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Xunit;
// API service for ASP.NET Core integration
public interface IApiService
{
Task<T> GetAsync<T>(string endpoint) where T : class;
Task<T> PostAsync<T>(string endpoint, object data) where T : class;
}
public class FlurlApiService : IApiService
{
private readonly IFlurlClient _client;
public FlurlApiService(IFlurlClient client)
{
_client = client;
}
public async Task<T> GetAsync<T>(string endpoint) where T : class
{
return await _client
.Request(endpoint)
.GetJsonAsync<T>();
}
public async Task<T> PostAsync<T>(string endpoint, object data) where T : class
{
return await _client
.Request(endpoint)
.PostJsonAsync(data)
.ReceiveJson<T>();
}
}
// DI configuration in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register Flurl client
services.AddSingleton<IFlurlClient>(sp =>
new FlurlClient("https://api.example.com")
.Configure(settings => {
settings.Timeout = TimeSpan.FromSeconds(30);
settings.BeforeCall = call => {
var logger = sp.GetService<ILogger<FlurlApiService>>();
logger?.LogInformation($"API call: {call.Request.Url}");
};
})
);
services.AddScoped<IApiService, FlurlApiService>();
}
// Unit test implementation
public class ApiServiceTests
{
[Fact]
public async Task GetUsers_ShouldReturnUserList()
{
// Arrange
using var httpTest = new HttpTest();
var expectedUsers = new[]
{
new User { Id = 1, Name = "John Doe" },
new User { Id = 2, Name = "Jane Smith" }
};
httpTest.RespondWithJson(expectedUsers);
var apiService = new FlurlApiService(
new FlurlClient("https://api.example.com")
);
// Act
var users = await apiService.GetAsync<User[]>("users");
// Assert
Assert.Equal(2, users.Length);
Assert.Equal("John Doe", users[0].Name);
// Verify HTTP call
httpTest.ShouldHaveCalled("https://api.example.com/users")
.WithVerb(HttpMethod.Get)
.Times(1);
}
[Fact]
public async Task CreateUser_WithValidData_ShouldReturnCreatedUser()
{
// Arrange
using var httpTest = new HttpTest();
var newUser = new { Name = "Mike Johnson", Email = "[email protected]" };
var createdUser = new User { Id = 3, Name = "Mike Johnson", Email = "[email protected]" };
httpTest.RespondWithJson(createdUser, 201);
var apiService = new FlurlApiService(
new FlurlClient("https://api.example.com")
);
// Act
var result = await apiService.PostAsync<User>("users", newUser);
// Assert
Assert.Equal(3, result.Id);
Assert.Equal("Mike Johnson", result.Name);
// Verify request content
httpTest.ShouldHaveCalled("https://api.example.com/users")
.WithVerb(HttpMethod.Post)
.WithContentType("application/json")
.WithRequestJson(newUser);
}
[Fact]
public async Task GetUser_WhenNotFound_ShouldThrowException()
{
// Arrange
using var httpTest = new HttpTest();
httpTest.RespondWith("User not found", 404);
var apiService = new FlurlApiService(
new FlurlClient("https://api.example.com")
);
// Act & Assert
var exception = await Assert.ThrowsAsync<FlurlHttpException>(
() => apiService.GetAsync<User>("users/999")
);
Assert.Equal(404, exception.StatusCode);
}
[Fact]
public async Task ApiCall_WithTimeout_ShouldThrowTimeoutException()
{
// Arrange
using var httpTest = new HttpTest();
httpTest.SimulateTimeout();
var apiService = new FlurlApiService(
new FlurlClient("https://api.example.com")
.Configure(settings => settings.Timeout = TimeSpan.FromMilliseconds(100))
);
// Act & Assert
await Assert.ThrowsAsync<FlurlHttpTimeoutException>(
() => apiService.GetAsync<User[]>("users")
);
}
}
// Integration test example
public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public ApiIntegrationTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Fact]
public async Task GetWeatherForecast_ShouldReturnForecast()
{
// Arrange
var client = _factory.CreateClient();
var baseUrl = client.BaseAddress.ToString();
// Act
var forecast = await baseUrl
.AppendPathSegment("WeatherForecast")
.GetJsonAsync<WeatherForecast[]>();
// Assert
Assert.NotEmpty(forecast);
Assert.All(forecast, f => Assert.InRange(f.TemperatureC, -20, 55));
}
}
// Test helpers for mocking
public static class FlurlTestHelpers
{
public static HttpTest SetupMockApi(string baseUrl)
{
var httpTest = new HttpTest();
// Configure common response headers
httpTest.WithSettings(settings => {
settings.FakeHttpMethod = true;
settings.BeforeCall = call => {
call.Response.Headers.Add("X-Test-Mode", "true");
};
});
return httpTest;
}
public static void MockSuccessResponse<T>(this HttpTest httpTest, T data, int statusCode = 200)
{
httpTest.RespondWithJson(data, statusCode);
}
public static void MockErrorResponse(this HttpTest httpTest, string message, int statusCode)
{
httpTest.RespondWith(message, statusCode);
}
}
// Test helper usage example
[Fact]
public async Task TestWithHelper()
{
using var httpTest = FlurlTestHelpers.SetupMockApi("https://api.example.com");
httpTest.MockSuccessResponse(new { Message = "Success" });
var result = await "https://api.example.com/test"
.GetJsonAsync<dynamic>();
Assert.Equal("Success", result.Message);
}