Fluent REST client with circuit breaker, retry policies, certificate support, and object pooling. Provides resilient HTTP communication with configurable error handling and response mapping.
$ dotnet add package Myth.RestA modern, fluent REST client for .NET with enterprise-grade resilience patterns. Built for developers who value clean code, type safety, and minimal boilerplate.
dotnet add package Myth.Restvar response = await Rest
.Create()
.Configure(config => config
.WithBaseUrl("https://api.github.com")
.WithHeader("User-Agent", "Myth.Rest"))
.DoGet("users/octocat")
.OnResult(result => result.UseTypeForSuccess<User>())
.OnError(error => error.ThrowForNonSuccess())
.BuildAsync();
var user = response.GetAs<User>();var newUser = new CreateUserRequest {
Name = "John Doe",
Email = "john@example.com"
};
var response = await Rest
.Create()
.Configure(config => config
.WithBaseUrl("https://api.example.com")
.WithBearerAuthorization(token))
.DoPost("users", newUser)
.OnResult(result => result
.UseTypeFor<User>(HttpStatusCode.Created)
.UseTypeFor<ValidationError>(HttpStatusCode.BadRequest))
.OnError(error => error.ThrowForNonSuccess())
.BuildAsync();
var user = response.GetAs<User>();var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRest(config => config
.WithBaseUrl("https://api.example.com")
.WithBearerAuthorization(builder.Configuration["ApiToken"])
.WithRetry());
var app = builder.Build();public class UserService
{
private readonly IRestRequest _restClient;
public UserService(IRestRequest restClient)
{
_restClient = restClient;
}
public async Task<User> GetUserAsync(int id, CancellationToken cancellationToken = default)
{
var response = await _restClient
.DoGet($"users/{id}")
.OnResult(r => r.UseTypeForSuccess<User>())
.OnError(e => e.ThrowForNonSuccess())
.BuildAsync(cancellationToken);
return response.GetAs<User>();
}
}builder.Services.AddRestFactory()
.AddRestConfiguration("github", config => config
.WithBaseUrl("https://api.github.com")
.WithHeader("User-Agent", "MyApp"))
.AddRestConfiguration("stripe", config => config
.WithBaseUrl("https://api.stripe.com")
.WithBearerAuthorization(stripeKey));public class MultiApiService
{
private readonly IRestFactory _factory;
public MultiApiService(IRestFactory factory)
{
_factory = factory;
}
public async Task<Repository> GetGitHubRepoAsync(string owner, string repo)
{
var response = await _factory
.Create("github")
.DoGet($"repos/{owner}/{repo}")
.OnResult(r => r.UseTypeForSuccess<Repository>())
.BuildAsync();
return response.GetAs<Repository>();
}
public async Task<Charge> CreateStripeChargeAsync(ChargeRequest charge)
{
var response = await _factory
.Create("stripe")
.DoPost("charges", charge)
.OnResult(r => r.UseTypeForSuccess<Charge>())
.BuildAsync();
return response.GetAs<Charge>();
}
}.Configure(config => config
.WithBaseUrl("https://api.example.com")
.WithTimeout(TimeSpan.FromSeconds(30))
.WithContentType("application/json")
.WithBearerAuthorization("your-token")
.WithHeader("X-Custom-Header", "value")
.WithBodySerialization(CaseStrategy.CamelCase)
.WithBodyDeserialization(CaseStrategy.SnakeCase))// Bearer Token
.WithBearerAuthorization("your-token")
// Basic Authentication
.WithBasicAuthorization("username", "password")
// Basic Authentication (pre-encoded)
.WithBasicAuthorization("base64EncodedToken")
// Custom Authorization
.WithAuthorization("CustomScheme", "token")builder.Services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
builder.Services.AddRest(config => config
.WithHttpClientFactory(serviceProvider.GetRequiredService<IHttpClientFactory>(), "MyApi"));For APIs that return interfaces, use type converters:
.Configure(config => config
.WithTypeConverter<IUser, UserDto>()
.WithTypeConverter<IProduct, ProductDto>()).Configure(config => config
.WithLogging(logger, logRequests: true, logResponses: true)).WithRetry() // 3 attempts, exponential backoff with jitter, server errors only.WithRetry(retry => retry
.WithMaxAttempts(5)
.UseExponentialBackoffWithJitter(
baseDelay: TimeSpan.FromSeconds(1),
multiplier: 2.0,
maxDelay: TimeSpan.FromSeconds(30),
jitterRange: TimeSpan.FromMilliseconds(100))
.ForServerErrors()
.ForExceptions(typeof(TaskCanceledException), typeof(HttpRequestException))).WithRetry(retry => retry
.WithMaxAttempts(3)
.UseExponentialBackoff(TimeSpan.FromSeconds(1), multiplier: 2.0)
.ForServerErrors()).WithRetry(retry => retry
.WithMaxAttempts(3)
.UseFixedDelay(TimeSpan.FromSeconds(2))
.ForStatusCodes(HttpStatusCode.ServiceUnavailable)).WithRetry(retry => retry
.WithMaxAttempts(3)
.UseRandom(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5))
.ForServerErrors()).WithRetry(retry => retry
.WithMaxAttempts(3)
.ForServerErrors() // 500, 502, 503, 504, 429
.ForStatusCodes(HttpStatusCode.RequestTimeout, HttpStatusCode.TooManyRequests)
.ForExceptions(typeof(TaskCanceledException)))Prevent cascading failures in distributed systems:
.WithCircuitBreaker(options => options
.UseFailureThreshold(5)
.UseTimeout(TimeSpan.FromMinutes(1))
.UseHalfOpenRetryTimeout(TimeSpan.FromSeconds(30)))Circuit Breaker States:
.DoGet("users")
.DoGet("users/123")
.DoGet("products?category=electronics&sort=price").DoPost("users", newUser)
.DoPost("orders", orderRequest).DoPut("users/123", updatedUser).DoPatch("users/123", partialUpdate).DoDelete("users/123")var response = await Rest
.Create()
.Configure(config => config
.WithBaseUrl("https://api.example.com")
.WithRetry())
.DoDownload("files/document.pdf")
.OnError(error => error.ThrowForNonSuccess())
.BuildAsync();
// Save to file
await response.SaveToFileAsync("./downloads", "document.pdf", replaceExisting: true);
// Or get as stream
var stream = response.ToStream();
// Or get as bytes
var bytes = response.ToByteArray();await using var fileStream = File.OpenRead("document.pdf");
var response = await Rest
.Create()
.Configure(config => config.WithBaseUrl("https://api.example.com"))
.DoUpload("files/upload", fileStream, "application/pdf")
.OnResult(r => r.UseTypeForSuccess<UploadResult>())
.BuildAsync();var fileBytes = File.ReadAllBytes("image.jpg");
.DoUpload("files/upload", fileBytes, "image/jpeg")[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
var response = await _restClient
.DoUpload("files/upload", file)
.OnResult(r => r.UseTypeForSuccess<UploadResult>())
.BuildAsync();
return Ok(response.GetAs<UploadResult>());
}.DoUpload("files/upload", file, settings => settings.UsePutAsMethod())
// Available: UsePostAsMethod(), UsePutAsMethod(), UsePatchAsMethod().OnResult(result => result
.UseTypeForSuccess<User>() // All 2xx codes
.UseTypeFor<ErrorResponse>(HttpStatusCode.BadRequest)
.UseTypeFor<ValidationErrors>(HttpStatusCode.UnprocessableEntity)
.UseEmptyFor(HttpStatusCode.NoContent))For APIs that return different structures with the same status code:
.OnResult(result => result
.UseTypeFor<SuccessResponse>(
HttpStatusCode.OK,
body => body.status == "success")
.UseTypeFor<ErrorResponse>(
HttpStatusCode.OK,
body => body.status == "error")).OnResult(result => result
.UseTypeFor<ErrorResponse>(new[] {
HttpStatusCode.BadRequest,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
})).OnResult(result => result.UseTypeForAll<ApiResponse>()).OnResult(result => result.DoNotMap()).OnError(error => error
.ThrowForNonSuccess() // Throw for any non-2xx status
.ThrowFor(HttpStatusCode.Unauthorized)
.NotThrowFor(HttpStatusCode.NotFound)).OnError(error => error
.ThrowFor(HttpStatusCode.BadRequest,
body => body.errorCode == "VALIDATION_FAILED"))Provide default responses for specific error scenarios:
.OnError(error => error
.UseFallback(HttpStatusCode.ServiceUnavailable, new {
message = "Service temporarily unavailable"
})
.UseFallback(HttpStatusCode.NotFound, new User {
Id = 0,
Name = "Unknown"
})).OnError(error => error
.ThrowForNonSuccess()
.NotThrowForNonMappedResult()).WithCertificate(
certificatePath: "client-cert.pem",
keyPath: "client-key.pem",
keyPassword: "optional-password")// From file
.WithCertificate(pfxPath: "client-cert.pfx", password: "password")
// From bytes
var pfxData = File.ReadAllBytes("client-cert.pfx");
.WithCertificate(pfxData: pfxData, password: "password")// By thumbprint
.WithCertificateFromStore(
thumbprint: "A1B2C3D4E5F6...",
storeLocation: StoreLocation.CurrentUser)
// By subject name
.WithCertificateFromStoreBySubject(
subjectName: "CN=MyCert",
storeLocation: StoreLocation.LocalMachine)var certificate = new X509Certificate2("client-cert.pfx", "password");
.WithCertificate(certificate).WithCertificate(options =>
{
options.Type = CertificateType.PemWithKey;
options.CertificatePath = "client-cert.pem";
options.KeyPath = "client-key.pem";
options.ValidateServerCertificate = false; // For development only
options.ServerCertificateValidationCallback = (sender, cert, chain, errors) =>
{
// Custom validation logic
return true;
};
})public class UserRepository : IUserRepository
{
private readonly IRestRequest _client;
public UserRepository(IRestRequest client)
{
_client = client;
}
public async Task<User> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
var response = await _client
.DoGet($"users/{id}")
.OnResult(r => r.UseTypeForSuccess<User>())
.OnError(e => e
.ThrowForNonSuccess()
.UseFallback(HttpStatusCode.NotFound, new User { Id = id, Name = "Unknown" }))
.BuildAsync(cancellationToken);
return response.GetAs<User>();
}
public async Task<User> CreateAsync(CreateUserRequest request, CancellationToken cancellationToken = default)
{
var response = await _client
.DoPost("users", request)
.OnResult(r => r
.UseTypeFor<User>(HttpStatusCode.Created)
.UseTypeFor<ValidationErrorResponse>(HttpStatusCode.BadRequest))
.OnError(e => e.ThrowForNonSuccess())
.BuildAsync(cancellationToken);
return response.GetAs<User>();
}
public async Task<bool> UpdateAsync(int id, UpdateUserRequest request, CancellationToken cancellationToken = default)
{
var response = await _client
.DoPut($"users/{id}", request)
.OnResult(r => r.UseEmptyFor(HttpStatusCode.NoContent))
.OnError(e => e.ThrowForNonSuccess())
.BuildAsync(cancellationToken);
return response.IsSuccessStatusCode();
}
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
{
var response = await _client
.DoDelete($"users/{id}")
.OnResult(r => r.UseEmptyFor(HttpStatusCode.NoContent))
.OnError(e => e
.ThrowForNonSuccess()
.NotThrowFor(HttpStatusCode.NotFound))
.BuildAsync(cancellationToken);
return response.IsSuccessStatusCode();
}
}Some APIs return 200 OK for all responses and use body fields to indicate errors:
var response = await Rest
.Create()
.Configure(config => config
.WithBaseUrl("https://legacy-api.com")
.WithRetry())
.DoGet("users")
.OnResult(result => result
.UseTypeFor<List<User>>(
HttpStatusCode.OK,
body => body.success == true))
.OnError(error => error
.ThrowFor(
HttpStatusCode.OK,
body => body.success == false)
.ThrowForNonSuccess())
.BuildAsync();builder.Services.AddRestFactory()
.AddRestConfiguration("user-service", config => config
.WithBaseUrl("http://user-service:8080")
.WithCircuitBreaker(options => options
.UseFailureThreshold(5)
.UseTimeout(TimeSpan.FromMinutes(1)))
.WithRetry(retry => retry
.WithMaxAttempts(3)
.UseExponentialBackoffWithJitter(TimeSpan.FromSeconds(1))
.ForServerErrors()))
.AddRestConfiguration("order-service", config => config
.WithBaseUrl("http://order-service:8080")
.WithCircuitBreaker(options => options
.UseFailureThreshold(3)
.UseTimeout(TimeSpan.FromMinutes(2)))
.WithRetry(retry => retry
.WithMaxAttempts(2)
.UseFixedDelay(TimeSpan.FromSeconds(2))));public class ProductService
{
private readonly IRestFactory _factory;
private readonly ILogger<ProductService> _logger;
public ProductService(IRestFactory factory, ILogger<ProductService> logger)
{
_factory = factory;
_logger = logger;
}
public async Task<Product> GetProductAsync(string productId)
{
try
{
var response = await _factory
.Create("catalog")
.DoGet($"products/{productId}")
.OnResult(r => r.UseTypeForSuccess<Product>())
.OnError(e => e
.ThrowForNonSuccess()
.UseFallback(HttpStatusCode.ServiceUnavailable, new Product
{
Id = productId,
Name = "Product Unavailable",
Available = false
}))
.BuildAsync();
_logger.LogInformation(
"Retrieved product {ProductId} in {ElapsedTime}ms with {Retries} retries",
productId,
response.ElapsedTime.TotalMilliseconds,
response.RetriesMade);
return response.GetAs<Product>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve product {ProductId}", productId);
throw;
}
}
}Every response contains comprehensive metadata:
var response = await Rest.Create()...BuildAsync();
Console.WriteLine($"Status: {response.StatusCode}");
Console.WriteLine($"URL: {response.Url}");
Console.WriteLine($"Method: {response.Method}");
Console.WriteLine($"Elapsed Time: {response.ElapsedTime}");
Console.WriteLine($"Retries Made: {response.RetriesMade}");
Console.WriteLine($"Fallback Used: {response.FallbackUsed}");
Console.WriteLine($"Is Success: {response.IsSuccessStatusCode()}");
// Access results
var user = response.GetAs<User>(); // Typed result
var json = response.ToString(); // JSON string
var bytes = response.ToByteArray(); // Byte array
var stream = response.ToStream(); // Stream
dynamic dyn = response.DynamicResult; // Dynamic objecttry
{
var response = await Rest.Create()...BuildAsync();
}
catch (NonSuccessException ex)
{
// HTTP error status codes
Console.WriteLine($"Status: {ex.Response.StatusCode}");
Console.WriteLine($"Content: {ex.Response}");
}
catch (NotMappedResultTypeException ex)
{
// No type mapping found for status code
Console.WriteLine($"Unmapped status: {ex.StatusCode}");
}
catch (DifferentResponseTypeException ex)
{
// Attempting to cast to wrong type
Console.WriteLine($"Expected: {ex.ExpectedType}, Actual: {ex.ActualType}");
}
catch (ParsingTypeException ex)
{
// JSON deserialization failed
Console.WriteLine($"Failed to parse: {ex.Content}");
}
catch (FileAlreadyExsistsOnDownloadException ex)
{
// File exists during download
Console.WriteLine($"File exists: {ex.FilePath}");
}
catch (NoActionMadeException)
{
// No HTTP action was defined
}
catch (CircuitBreakerOpenException ex)
{
// Circuit breaker is open
Console.WriteLine("Service circuit breaker is open");
}Always Use Dependency Injection: Register REST clients as services for better testability and configuration management
Configure Retry Policies: Use retry policies in production for transient failure resilience
Implement Circuit Breakers: Prevent cascade failures in distributed systems
Use Named Configurations: Leverage the factory pattern when integrating with multiple APIs
Handle Errors Gracefully: Use fallbacks for non-critical operations
Leverage Strong Typing: Always map responses to strongly typed models
Configure Timeouts: Set appropriate timeouts based on your API characteristics
Enable Logging: Use structured logging for debugging and monitoring
Secure Sensitive Headers: Use specific authorization methods instead of manual headers for tokens
Use CancellationTokens: Always pass cancellation tokens to support request cancellation
Myth.Rest uses ObjectPool<RestBuilder> internally to minimize allocations and improve performance in high-throughput scenarios. The pooling is transparent and automatic.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.