RestEase

repository·master·Indexed 22 days ago

https://github.com/canton7/restease

A type-safe REST API client library for .NET that allows developers to define API endpoints as interfaces. RestEase generates the implementation of these interfaces at runtime or compile-time, simplifying interactions with remote services. It supports various HTTP verbs via attributes like [Get] and [Post], flexible return types including Task<T> and Task<HttpResponseMessage>, and advanced parameter handling for query strings, path segments, and request bodies.

Tokens
12.3K
Snippets
37
Records
48
Agent score
28%

What's inside RestEase

  1. Understand Header Merging and Redefinition Rules

    master

    RestEase allows defining headers in four places: Interface Attributes, Interface Properties, Method Attributes, and Method Parameters. They are merged using these rules:

    1. Interface Merging: Constant and Variable Interface headers are merged. If a header is defined both as an attribute and a property, the request will contain multiple values for that header.
    2. Method Overriding: Method headers replace Interface headers. If a header name exists on both, the method-level definition wins.
    3. Parameter Merging: Method headers and Method Parameters are merged.
    4. Null Values: A header with a null value will not be added to the request, but it can be used to replace/remove a previously defined header of the same name.
  2. Customize the request engine by subclassing IRequester

    master

    RestEase's core logic resides in the IRequester interface. When you call RestClient.For<T>(), a generated class builds a RequestInfo object and passes it to an IRequester.

    You can customize the entire request lifecycle by subclassing the default Requester implementation and providing your instance to RestClient.For<T>. The Requester class is designed to be easily extended by overriding its virtual methods.

  3. Use Path Properties for shared path segments

    master

    If a placeholder (like an accountId) is present in most or all paths of an interface, you can define it as a [Path] property on the interface.

    • Properties must have both a get and a set.
    • If the property name matches the placeholder name, you don't need to specify the name in the attribute.
    • If a method parameter has the same name as a path property, the method parameter takes precedence.
    • You can use [BasePath] if the property segment is at the very start of all paths.
    public interface ISomeApi
    {
        [Path("accountId")]
        int AccountId { get; set; }
    
        [Get("{accountId}/profile")]
        Task<Profile> GetProfileAsync();
    
        [Delete("{accountId}")]
        Task DeleteAsync([Path("accountId")] int accountId);
    }
    
    var api = RestClient.For<ISomeApi>("https://api.example.com/user");
    api.AccountId = 3;
    
    // Requests https://api.example.com/user/3/profile
    var profile = await api.GetProfileAsync();
    
    // Requests https://api.example.com/user/4/profile
    await api.DeleteAsync(4);
  4. Manage HttpClient and RestEase interface lifetimes

    master

    Each RestEase interface implementation creates its own HttpClient instance.

    • For .NET Core 2.1+: HttpClient handles connection pooling as expected. You can create and use interface instances freely.
    • For older .NET versions: Avoid creating many HttpClient instances. Instead, create a single HttpClient and pass it to RestClient.For<T> to share it across multiple interface instances. This is particularly useful if you use properties (like Path or Header properties) that change frequently, as you can create many interface instances sharing one underlying client.
  5. Customize serialization and deserialization via subclasses

    master

    You can completely override how RestEase handles data by subclassing specific serializer/deserializer classes and assigning them to the RestClient properties:

    • ResponseDeserializer: Subclass to control how responses are deserialized. Use ResponseDeserializer.HandlesStrings = true if you want the deserializer to receive raw strings (allowing you to change the default behavior for Task<string> return types).
    • RequestBodySerializer: Subclass to control how request bodies are serialized (used when decorated with [Body(BodySerializationMethod.Serialized)]).
    • RequestQueryParamSerializer: Subclass to control how scalar and collection query parameters are serialized.
    • RequestPathParamSerializer: Subclass to control how path parameters are serialized (required if using [Path(PathSerializationMethod.Serialized)]).
    • QueryStringBuilder: Subclass to control how the collection of query parameters is encoded into a single query string.
  6. Understand Path Construction and Hierarchy

    master

    The final request path is constructed by concatenating three parts:

    1. Base Address (e.g., https://api.example.com)
    2. Base Path (optional, e.g., api/v1)
    3. Method Path (from the [Get("path")] attribute)

    Path Resolution Rules

    • Method Path starts with /: The Base Path is ignored, but the Base Address is kept.
    • Method Path is absolute (starts with http:// or https://): Both Base Address and Base Path are ignored.
    • Base Path is absolute: The Base Address is ignored.

    Configuring Base Address and Base Path

    • Base Address: Can be set via RestClient.For<T>(uri), new RestClient(uri), by passing a configured HttpClient, or using the [BaseAddress(...)] attribute on the interface. If set via RestClient, the attribute is ignored.
    • Base Path: Set using the [BasePath(...)] attribute on the interface.
  7. Reuse a HttpClient for multiple RestEase interfaces

    master

    If you want multiple RestEase interfaces to share the same underlying HttpClient configuration (like a shared BaseAddress), use the UseWithRestEaseClient<T> extension method on an IHttpClientBuilder.

    To use this pattern:

    1. Register a named HttpClient using AddHttpClient("name").
    2. Configure the BaseAddress on that client.
    3. Call .UseWithRestEaseClient<T>() for each interface you want to associate with that client.
    services.AddHttpClient("example")
        .ConfigureHttpClient(x => x.BaseAddress = new Uri("https://api.example.com"))
        .UseWithRestEaseClient<ISomeApi>()
        .UseWithRestEaseClient<ISomeOtherApi>();
  8. Install RestEase via NuGet

    master

    RestEase is available on NuGet. To use it in your project, install the RestEase package.

    If you are using C# 9 or .NET 5 (or higher), it is recommended to also reference RestEase.SourceGenerator to enable compile-time errors and faster execution. This is also required if you are targeting platforms that do not support runtime code generation, such as iOS or .NET Native.

    # Install the main package
    dotnet add package RestEase
    
    # Recommended for .NET 5+ or iOS/.NET Native
    dotnet add package RestEase.SourceGenerator
  9. Use Polly for resilience with RestClient

    master

    To add retry logic or other resilience patterns to your RestEase client, install Microsoft.Extensions.Http.Polly.

    If using RestClient.For<T>() directly, use a PolicyHttpMessageHandler to wrap your IAsyncPolicy<HttpResponseMessage>.

    If using HttpClientFactory with dependency injection, use AddRestEaseClient<T> and chain it with AddPolicyHandler(policy) or use the built-in AddTransientHttpErrorPolicy extension.

    // Direct RestClient usage
    var policy = Policy
        .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
        .RetryAsync();
    
    var api = RestClient.For<ISomeApi>("https://api.example.com", new PolicyHttpMessageHandler(policy));
    
    // HttpClientFactory usage
    services.AddRestEaseClient<ISomeApi>("https://api.example.com")
        .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[] 
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        }));
  10. Cancel requests using CancellationToken

    master

    To enable request cancellation, include a CancellationToken as one of the method parameters in your interface definition.

    Note: If your method returns Task<HttpResponseMessage> or Task<Stream>, the CancellationToken will cancel the request initiation but will not cancel the download of the response body.

    public interface ISomeApi
    {
        [Get("very-large-response")]
        Task<LargeResponse> GetVeryLargeResponseAsync(CancellationToken cancellationToken);
    }
  11. Serialize Path Parameters with custom serializers

    master

    To use custom serialization logic for path parameters (for example, to serialize Enums using their [Display] names), use PathSerializationMethod.Serialized in the [Path] attribute.

    You must also provide a RequestPathParamSerializer implementation when initializing the RestClient.

    public enum MyEnum
    {
        [Display(Name = "first")]
        First,
        [Display(Name = "second")]
        Second,
    }
    
    public interface ISomeApi
    {
        [Get("path/{param}")]
        Task<string> GetAsync([Path(PathSerializationMethod.Serialized)] MyEnum param);
    }
    
    ISomeApi api = new RestClient()
    {
        RequestPathParamSerializer = new StringEnumRequestPathParamSerializer()
    }.For<ISomeApi>("https://api.example.com");
    
    // Requests https://api.example.com/path/first
    await api.GetAsync(MyEnum.First);
  12. Add advanced functionality via Extension Methods

    master

    To provide complex functionality (like multipart form data uploads) while keeping a clean interface, use extension methods. There are two patterns:

    1. Wrapping other methods (Recommended/Testable): Define a method on the interface that is only used by the extension method (e.g., accepting HttpContent). This allows you to unit test the extension logic.
    2. Using IRequester directly (Not Testable): Add an IRequester property to your interface. The extension method can then use this property to build a RequestInfo and call RequestVoidAsync directly. This bypasses the interface attributes.
    // Pattern 1: Wrapping
    public interface ISomeApi
    {
        [Post("upload")]
        Task UploadAsync([Body] HttpContent content);
    }
    
    public static class SomeApiExtensions
    {
        public static Task UploadAsync(this ISomeApi api, byte[] imageData) 
        {
            var content = new MultipartFormDataContent();
            // ... build content ...
            return api.UploadAsync(content);
        }
    }
    
    // Pattern 2: Using IRequester
    public interface ISomeApi
    {
        IRequester Requester { get; }
    }
    
    public static class SomeApiExtensions
    {
        public static Task UploadAsync(this ISomeApi api, byte[] imageData)
        {
            var requestInfo = new RequestInfo(HttpMethod.Post, "upload");
            // ... build requestInfo ...
            return api.Requester.RequestVoidAsync(requestInfo);
        }
    }