Skip to content

MelbourneDeveloper/RestClient.Net

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Repository files navigation

RestClient.Net

diagram

The safest way to make REST calls in C#

New! Generate MCP servers from OpenAPI specs!!!

Built from the ground up with functional programming, type safety, and modern .NET patterns. Successor to the original RestClient.Net.

What Makes It Different

This library is uncompromising in its approach to type safety and functional design:

  • HttpClient extensions - Works with HttpClient lifecycle management via IHttpClientFactory.CreateClient()
  • Result types - Explicit error handling without exceptions
  • Exhaustiveness checking - Compile-time guarantees via Exhaustion
  • Functional composition - Pure functions with no interfaces

Features

  • Generate an MCP Server and client code from an OpenAPI 3.x spec.
  • Result Types - Returns Result<TSuccess, HttpError<TError>> with closed hierarchy types for compile-time safety (Outcome package)
  • Zero Exceptions - No exception throwing for predictable error handling
  • Progress Reporting - Built-in download/upload progress tracking
  • Polly Integration - Support for convention-based retry policies and resilience patterns
  • Async/Await Only - Modern async patterns throughout
  • HttpClient Extensions - Works with IHttpClientFactory.CreateClient() for proper pooled connections and DNS behavior handling
  • Exhaustiveness Checking - Uses Exhaustion for compile-time completeness guarantees (stopgap until C# adds discriminated unions)

The design focuses on discriminated unions for results, and adding the Exhaustion analyzer package gives you exhaustive pattern matching on type in C#. RestClient.Net is well ahead of the game. You can use discriminated unions with exhaustiveness checks in C# right now.

Installation

Install the core package:

dotnet add package RestClient.Net

RestClient.Net installs the Exhaustion analyzer. Is is a Roslyn analyzer that provides compile-time exhaustiveness checking for pattern matching. Without it, you lose one of RestClient.Net's core safety guarantees.

Usage

Basic GET Request

A complete example making a GET request to JSONPlaceholder:

usingSystem.Text.Json;usingRestClient.Net;usingUrls;// Define modelsinternalsealedrecordPost(intUserId,intId,stringTitle,stringBody);internalsealedrecordErrorResponse(stringMessage);// Create HttpClient (use IHttpClientFactory in production)usingvarhttpClient=newHttpClient();// Make the GET callvarresult=awaithttpClient.GetAsync(url:"https://jsonplaceholder.typicode.com/posts/1".ToAbsoluteUrl(),deserializeSuccess:DeserializePost,deserializeError:DeserializeError).ConfigureAwait(false);// Pattern match on the result - MUST handle all casesvaroutput=resultswitch{OkPost(varpost)=>$"Success: {post.Title} by user {post.UserId}",ErrorPost(ResponseErrorPost(varerrorBody,varstatusCode,_))=>$"Error {statusCode}: {errorBody.Message}",ErrorPost(ExceptionErrorPost(varexception))=>$"Exception: {exception.Message}",};Console.WriteLine(output);asyncTask<Post?>DeserializePost(HttpResponseMessageresponse,CancellationTokenct)=>awaitJsonSerializer.DeserializeAsync<Post>(awaitresponse.Content.ReadAsStreamAsync(ct).ConfigureAwait(false),newJsonSerializerOptions{PropertyNameCaseInsensitive=true},cancellationToken:ct).ConfigureAwait(false);asyncTask<ErrorResponse?>DeserializeError(HttpResponseMessageresponse,CancellationTokenct)=>awaitJsonSerializer.DeserializeAsync<ErrorResponse>(awaitresponse.Content.ReadAsStreamAsync(ct).ConfigureAwait(false),newJsonSerializerOptions{PropertyNameCaseInsensitive=true},cancellationToken:ct).ConfigureAwait(false);

Result Type and Type Aliases

C# doesn't officially support discriminated unions, but you can achieve closed type hierarchies with the sealed modifier. The Outcome package gives you a set of Result types designed for exhaustiveness checks. Until C# gains full discriminated union support, you need to add type aliases like this:

// Type aliases for concise pattern matching (usually in GlobalUsings.cs)globalusingOkPost=Outcome.Result<Post,Outcome.HttpError<ErrorResponse>>.Ok<Post,Outcome.HttpError<ErrorResponse>>;globalusingErrorPost=Outcome.Result<Post,Outcome.HttpError<ErrorResponse>>.Error<Post,Outcome.HttpError<ErrorResponse>>;globalusingResponseErrorPost=Outcome.HttpError<ErrorResponse>.ErrorResponseError;globalusingExceptionErrorPost=Outcome.HttpError<ErrorResponse>.ExceptionError;

If you use the OpenAPI generator, it will generate these type aliases for you automatically.

OpenAPI Client and MCP Code Generation

Generate type-safe C# clients and MCP servers from OpenAPI specs.

Client Generation

dotnet add package RestClient.Net.OpenApiGenerator

Generate extension methods from OpenAPI 3.x specs:

// Generated code usageusingYourApi.Generated;varhttpClient=factory.CreateClient();// All HTTP methods supported with Result typesvaruser=awaithttpClient.GetUserById("123",ct);varcreated=awaithttpClient.CreateUser(newUser,ct);varupdated=awaithttpClient.UpdateUser((Params:"123",Body:user),ct);vardeleted=awaithttpClient.DeleteUser("123",ct);// Pattern match on resultsswitch(user){caseOkUser(varsuccess):Console.WriteLine($"User: {success.Name}");break;caseErrorUser(varerror):Console.WriteLine($"Error: {error.StatusCode}");break;}

The generator creates extension methods on HttpClient, model classes from schemas, and result type aliases for pattern matching.

MCP Server Generation

Generate Model Context Protocol servers for Claude Code from OpenAPI specs:

# Generate API client first dotnet run --project RestClient.Net.OpenApiGenerator.Cli -- \ -u api.yaml \ -o Generated \ -n YourApi.Generated # Generate MCP tools from the same spec dotnet run --project RestClient.Net.McpGenerator.Cli -- \ --openapi-url api.yaml \ --output-file Generated/McpTools.g.cs \ --namespace YourApi.Mcp \ --server-name YourApi \ --ext-namespace YourApi.Generated \ --tags "Search,Resources"

The MCP generator wraps the generated extension methods as MCP tools that Claude Code can invoke.

Complete example: See Samples/NucliaDbClient.McpServer for a working MCP server built from the NucliaDB OpenAPI spec. The example includes:

  • Generated client code (Samples/NucliaDbClient/Generated)
  • Generated MCP tools (NucliaDbMcpTools.g.cs)
  • MCP server host project (NucliaDbClient.McpServer)
  • Docker Compose setup for NucliaDB
  • Claude Code integration script (run-for-claude.sh)

Exhaustiveness Checking with Exhaustion

Exhaustion is integral to RestClient.Net's safety guarantees. It's a Roslyn analyzer that ensures you handle every possible case when pattern matching on Result types.

What Happens Without Exhaustion

If you remove a switch arm and don't have Exhaustion installed, the code compiles but may crash at runtime:

// DANGEROUS - compiles but may throw at runtimevaroutput=resultswitch{OkPost(varpost)=>$"Success: {post.Title}",ErrorPost(ResponseErrorPost(varerrorBody,varstatusCode,_))=>$"Error {statusCode}: {errorBody.Message}",// Missing ExceptionErrorPost case - will throw at runtime if an exception occurs!};

What Happens With Exhaustion

With Exhaustion installed, the compiler catches this at build time:

error EXHAUSTION001: Switch on Result is not exhaustive; Matched: Ok<Post, HttpError<ErrorResponse>>, Error<Post, HttpError<ErrorResponse>> with ErrorResponseError<ErrorResponse> Missing: Error<Post, HttpError<ErrorResponse>> with ExceptionError<ErrorResponse> 

diagram

Your build fails until you handle all cases. This is the difference between runtime crashes and compile-time safety.

Installing Exhaustion Without RestClient.Net

Add it to your project:

dotnet add package Exhaustion

Exhaustion works by analyzing sealed type hierarchies in switch expressions and statements. When you match on a Result<TSuccess, HttpError<TError>>, it knows there are exactly three possible cases:

  • Ok<TSuccess, HttpError<TError>> - Success case
  • Error<TSuccess, HttpError<TError>> with ErrorResponseError<TError> - HTTP error response
  • Error<TSuccess, HttpError<TError>> with ExceptionError<TError> - Exception during request

If you don't handle all three, your code won't compile.

Progress Reporting

You can track upload progress with ProgressReportingHttpContent. This example writes to the console when there is a progress report.

varfileBytes=awaitFile.ReadAllBytesAsync("document.pdf");usingvarcontent=newProgressReportingHttpContent(fileBytes,progress:(current,total)=>Console.WriteLine($"Progress: {current}/{total} bytes ({current*100/total}%)"));varhttpClient=httpClientFactory.CreateClient();varresult=awaithttpClient.PostAsync<UploadResponse,string>(url:"https://api.example.com/upload".ToAbsoluteUrl(),requestBody:content,deserializeSuccess:(r,ct)=>r.Content.ReadFromJsonAsync<UploadResponse>(ct),deserializeError:(r,ct)=>r.Content.ReadAsStringAsync(ct));

Mocking with Delegating Handlers for Tests

Mock HTTP calls by inheriting from DelegatingHandler and overriding SendAsync (see Microsoft docs):

classMockHandler:DelegatingHandler{protectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){varresponse=(request.Method.Method,request.RequestUri?.PathAndQuery)switch{("GET","/posts/1")=>newHttpResponseMessage(HttpStatusCode.OK){Content=JsonContent.Create(newPost(1,1,"Title","Body"))},("POST","/posts")=>newHttpResponseMessage(HttpStatusCode.Created){Content=JsonContent.Create(new{Id=101})}, _ =>newHttpResponseMessage(HttpStatusCode.NotFound)};returnTask.FromResult(response);}}// Create HttpClient with mock handlervarhttpClient=newHttpClient(newMockHandler());// Make calls - they hit the mock handlervarresult=awaithttpClient.GetAsync(url:"https://api.example.com/posts/1".ToAbsoluteUrl(),deserializeSuccess:DeserializePost,deserializeError:DeserializeError);// Result contains mocked dataresultswitch{OkPost(varpost)=>Console.WriteLine($"Got mocked post: {post.Title}"),ErrorPost(varerror)=>Console.WriteLine($"Error: {error.StatusCode}")};

Upgrading from RestClient.Net 6.x

You can continue to use the V6 IClient interface with RestClient .Net 7. RestClient.Net 7 is a complete rewrite with a functional architecture. For existing v6 users, RestClient.Net.Original provides a polyfill that implements the v6 IClient interface using v7 under the hood.

Using the Polyfill

Install both packages:

dotnet add package RestClient.Net.Original dotnet add package RestClient.Net.Abstractions --version 6.0.0

Use Client to maintain v6 compatibility while benefiting from v7's improvements:

usingRestClient.Net;varclient=newClient(httpClientFactory,baseUrl:"https://api.example.com".ToAbsoluteUrl(),defaultRequestHeaders:newHeadersCollection(),throwExceptionOnFailure:true);// Continue using your existing v6 IClient extension methodsvaruser=awaitclient.GetAsync<User>("users/123");varcreated=awaitclient.PostAsync<User,CreateUserRequest>(newUser,"users");varupdated=awaitclient.PutAsync<User,UpdateUserRequest>(updateUser,"users/123");vardeleted=awaitclient.DeleteAsync("users/123");

This approach allows gradual migration. You can keep your existing implementations working while incrementally adopting v7's functional patterns.

About

The safest way to make REST calls in C# with an MCP Generator

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 8

Languages