Skip to content

UruIT/rest-client

Repository files navigation

rest-client

License: MIT GitHub stars

Rest Client is a C# library that allows you to consume an HTTP REST service, mapping HTTP response codes to different C# types allowing you full control over what you expect when calling such service.

Description

This library uses a fluent API that is separated in three parts:

  • Specifies the HTTP method, host, resource and optionally the body (passing in a C# object).
  • Sets up a pipeline of processors that process the response in sequence, building the return type incrementally and compositionally.
  • (Optional) Configures settings of the JSON serializer, both for the request body and the response body.
  • (Optional) Configures other aspects of the HTTP request like headers, TLS certificates, etc.

Basic Usage

We'll describe some sample usages of this library, from simple cases to more complex ones.

In our example, we have an API for managing information about cats. First of all we cant to call the API "GET http://api.catsforall.com/cats/myCatName", allowing us to search cats by name. This API returns JSON data about the cat, for example { "Name": "Whiskers", "Age": 5, "Lives": 9 }, which we want to represent as the Cat C# type.

public class Cat
{
    public string Name { get; set; }

    public int Age { get; set; }

    public int Lives { get; set; }
}	

The API is simple, it either returns a 200 status Code with the JSON body, or it returns a 4xx or 5xx status code.

Using rest-client we can call this API in the following way:

public Cat SearchCatByName(string catName)
{
    return restClient
        .Get<Cat>("http://api.catsforall.com", "/cats/" + catName)            
        .GetResult();
}

Now, what if we want to add a new cat by calling "POST http://api.catsforall.com/cats"? We do so like this:

public void AddCat(Cat cat)
{
    restClient.PostDefault("http://api.catsforall.com", "/cats", cat);
}

We can also do the usual PUT and DELETE calls:

public void UpdateCat(Cat cat)
{
    restClient.PutDefault("http://api.catsforall.com", "/cats/" + cat.Name, cat);
}
public void DeleteCat(string catName)
{
    restClient.DeleteDefault("http://api.catsforall.com", "/cats/" + catName);
}

Managing 404 status codes

Since our API is RESTful, whenever the client requests a resource that doesn't exist the API returns a 404 Not Found status code. However, what if we want to handle that case programatically? With our previous SearchCatByName function, rest-client throws a RestException whenever it encounters a 4xx or 5xx status code, including 404. An example of handling the 404 case would be the following:

public void ShowCatLives(string catName)
{
    string messageToShow = string.Empty;
    try
    {
        var cat = SearchCatByName(catName);
        messageToShow = catName + " has " + cat.Lives + " lives";
    }
    catch (RestException ex)
    {
        if (ex.HttpError.StatusCode == System.Net.HttpStatusCode.NotFound)
        {
            messageToShow = "Cat doesn't exist";
        }
        else
        {
            throw;
        }
    }

    lblCatLives.Text = messageToShow;
}

We can improve this way of handling 404 cases in the following way:

public OptionStrict<Cat> SearchCatByName(string catName)
{
    return restClient
        .Get<OptionStrict<Cat>>("http://api.catsforall.com", "/cats/" + catName)
        .AddProcessors(new OptionAsNotFoundProcessor<Cat>())
        .GetResult();
}

The only thing that changed from the original method is the return type, which changes from Cat to OptionStrict<Cat>, and we added a line AddProcessors(new OptionAsNotFoundProcessor<Cat>()).

Let's talk about the change in the returning type first. OptionStrict<T> is a type from the Csharp-Monad library, it represents a nullable type (which can be either a value type or reference type). A value with type OptionStrict<T> is either empty or has a value. We can use this type to represent our new method: If the API call returns a 200 status code with the JSON body, then we return a Cat value; however, if the API call returns a 404 status code, we return an empty value (represented by Option<Cat>.Nothing).

With this new design, our previous example can be simplified:

public void ShowCatLives(string catName)
{
    var catOpt = SearchCatByName(catName);
    var messageToShow = catOpt.Match(
        Nothing: () => "Cat doesn't exist",
        Just: cat => catName + " has " + cat.Lives + " lives");

    lblCatLives.Text = messageToShow;
}

The Match function pattern matches on the state of the optional. Its implementation is equivalent to this:

public TResult Match<TResult, TValue>(OptionStrict<TValue> opt, Func<TValue, TResult> Just, Func<TResult> Nothing)
{
    return opt.HasValue ? Just(opt.Value) : Nothing();
} 

To make this implementation possible, we need to introduce processors, as we did with AddProcessors(new OptionAsNotFoundProcessor<Cat>())

Processors

rest-client uses a pipeline of processors that check whether they find a specific HTTP status code, and if they find it they return a value of the returning type.

The processor pipeline is the following:

Processor Pipeline

Each processor can take an action with the HTTP response, or pass the response to the next processor in the pipeline. This way we can compose processors to add functionality to our return types based on the HTTP status code we want to handle.

These processors can also incrementally build types using values obtained from the rest of the pipeline. In the above case, OptionAsNotFoundProcessor returns OptionStrict<TResult>, but uses the TResult value returned by SuccessProcessor. We could add new processors on top of OptionAsNotFoundProcessor to extend the returned C# type if we wanted (as we'll see in the next section when handling 4xx and 5xx status codes).

Managing 4xx / 5xx status codes

Like it was stated previously, whenever a 4xx or 5xx status code arrives, rest-client throws an exception. To handle such cases you need to catch a RestException, like this:

public void ShowCatLives(string catName)
{
    string messageToShow = string.Empty;
    try
    {
        var catOpt = SearchCatByName(catName);
        messageToShow =  catOpt.Match(
            Nothing: () => "Cat doesn't exist",
            Just: cat => catName + " has " + cat.Lives + " lives");
    }
    catch (RestException ex)
    {
        messageToShow = "Error with status code " + ex.HttpError.StatusCode + " and message " + ex.Message;
    }

    lblCatLives.Text = messageToShow;
}

However, with rest-client we can handle HTTP errors with C# types without forcing the user to use try-catch blocks for managing control flow.

We can handle such errors by having a return type that determines if there was an error or not; if there was an error it should return specific data about the error (like an error message); if there was no error then it should return the data type we want.

We use EitherStrict<RestBusinessError, TResult> in such case. EitherStrict<TLeft, TRight> is a type from the Csharp-Monad library; it represents an union type, where it either has a TLeft value or a TRight value, and you can check which one is which (for example by the bool IsLeft and bool IsRight properties). In rest-client, we determine TLeft to be the error case, and we represent it with the RestBusinessError type, which is the following:

public enum RestErrorType
{
    ValidationError,
    InternalError
}

public class RestBusinessError
{
    public RestErrorType ErrorType { get; set; }

    public string Message { get; set; }

    public string Details { get; set; }
}

RestErrorType is ValidationError when the API returned a 4xx status code, and InternalError when the API returned a 5xx status code. Message and Details provide additional information about the HTTP error.

With such types, we can then update our previous API call with the following:

public EitherStrict<RestBusinessError, OptionStrict<Cat>> SearchCatByName(string catName)
{
    return restClient
        .Get<EitherStrict<RestBusinessError, OptionStrict<Cat>>>("http://api.catsforall.com", "/cats/" + catName)
        .AddProcessors(new EitherRestErrorProcessor<OptionStrict<Cat>>().Default()
            .AddProcessors(new OptionAsNotFoundProcessor<Cat>()))
        .GetResult();
}

As expected, we changed the return type from OptionStrict<Cat> to EitherStrict<RestBusinessError, OptionStrict<Cat>>.

We also added a new processor, called EitherRestErrorProcessor. Just like with OptionStrict, we need to tell our library how to handle the error status codes and tell him that he should return a RestBusinessError value if he encounters such status codes.

EitherRestErrorProcessor is added at the top of the pipeline, and checks if the response has a 4xx or 5xx status code. If it does then it returns RestBusinessError, if not it delegates the processing to the rest of the pipeline (as seen in the previous section).

We can rewrite our previous method using this new API call:

public void ShowCatLives(string catName)
{
    var catRes = SearchCatByName(catName);
    var messageToShow = catRes.Match(
        Left: err => "Error with status code " + err.ErrorType.ToHttpStatusCode() + " and message " + err.Message,
        Right: catOpt => catOpt.Match(
            Nothing: () => "Cat doesn't exist",
            Just: cat => catName + " has " + cat.Lives + " lives"));

    lblCatLives.Text = messageToShow;
}

Just like OptionStrict, the Match function on EitherStrict uses pattern matching, in this way:

public TResult Match<TLeft, TRight, TResult>(EitherStrict<TLeft, TRight> either, Func<TRight, TResult> Right, Func<TLeft, TResult> Left)
{
    return either.IsLeft ? Left(either.Left) : Right(either.Right);
}

Sample

This library includes a sample application. You can use it to view the usage of this library, as well as for making tests when developing or making changes to this library.

Authors

gonzaw
gonzaw

License

Licensed under the MIT License, Copyright © 2017 UruIT.

See LICENSE for more information.

About

C# REST client that allows mapping of HTTP status codes to different C# types to provide full control over the response

Resources

License

Stars

Watchers

Forks

Packages

No packages published