ReqRest

Getting Started

Introduction

This guide is going to teach you how to work with the ReqRest library by showing you, step-by-step, how an API client can be written with ReqRest. The client is going to interact with the JSON Placeholder API created by typicode. JSON Placeholder is an awesome fake REST API which mocks responses and is thus ideal for demo projects. If you want to learn more about it, have a look at the documentation here.

Towards the end of this article, the API client will be able to interact with the /todos and /todos/{id} endpoints. These endpoints deal with the TodoItem resource which looks like this:

{
    "userId": 1,
    "id": 1,
    "title": "Learn about ReqRest",
    "completed": false
}

By using the final client, a user will be able to interact with the API using code similar to this:

JsonPlaceholderClient client = new JsonPlaceholderClient();

// Get the TodoItem with the ID 1.
var (response, resource) = await client.Todos(1).Get().FetchAsync();
resource.Match(
    item => Console.WriteLine($"Task: {item.Title}."),
    ()   => Console.WriteLine($"Unexpected status code: {response.StatusCode}.")
);

Note

This guide will make use of new language features introduced with C# 8.0, namely Nullable Reference Types. If your current development environment doesn't support C# 8.0, simply ignore every ? behind a reference type in the code below.

Installation and Project Setup

Create a new console project and reference both the ReqRest and the ReqRest.Serializers.NewtonsoftJson package, for example via:

dotnet new console -n GettingStarted
dotnet add package ReqRest
dotnet add package ReqRest.Serializers.NewtonsoftJson

Afterwards, create the TodoItem DTO class which is going to be used in the following code.

using Newtonsoft.Json;

public class TodoItem
{
    [JsonProperty("id")]
    public int? Id { get; set; }

    [JsonProperty("userId")]
    public int? UserId { get; set; }

    [JsonProperty("title")]
    public string? Title { get; set; }

    [JsonProperty("completed")]
    public bool? Completed { get; set; }
}

Creating the JsonPlaceholderClient

Independent of what API you are wrapping, you will always have to create a class which inherits from the RestClient class. Such a client is the entry point for creating requests against the REST API.

Create a new class called JsonPlaceholderClient, inherit from the RestClient class and copy the code below.

using System;
using ReqRest;

public sealed class JsonPlaceholderClient : RestClient
{
    private static readonly RestClientConfiguration DefaultConfig = new RestClientConfiguration()
    {
        BaseUrl = new Uri("https://jsonplaceholder.typicode.com"),
    };

    public JsonPlaceholderClient(RestClientConfiguration? configuration = null)
        : base(configuration ?? DefaultConfig) { }
}

As you can see, any RestClient must be configured with a RestClientConfiguration. This configuration holds several important values with the most important one being the BaseUrl. This URL is later on combined with the paths of the API endpoints, e.g. "/todos".

Tip

It is recommended to follow the example above and make the RestClientConfiguration an optional constructor parameter so that a user can pass in a custom configuration, but doesn't have to.

By now, the client can already be instantiated, but it cannot really do anything. As a next step, it will be extended with support for the /todos interface, so that code like client.Todos().[...] becomes possible.

About REST API Interfaces

ReqRest uses the RestInterface class to group requests to the same endpoint. At this point, it makes sense to have a look at which requests the JSON Placeholder API supports for the TodoItem resource.

Endpoint HTTP Method Result
/todos GET TodoItem[]
POST TodoItem
/todos/{id} GET TodoItem
PUT TodoItem
DELETE { }

You can see that the /todos and /todos/{id} endpoints basically form two separate groups with separate, possible requests. ReqRest models these two groups via the aforementioned RestInterface class. Each group gets projected to a single class deriving from RestInterface which then exposes the methods for creating the different requests which can be made against that interface.

In this case, the two classes will be called TodosInterface (for /todos, because this interface deals with multiple items) and TodoInterface (for /todos/{id}, because this interface deals with a single item).

If this sounds complicated, don't be afraid! The code following below will help a lot to understand this API design.

Creating the TodosInterface

Every class inheriting from RestInterface requires at least the following code:

using ReqRest;
using ReqRest.Builders;

public sealed class TodosInterface : RestInterface
{
    internal TodosInterface(RestClient restClient)
        : base(restClient) { }

    // Every RestInterface is responsible of building up the URL which is ultimately going 
    protected override UrlBuilder BuildUrl(UrlBuilder baseUrl) =>
        baseUrl / "todos";
}

Two things are happening here:

First of all, the TodosInterface expects a RestClient instance which immediately gets passed to the base class. This client is required by the interface, because it needs to read out certain values from the client's configuration, e.g. the BaseUrl.

