Myth.Rest 4.3.0-preview.2

This is a prerelease version of Myth.Rest.
There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Myth.Rest" Version="4.3.0-preview.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Myth.Rest" Version="4.3.0-preview.2" />
                    
Directory.Packages.props
<PackageReference Include="Myth.Rest" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Myth.Rest --version 4.3.0-preview.2
                    
#r "nuget: Myth.Rest, 4.3.0-preview.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Myth.Rest@4.3.0-preview.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Myth.Rest&version=4.3.0-preview.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Myth.Rest&version=4.3.0-preview.2&prerelease
                    
Install as a Cake Tool

<img style="float: right;" src="myth-rest-logo.png" alt="drawing" width="250"/>

Myth.Rest

NuGet Version NuGet Version

License

pt-br en

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

.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

  1. Always Use Dependency Injection: Register REST clients as services for better testability and configuration management

  2. Configure Retry Policies: Use retry policies in production for transient failure resilience

  3. Implement Circuit Breakers: Prevent cascade failures in distributed systems

  4. Use Named Configurations: Leverage the factory pattern when integrating with multiple APIs

  5. Handle Errors Gracefully: Use fallbacks for non-critical operations

  6. Leverage Strong Typing: Always map responses to strongly typed models

  7. Configure Timeouts: Set appropriate timeouts based on your API characteristics

  8. Enable Logging: Use structured logging for debugging and monitoring

  9. Secure Sensitive Headers: Use specific authorization methods instead of manual headers for tokens

  10. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed