FunctionalDdd.Http 3.0.0-alpha.44

This is a prerelease version of FunctionalDdd.Http.
dotnet add package FunctionalDdd.Http --version 3.0.0-alpha.44
                    
NuGet\Install-Package FunctionalDdd.Http -Version 3.0.0-alpha.44
                    
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="FunctionalDdd.Http" Version="3.0.0-alpha.44" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FunctionalDdd.Http" Version="3.0.0-alpha.44" />
                    
Directory.Packages.props
<PackageReference Include="FunctionalDdd.Http" />
                    
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 FunctionalDdd.Http --version 3.0.0-alpha.44
                    
#r "nuget: FunctionalDdd.Http, 3.0.0-alpha.44"
                    
#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 FunctionalDdd.Http@3.0.0-alpha.44
                    
#: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=FunctionalDdd.Http&version=3.0.0-alpha.44&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FunctionalDdd.Http&version=3.0.0-alpha.44&prerelease
                    
Install as a Cake Tool

FunctionalDdd.Http

HTTP client extensions for Railway Oriented Programming with Result and Maybe monads.

Overview

This library provides fluent extension methods for working with HttpResponseMessage in a functional style, integrating seamlessly with the Railway Oriented Programming patterns from FunctionalDdd.RailwayOrientedProgramming.

Features

  • Specific Status Code Handling: Handle 401 Unauthorized, 403 Forbidden, 409 Conflict
  • Range-based Error Handling: Handle all 4xx client errors or 5xx server errors at once
  • EnsureSuccess: Functional alternative to EnsureSuccessStatusCode() that returns Result
  • Error Handling for HTTP Status Codes: Handle specific status codes (404 Not Found) functionally
  • Custom Error Handling: Flexible callbacks for failed HTTP responses
  • JSON Deserialization: Native support for deserializing to Result<T> and Maybe<T>
  • Fluent Composition: Chain HTTP operations with Railway Oriented Programming patterns
  • Async-First: All methods support asynchronous workflows with proper cancellation token support
  • AOT Compatible: Fully compatible with Native AOT compilation

Installation

dotnet add package FunctionalDdd.Http

Usage

Basic Error Handling

using FunctionalDdd;

// Handle 404 Not Found
var result = await httpClient.GetAsync($"api/users/{userId}", ct)
    .HandleNotFoundAsync(Error.NotFound("User not found", userId))
    .ReadResultFromJsonAsync(UserJsonContext.Default.User, ct);

// Result will contain either:
// - Success with User object
// - Failure with NotFoundError

Handle Specific HTTP Status Codes

// Handle authentication/authorization errors
var result = await httpClient.PostAsync("api/admin/users", content, ct)
    .HandleUnauthorizedAsync(Error.Unauthorized("Please login"))
    .HandleForbiddenAsync(Error.Forbidden("Admin access required"))
    .HandleConflictAsync(Error.Conflict("Username already exists"))
    .ReadResultFromJsonAsync(UserJsonContext.Default.User, ct);

// Each handler only intercepts its specific status code
// - 401 ? UnauthorizedError
// - 403 ? ForbiddenError
// - 409 ? ConflictError
// - Other codes pass through to next handler

Handle Error Ranges

// Handle all client errors (4xx) or server errors (5xx) at once
var result = await httpClient.GetAsync("api/data", ct)
    .HandleClientErrorAsync(code => Error.BadRequest($"Client error: {code}"))
    .HandleServerErrorAsync(code => Error.ServiceUnavailable($"Server error: {code}"))
    .ReadResultFromJsonAsync(DataJsonContext.Default.Data, ct);

// Client errors (400-499) ? Custom error via factory
// Server errors (500+) ? Custom error via factory
// Success codes ? Continue to JSON deserialization

Ensure Success Status

// Functional alternative to EnsureSuccessStatusCode()
var result = await httpClient.DeleteAsync($"api/items/{id}", ct)
    .EnsureSuccessAsync()  // Returns Result instead of throwing
    .TapAsync(response => _logger.LogInformation("Deleted item {Id}", id));

// With custom error factory
var result = await httpClient.GetAsync("api/data", ct)
    .EnsureSuccessAsync(code => Error.Unexpected($"API call failed with {code}"))
    .ReadResultFromJsonAsync(jsonContext, ct);

Custom Error Handling

var result = await httpClient.PostAsync("api/orders", content, ct)
    .HandleFailureAsync(
        async (response, context, ct) =>
        {
            var errorContent = await response.Content.ReadAsStringAsync(ct);
            return Error.BadRequest($"Order creation failed: {errorContent}");
        },
        context: null,
        ct);

JSON Deserialization with Maybe

// Returns Maybe<T> when the value might be null
var result = await httpClient.GetAsync($"api/users/{userId}/profile", ct)
    .ReadResultMaybeFromJsonAsync(ProfileJsonContext.Default.Profile, ct)
    .MapAsync(maybe => maybe.HasValue 
        ? $"Profile: {maybe.Value.Name}" 
        : "No profile available");

Railway Oriented Programming Chain

var result = await httpClient.GetAsync($"api/products/{productId}", ct)
    .HandleNotFoundAsync(Error.NotFound("Product", productId))
    .ReadResultFromJsonAsync(ProductJsonContext.Default.Product, ct)
    .EnsureAsync(
        p => p.IsAvailable, 
        Error.Conflict("Product is not available"))
    .TapAsync(p => _logger.LogInformation("Retrieved product: {Name}", p.Name))
    .MapAsync(p => new ProductViewModel(p));

Composing Multiple Status Handlers

// Chain multiple handlers for comprehensive error handling
var result = await httpClient.PostAsync("api/orders", orderContent, ct)
    .HandleUnauthorizedAsync(Error.Unauthorized("Please login to place orders"))
    .HandleForbiddenAsync(Error.Forbidden("Your account cannot place orders"))
    .HandleConflictAsync(Error.Conflict("Order already exists"))
    .HandleClientErrorAsync(code => Error.BadRequest($"Invalid order data: {code}"))
    .HandleServerErrorAsync(code => Error.ServiceUnavailable($"Order service unavailable: {code}"))
    .ReadResultFromJsonAsync(OrderJsonContext.Default.Order, ct)
    .TapAsync(order => _logger.LogInformation("Order {OrderId} created", order.Id));

// Handlers are evaluated in order - first match wins

Working with Result<HttpResponseMessage>

Result<HttpResponseMessage> responseResult = await httpClient
    .GetAsync("api/data", ct)
    .HandleNotFoundAsync(Error.NotFound("Data not found"));

// Chain further operations on the Result
var data = await responseResult
    .ReadResultFromJsonAsync(DataJsonContext.Default.Data, ct)
    .MapAsync(d => d.Transform());

API Reference

Status Code Handlers

HandleNotFound / HandleNotFoundAsync

Converts HTTP 404 responses to NotFoundError.

Result<HttpResponseMessage> HandleNotFound(
    this HttpResponseMessage response, 
    NotFoundError notFoundError)

Task<Result<HttpResponseMessage>> HandleNotFoundAsync(
    this Task<HttpResponseMessage> responseTask, 
    NotFoundError notFoundError)
HandleUnauthorized / HandleUnauthorizedAsync

Converts HTTP 401 responses to UnauthorizedError.

Result<HttpResponseMessage> HandleUnauthorized(
    this HttpResponseMessage response, 
    UnauthorizedError unauthorizedError)

Task<Result<HttpResponseMessage>> HandleUnauthorizedAsync(
    this Task<HttpResponseMessage> responseTask, 
    UnauthorizedError unauthorizedError)
HandleForbidden / HandleForbiddenAsync

Converts HTTP 403 responses to ForbiddenError.

Result<HttpResponseMessage> HandleForbidden(
    this HttpResponseMessage response, 
    ForbiddenError forbiddenError)

Task<Result<HttpResponseMessage>> HandleForbiddenAsync(
    this Task<HttpResponseMessage> responseTask, 
    ForbiddenError forbiddenError)
HandleConflict / HandleConflictAsync

Converts HTTP 409 responses to ConflictError.

Result<HttpResponseMessage> HandleConflict(
    this HttpResponseMessage response, 
    ConflictError conflictError)

Task<Result<HttpResponseMessage>> HandleConflictAsync(
    this Task<HttpResponseMessage> responseTask, 
    ConflictError conflictError)

Range Handlers

HandleClientError / HandleClientErrorAsync

Handles any HTTP client error (4xx) status codes with a custom error factory.

Result<HttpResponseMessage> HandleClientError(
    this HttpResponseMessage response,
    Func<HttpStatusCode, Error> errorFactory)

Task<Result<HttpResponseMessage>> HandleClientErrorAsync(
    this Task<HttpResponseMessage> responseTask,
    Func<HttpStatusCode, Error> errorFactory)

Example:

var result = httpClient.GetAsync(url, ct)
    .HandleClientErrorAsync(code => code switch
    {
        HttpStatusCode.BadRequest => Error.BadRequest("Invalid request"),
        HttpStatusCode.NotFound => Error.NotFound("Resource not found"),
        _ => Error.Unexpected($"Client error: {code}")
    });
HandleServerError / HandleServerErrorAsync

Handles any HTTP server error (5xx) status codes with a custom error factory.

Result<HttpResponseMessage> HandleServerError(
    this HttpResponseMessage response,
    Func<HttpStatusCode, Error> errorFactory)

Task<Result<HttpResponseMessage>> HandleServerErrorAsync(
    this Task<HttpResponseMessage> responseTask,
    Func<HttpStatusCode, Error> errorFactory)

Example:

var result = httpClient.PostAsync(url, content, ct)
    .HandleServerErrorAsync(code => Error.ServiceUnavailable($"API error: {code}"));

Success Validation

EnsureSuccess / EnsureSuccessAsync

Ensures the HTTP response has a success status code, otherwise returns an error. This is a functional alternative to HttpResponseMessage.EnsureSuccessStatusCode().

Result<HttpResponseMessage> EnsureSuccess(
    this HttpResponseMessage response,
    Func<HttpStatusCode, Error>? errorFactory = null)

Task<Result<HttpResponseMessage>> EnsureSuccessAsync(
    this Task<HttpResponseMessage> responseTask,
    Func<HttpStatusCode, Error>? errorFactory = null)

Example:

// Default error
var result = await httpClient.DeleteAsync($"api/items/{id}", ct)
    .EnsureSuccessAsync();

// Custom error
var result = await httpClient.PutAsync(url, content, ct)
    .EnsureSuccessAsync(code => Error.Unexpected($"Update failed: {code}"));

Custom Error Handling

HandleFailureAsync

Custom error handling for any non-success status code.

Task<Result<HttpResponseMessage>> HandleFailureAsync<TContext>(
    this HttpResponseMessage response,
    Func<HttpResponseMessage, TContext, CancellationToken, Task<Error>> callbackFailedStatusCode,
    TContext context,
    CancellationToken cancellationToken)

JSON Deserialization

ReadResultFromJsonAsync

Deserialize JSON response to Result<T>. Returns error if response is null.

Task<Result<TValue>> ReadResultFromJsonAsync<TValue>(
    this HttpResponseMessage response,
    JsonTypeInfo<TValue> jsonTypeInfo,
    CancellationToken cancellationToken)
ReadResultMaybeFromJsonAsync

Deserialize JSON response to Result<Maybe<T>>. Null responses become Maybe.None.

Task<Result<Maybe<TValue>>> ReadResultMaybeFromJsonAsync<TValue>(
    this HttpResponseMessage response,
    JsonTypeInfo<TValue> jsonTypeInfo,
    CancellationToken cancellationToken)

Design Principles

This library follows these design principles:

  1. Separation of Concerns: HTTP infrastructure concerns are separated from core functional programming patterns
  2. Dependency Inversion: Infrastructure depends on core abstractions, not vice versa
  3. Composability: All methods integrate with Railway Oriented Programming patterns
  4. Explicit Error Handling: No hidden exceptions; all errors are represented in the type system
  5. No Polly Overlap: Focused on status code handling, not resilience patterns (use Polly for retry/circuit breaker)
  • FunctionalDdd.RailwayOrientedProgramming: Core Result and Maybe monads
  • FunctionalDdd.Asp: ASP.NET Core integration (converts Result to ActionResult/IResult)

License

MIT

Contributing

Contributions are welcome! Please see the main repository for contribution guidelines.

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-alpha.44 34 1/13/2026
3.0.0-alpha.20 41 1/6/2026
3.0.0-alpha.19 40 1/5/2026
3.0.0-alpha.13 39 1/5/2026
3.0.0-alpha.9 38 1/5/2026