In addition, every class inheriting from RestInterface must override the BuildUrl(UrlBuilder) method. This method is used by ReqRest to build up the final URL for a request against this interface. As you can see, the TodosInterface appends the "todos" string to a given baseUrl. In this example, baseUrl will always come directly from the JsonPlaceholderClient which means that is is going to be https://jsonplaceholder.typicode.com. The BuildUrl method above transforms this URL to https://jsonplaceholder.typicode.com/todos.

Note

The slash in baseUrl / "todos" is syntactic sugar replacing the AppendPath method and simply adds the "todos" string to the URL's path while taking care of correctly formatting the slashes. This works similar to .NET's Path.Combine(string, string) method.

At this point, the TodosInterface can be instantiated and thus integrated into the JsonPlaceholderClient:

using System;
using ReqRest;

public sealed class JsonPlaceholderClient : RestClient
{
    // [...]

    public TodosInterface Todos() =>
        new TodosInterface(this);
}

Now you can write code like new JsonPlaceholderClient().Todos(), but there is no possibility to create any requests with this interface (for example via a Get() method). This is going to change now! Add the Get() method below to the TodosInterface.

using System.Collections.Generic;
using ReqRest;
using ReqRest.Builders;
using ReqRest.Serializers.NewtonsoftJson;

public sealed class TodosInterface : RestInterface
{
    // [...]

    public ApiRequest<IList<TodoItem>> Get() =>
        BuildRequest()
            .Get()
            .Receive<IList<TodoItem>>().AsJson(forStatusCodes: 200);
}

Again, a lot is happening here, but ideally the code speaks for itself.

Get() calls the BuildRequest() method and afterwards calls a few methods to declare that the request uses the GET HTTP method and will receive a list of TodoItems as JSON if the server sends back a response with the status code 200.

Don't be afraid to use IntelliSense and Visual Studio to see the documentation of the called methods above. They will explain in detail what they do exactly.

At this point, the API client is functional for the first time! Go ahead and try it out:

public static async Task Main(string[] args)
{
    var client = new JsonPlaceholderClient();
    var response = await client.Todos().Get().FetchResponseAsync();
    var resource = await response.DeserializeResourceAsync();

    // The API may not always return the status code 200.
    // ReqRest automatically does the checking for you and calls the correct method depending on the actual status code.
    resource.Match(
        todoItems => Console.WriteLine($"Received status code 200. There are {todoItems.Count} items!"),
        ()        => Console.WriteLine($"Received an unexpected status code: {response.StatusCode}.")
    );
}

In the table above, you can see that there is another request that can be made to the API - the POST /todos request. There is one important difference to the GET request above. When making a POST request, it should be possible to pass the data, i.e. the TodoItem to the request.

In the code, this can easily done via a parameter. This means that the TodosInterface can be extended like this:

using System.Collections.Generic;
using ReqRest;
using ReqRest.Builders;
using ReqRest.Serializers.NewtonsoftJson;

public sealed class TodosInterface : RestInterface
{
    // [...]

    public ApiRequest<TodoItem> Post(TodoItem? todoItem) =>
        BuildRequest()
            .PostJson(todoItem)
            .Receive<TodoItem>().AsJson(forStatusCodes: 201);
}

This new method can be used like this:

public static async Task Main(string[] args)
{
    var client = new JsonPlaceholderClient();
    var itemToPost = new TodoItem() { Title = "Try out the new POST method!" };
    var (response, resource) = await client.Todos().Post(itemToPost).FetchAsync();

    resource.Match(
        createdItem => Console.WriteLine($"Created the item. It has the ID {createdItem.Id}."),
        ()          => Console.WriteLine($"POSTing failed with status code: {response.StatusCode}.")
    );
}
Note

The FetchAsync() method in the example above is nothing else but a shortcut for the following:

var response = await client.Todos().Post(itemToPost).FetchResponseAsync();
var resource = await response.DeserializeResponseAsync();

At this point, the TodosInterface is entirely done. It is now possible to create and make every single request to the /todos endpoint using the JsonPlaceholderClient!

Creating the TodoInterface

The next interface to be created is the /todos/{id} interface for interacting with a single TodoItem.

This works similarly to the creation of the TodosInterface, but has a few twists:

using ReqRest;
using ReqRest.Builders;
using ReqRest.Serializers.NewtonsoftJson;
using ReqRest.Http;

public sealed class TodoInterface : RestInterface
{
    private int _id;

    internal TodoInterface(int id, RestClient restClient) 
        : base(restClient)
    {
        _id = id;
    }

    protected override UrlBuilder BuildUrl(UrlBuilder baseUrl) =>
        baseUrl / "todos" / $"{_id}";

    public ApiRequest<TodoItem> Get() =>
        BuildRequest()
            .Get()
            .Receive<TodoItem>().AsJson(200);

