Myth.Rest
4.3.0-preview.2
See the version list below for details.
dotnet add package Myth.Rest --version 4.3.0-preview.2
NuGet\Install-Package Myth.Rest -Version 4.3.0-preview.2
<PackageReference Include="Myth.Rest" Version="4.3.0-preview.2" />
<PackageVersion Include="Myth.Rest" Version="4.3.0-preview.2" />
<PackageReference Include="Myth.Rest" />
paket add Myth.Rest --version 4.3.0-preview.2
#r "nuget: Myth.Rest, 4.3.0-preview.2"
#:package Myth.Rest@4.3.0-preview.2
#addin nuget:?package=Myth.Rest&version=4.3.0-preview.2&prerelease
#tool nuget:?package=Myth.Rest&version=4.3.0-preview.2&prerelease
<img style="float: right;" src="myth-rest-logo.png" alt="drawing" width="250"/>
Myth.Rest
A modern, fluent REST client for .NET with enterprise-grade resilience patterns. Built for developers who value clean code, type safety, and minimal boilerplate.
Why Myth.Rest?
- Fluent API: Intuitive, chainable interface that reads like natural language
- Enterprise Resilience: Built-in retry policies with exponential backoff and circuit breakers
- Type Safety: Strong typing with automatic JSON serialization/deserialization
- Zero Configuration: Works out of the box with sensible defaults
- Flexible Architecture: Object pooling for high-performance scenarios
- DI-First Design: Seamless ASP.NET Core integration with named configurations
- Comprehensive File Support: Upload and download files with minimal code
- Smart Error Handling: Conditional error handling with fallback support
Installation
dotnet add package Myth.Rest
Quick Start
Simple GET Request
var 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>();
POST Request with Body
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>();
Dependency Injection
Basic Registration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRest(config => config
.WithBaseUrl("https://api.example.com")
.WithBearerAuthorization(builder.Configuration["ApiToken"])
.WithRetry());
var app = builder.Build();
Using in Services
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>();
}
}
Factory Pattern (Multiple APIs)
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>();
}
}
Configuration
Basic Configuration
.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))
Authorization Methods
// Bearer Token
.WithBearerAuthorization("your-token")
// Basic Authentication
.WithBasicAuthorization("username", "password")
// Basic Authentication (pre-encoded)
.WithBasicAuthorization("base64EncodedToken")
// Custom Authorization
.WithAuthorization("CustomScheme", "token")
Using HttpClientFactory
builder.Services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
});
builder.Services.AddRest(config => config
.WithHttpClientFactory(serviceProvider.GetRequiredService<IHttpClientFactory>(), "MyApi"));
Type Converters
For APIs that return interfaces, use type converters:
.Configure(config => config
.WithTypeConverter<IUser, UserDto>()
.WithTypeConverter<IProduct, ProductDto>())
Logging
.Configure(config => config
.WithLogging(logger, logRequests: true, logResponses: true))
Retry Policies
Default Retry (Recommended)
.WithRetry() // 3 attempts, exponential backoff with jitter, server errors only
Custom Retry Strategies
Exponential Backoff with Jitter (Production)
.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)))
Exponential Backoff
.WithRetry(retry => retry
.WithMaxAttempts(3)
.UseExponentialBackoff(TimeSpan.FromSeconds(1), multiplier: 2.0)
.ForServerErrors())
Fixed Delay
.WithRetry(retry => retry
.WithMaxAttempts(3)
.UseFixedDelay(TimeSpan.FromSeconds(2))
.ForStatusCodes(HttpStatusCode.ServiceUnavailable))
Random Delay
.WithRetry(retry => retry
.WithMaxAttempts(3)
.UseRandom(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5))
.ForServerErrors())
Retry Configuration Options
.WithRetry(retry => retry
.WithMaxAttempts(3)
.ForServerErrors() // 500, 502, 503, 504, 429
.ForStatusCodes(HttpStatusCode.RequestTimeout, HttpStatusCode.TooManyRequests)
.ForExceptions(typeof(TaskCanceledException)))
Circuit Breaker
Prevent cascading failures in distributed systems:
.WithCircuitBreaker(options => options
.UseFailureThreshold(5)
.UseTimeout(TimeSpan.FromMinutes(1))
.UseHalfOpenRetryTimeout(TimeSpan.FromSeconds(30)))
Circuit Breaker States:
- Closed: Normal operation
- Open: Failure threshold exceeded, requests are blocked
- Half-Open: Testing if service recovered
HTTP Operations
GET
.DoGet("users")
.DoGet("users/123")
.DoGet("products?category=electronics&sort=price")
POST
.DoPost("users", newUser)
.DoPost("orders", orderRequest)
PUT
.DoPut("users/123", updatedUser)
PATCH
.DoPatch("users/123", partialUpdate)
DELETE
.DoDelete("users/123")
File Operations
Download Files
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();
Upload Files
From Stream
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();
From Byte Array
var fileBytes = File.ReadAllBytes("image.jpg");
.DoUpload("files/upload", fileBytes, "image/jpeg")
From IFormFile (ASP.NET Core)
[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>());
}
Custom HTTP Method
.DoUpload("files/upload", file, settings => settings.UsePutAsMethod())
// Available: UsePostAsMethod(), UsePutAsMethod(), UsePatchAsMethod()
Result Handling
Type Mapping by Status Code
.OnResult(result => result
.UseTypeForSuccess<User>() // All 2xx codes
.UseTypeFor<ErrorResponse>(HttpStatusCode.BadRequest)
.UseTypeFor<ValidationErrors>(HttpStatusCode.UnprocessableEntity)
.UseEmptyFor(HttpStatusCode.NoContent))
Conditional Type Mapping
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"))
Multiple Status Codes
.OnResult(result => result
.UseTypeFor<ErrorResponse>(new[] {
HttpStatusCode.BadRequest,
HttpStatusCode.Conflict,
HttpStatusCode.UnprocessableEntity
}))
All Status Codes
.OnResult(result => result.UseTypeForAll<ApiResponse>())
No Type Mapping
.OnResult(result => result.DoNotMap())
Error Handling
Basic Error Handling
.OnError(error => error
.ThrowForNonSuccess() // Throw for any non-2xx status
.ThrowFor(HttpStatusCode.Unauthorized)
.NotThrowFor(HttpStatusCode.NotFound))
Conditional Error Handling
.OnError(error => error
.ThrowFor(HttpStatusCode.BadRequest,
body => body.errorCode == "VALIDATION_FAILED"))
Fallback Responses
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"
}))
Don't Throw for Unmapped Results
.OnError(error => error
.ThrowForNonSuccess()
.NotThrowForNonMappedResult())
Client Certificates (mTLS)
PEM Certificate with Key
.WithCertificate(
certificatePath: "client-cert.pem",
keyPath: "client-key.pem",
keyPassword: "optional-password")
PFX Certificate
// From file
.WithCertificate(pfxPath: "client-cert.pfx", password: "password")
// From bytes
var pfxData = File.ReadAllBytes("client-cert.pfx");
.WithCertificate(pfxData: pfxData, password: "password")
From Certificate Store
// By thumbprint
.WithCertificateFromStore(
thumbprint: "A1B2C3D4E5F6...",
storeLocation: StoreLocation.CurrentUser)
// By subject name
.WithCertificateFromStoreBySubject(
subjectName: "CN=MyCert",
storeLocation: StoreLocation.LocalMachine)
X509Certificate2 Instance
var certificate = new X509Certificate2("client-cert.pfx", "password");
.WithCertificate(certificate)
Advanced Certificate Configuration
.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;
};
})
Advanced Patterns
Repository Pattern
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();
}
}
Working with Legacy APIs
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();
Microservices Communication
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))));
Resilient E-Commerce Service
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;
}
}
}
Response Metadata
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 object
Exception Types
try
{
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");
}
Best Practices
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
Performance
Myth.Rest uses ObjectPool<RestBuilder> internally to minimize allocations and improve performance in high-throughput scenarios. The pooling is transparent and automatic.
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
| 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
- Myth.Commons (>= 4.3.0-preview.2)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Myth.Rest:
| Package | Downloads |
|---|---|
|
Packs.Template.BaseApi
Basis for any Packs API |
|
|
Harpy.Presentation
Basis for the presentation layer of the Harpy Framework |
|
|
Harpy.Test.Presentation
Basis for the presentation test layer of the Harpy Framework |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.4.0-preview.6 | 43 | 2/20/2026 |
| 4.4.0-preview.5 | 43 | 2/19/2026 |
| 4.4.0-preview.4 | 38 | 2/18/2026 |
| 4.4.0-preview.3 | 44 | 2/18/2026 |
| 4.4.0-preview.2 | 43 | 2/17/2026 |
| 4.4.0-preview.1 | 48 | 2/14/2026 |
| 4.3.0 | 101 | 2/1/2026 |
| 4.3.0-preview.3 | 49 | 2/1/2026 |
| 4.3.0-preview.2 | 138 | 12/22/2025 |
| 4.2.1-preview.1 | 626 | 12/2/2025 |
| 4.2.0 | 482 | 11/30/2025 |
| 4.2.0-preview.1 | 76 | 11/29/2025 |
| 4.1.0 | 358 | 11/27/2025 |
| 4.1.0-preview.3 | 142 | 11/27/2025 |
| 4.1.0-preview.2 | 137 | 11/27/2025 |
| 4.1.0-preview.1 | 139 | 11/26/2025 |
| 4.0.1 | 194 | 11/22/2025 |
| 4.0.1-preview.8 | 164 | 11/22/2025 |
| 4.0.1-preview.7 | 162 | 11/22/2025 |
| 4.0.1-preview.6 | 150 | 11/22/2025 |