    public ApiRequest<TodoItem> Put(TodoItem? todoItem) =>
        BuildRequest()
            .PutJson(todoItem)
            .Receive<TodoItem>().AsJson(200);

    // This is a little bit special.
    // The JSON Placeholder API returns an empty object {} when a DELETE request succeeds. 
    //
    // For demonstration purposes, this code interprets this as 'NoContent'.
    // Normally, NoContent should be used when a request returns an empty HTTP content
    // (which is also typical for a DELETE request).
    public ApiRequest<NoContent> Delete() =>
        BuildRequest()
            .Delete()
            .ReceiveNoContent(200);
}

There are two specialties in this code - the first one is how the ID gets transported into the class. This is ideally done via the constructor (you will soon see why) and then storing it as a field, until it is needed by BuildUrl.

Furthermore, the Delete() method uses the ReceiveNoContent method to declare that it receives the special NoContent type. As you can see, there is no additional method call like AsJson(...) here, because it is not necessary to declare which data format "No Content" has.

Note

There are also other methods similar to ReceiveNoContent available, for example ReceiveString or ReceiveByteArray.

After creating the TodoInterface class, don't forget to add it to the JsonPlaceholderClient:

using System;
using ReqRest;

public sealed class JsonPlaceholderClient : RestClient
{
    // [...]

    public TodoInterface Todos(int id) =>
        new TodoInterface(id, this);
}

Here you can see why the ID should ideally be passed via the constructor - because it allows the client to just pass it forward when creating the interface instance.

When you have two interfaces which interact with the same resource (and a similar endpoint) like in this example, it is a good idea to give the two methods the same name by overloading them. This allows expressive code like this:

client.Todos().[...]    // for accessing /todos
client.Todos(123).[...] // for accessing /todos/123

As you can see, the methods exposed by the client can be directly mapped to the endpoint, just in a "more C#-ish" way.

Using the Client

First of all, congratulations! You have finished your first, fully functional REST API client with ReqRest! Even though it is not complete yet, you can already use it to interact with the APIs TodoItem resource.

Feel free to go ahead and try it out by making some requests and see how it works.

To show a concrete (and more complex) example of how it can be used, the following code section displays a way to delete all items with an ID smaller than 10. This example shows how you can use both ReqRest's response type matching and the plain, old status code checking.

public static async Task Main(string[] args)
{
    var client = new JsonPlaceholderClient();
    var items = await FetchItemsToUpdate(client);

    foreach (var item in items)
    {
        await DeleteItem(client, item);
    }
}

static async Task<IEnumerable<TodoItem>> FetchItemsToUpdate(JsonPlaceholderClient client)
{
    var resource = await client.Todos().Get().FetchResourceAsync();

    // Let ReqRest do the work of checking what gets returned for which status code.
    // Note that Match(...) supports returning values inside of the functions.
    // The result of the function which ultimately gets called also gets returned by Match(...).
    return resource.Match(
        items => items.Where(item => item.Id < 10),
        ()    => throw new Exception("Failed to fetch any TODOs.")
    );
}

static async Task DeleteItem(JsonPlaceholderClient client, TodoItem item)
{
    var id = item.Id.GetValueOrDefault();
    var response = await client.Todos(id).Delete().FetchResponseAsync();
    
    // The response is nothing else but a wrapper around the System.Net.Http.HttpResponseMessage.
    // As such, we have access to the underlying properties, e.g. the StatusCode.
    if (response.StatusCode != HttpStatusCode.OK)
    {
        throw new Exception($"Failed to delete the item with the ID {id}.");
    }

    // Note:
    // Alternatively to checking the response's status code, you could also
    // use FetchResource like in the method above.
    // This leads to a more functional style and lets ReqRest do the status
    // code checking for you, but it is certainly the more "unusual" style
    // and may be harder to understand in the beginning.
    // The code would look like this:

    var result = await client.Todos(id).Delete().FetchResourceAsync();
    result.Match(
        noContent => { /* All good, deleting worked. */ }, 
        ()        => throw new Exception($"Failed to delete the item with the ID {id}.")
    );

    // Or with an alternative to Match:
    if (!result.TryGetValue(out NoContent _))
    {
        throw new Exception($"Failed to delete the item with the ID {id}.");
    }

    // All in all, ReqRest gives you a lot of options to get your task done.
    // It is up to you to choose which flavor you prefer for which scenario.
}

Next Steps

If you got this far, you should have an overview of what ReqRest is capable of. If you are still interested in learning more, feel free to check out the guides here or browse the API documentation.

Back to top Copyright © 2019 ReqRest - Generated with DocFX - Material Theme made by Oscar Vásquez - Impressum (Imprint)