Vlad's blog

In programming veritas

Components of good Web API client. Part 1.

leave a comment »

There are a lot of posts and guides devoted to the subject of creating Web API, but most of them are focused on server side part. The client code is often covered superficially making impression that this subject is trivial and is not worth discussing. In this post I describe some important aspects of developing Web API client. The example code written in C# but many concepts are language agnostic.

For the sake of illustration let’s consider JIRA like system that allows to create,delete and update projects.

Resource: a collection of projects
Url: http://localhost:54531/api/projects
HTTP methods: GET,POST

Resource: a project
Url: http://localhost:54531/api/projects/{id}
HTTP methods: GET,PUT,DELETE,PATCH

Example:
Url: http://localhost:54531/api/projects
HTTP method: GET

Response:

[
 {
 "id": "a3260d06-4c19-4422-bf36-d2e90bd5c85b",
 "name": "Project 1",
 "state": "InProgress",
 "created": "2018-01-13T13:15Z",
 "updated": "2018-02-14T15:07Z" 
 }
]

Also API must support paging and data shaping.
Example:
Url: http://localhost:54531/api/projects?pageNumber=1&pageSize=20&fields=id,name

First version

If we follow REST approach our API should be focused on resources which are projects. Let’s start from a collection of projects.
Url: http://localhost:54531/api/projects
HTTP methods: GET,POST
First thing you want in your client API is a method for getting a collection of resources. The first version simple returns a list of projects.

public static async Task Test()
{
    List<ProjectDto> projects = await ProjectsResource.Get();
}

class ProjectsResource
{
    public static Task<List<ProjectDto>> Get()
}

class ProjectDto
{
    public ProjectResource This { get; private set; }

    public string Id { get; set; }
    public string Name { get; set; }
    public DateTime Created { get; set; }
    public DateTime Updated { get; set; }
    public ProjectState State { get; set; }

    public enum ProjectState
    {
        NotStarted,
        InProgress,
        Finished
    }
}

Handle failures in a functional way

When dealing with distributed services you should expect failures. Client API should provide some way how to handle different communication errors. Usually you should wrap client code in try/catch block. I personally prefer more functional style when a method returns Result object that contains data plus error related information.

public static async Task Test()
{
    Result<List<ProjectDto>> getProjectsResult = await ProjectsResource.Get();

    if (getProjectsResult.IsSuccess)
    {
        List<ProjectDto> projects = getProjectsResult.Value;
    }
}

class ProjectsResource
{
    public static Task<Result<List<ProjectDto>>> Get()
}

Result class is a part of Functional Extensions for C# library which is also available on NuGet.

Data shaping

Many APIs support an idea of data shaping in order to minimize traffic travelling from Client of Web API to Server. In our example Client can specify a comma separated list of project’s fields, i.e. http://localhost:54531/api/projects?fields=id,name
When you specify fields list as a string in your code it is easy to make a mistake. Hence it would be a good idea to let a compiler to take care of it.

public static async Task Test()
{
    Result<List<ProjectDto>> getProjectsResult = await ProjectsResource.Get(
        fields => fields.Id().Name());
}

class ProjectsResource
{
    public static Task<Result<List<ProjectDto>>> Get(Action<FieldsBuilder> fields)
}

class FieldsBuilder
{
    public FieldsBuilder Id()
    public FieldsBuilder Name()
    public FieldsBuilder Created()
    public FieldsBuilder Updated()
    public FieldsBuilder State()
}

Paging

When API exposes collection resources it is common to implement paging. This task consists of two sub tasks:

  • Fluent builder for specifying page number and page size
  • Some way of iteration over pages

Paging builder is defined similar to FieldsBuilder.

class PagingBuilder
{
    public PagingBuilder PageNumber(int number) {}
    public PagingBuilder PageSize(int number) {}
}

public static async Task Test()
{
    var getProjectsResult = await ProjectsResource.Get(
        paging => paging.PageNumber(1).PageSize(100),
        fields => fields.Id().Name());
}

In order to provide a mechanism for iteration through pages class PagedResult is introduced.

class PagedResult<T>
{
    public List<T> Items { get; private set; }
    public bool IsEndPage()
    public async Task<Result<PagedResult<T>>> NextPage()
}

So ProjectsResource.Get now returns PagedResult.

class ProjectsResource
{
    public static Task<Result<PagedResult<ProjectDto>>> Get(Action<PagingBuilder> paging, 
Action<FieldsBuilder> fields)
}

Below is the code that demonstrates how to build a request and then iterate through the pages in the response.

public static async Task Test()
{
    Result<PagedResult<ProjectDto>> result = await ProjectsResource.Get(
            paging => paging.PageNumber(1).PageSize(100),
            fields => fields.Id().Name())                ;

    if (result.IsSuccess)
    {
        PagedResult<ProjectDto> page = result.Value;

        while (true)
        {
            foreach (ProjectDto project in page.Items)
            {
                // Do something
            }

            if(page.IsEndPage())
                break;

            Result<PagedResult<ProjectDto>> nextPageResult = await page.NextPage();

            if(nextPageResult.IsFailure)
                throw new Exception($"Failed to get next page: {nextPageResult.Error}");

            page = nextPageResult.Value;
        }
    }
}

Transactions

When you work with collection often you want to read, modify and then commit the changes as one batch. Briefly this functionality can be represented as as a sequence of steps.

  • Read a collection of resources
  • Modify the collection or individual elements
  • Detect changes that need to be send to a remote service
  • Send the changes

The first thing we need to do is to split two activities: iteration over read only collection of elements and iteration over a collection that can be modified. So we need to rename PagedResult to ReadOnlyPagedResult and introduce a new class TransactedPagedResult.

class ReadOnlyPagedResult<T>
{
    public List<T> Items { get; private set; }
    public bool IsEndPage()
    public async Task<Result<PagedResult<T>>> NextPage()
}

class TransactedPagedResult<T>
{
    public List<T> Items { get; private set; }
    public bool IsEndPage()
    public async Task<Result<TransactedPagedResult<T>>> NextPage()
    public Task<Result> SaveChanges()
}

Also there is a new class RequestCollection which is returned by ProjectResource.Get(). It has two methods:

  • Execute() to iterate over read only collection
  • ExecuteForModification() for modification
  • class RequestCollection<T>
    {
        public Task<Result<ReadOnlyPagedResult<T>>> Execute()
        public Task<Result<TransactedPagedResult<T>>> ExecuteForModification()
    }
    
    class ProjectsResource
    {
        public static RequestCollection<ProjectDto> Get(
    Action<PagingBuilder> paging, Action<FieldsBuilder> fields)
    }
    

    Below is an example how we can modify a collection of resources

    public static async Task Test5()
    {
        Result<TransactedPagedResult<ProjectDto>> result = await ProjectsResource.Get(
                paging => paging.PageNumber(1).PageSize(100),
                fields => fields.Id().Name())
            .ExecuteForModification();
    
        if (result.IsSuccess)
        {
            TransactedPagedResult<ProjectDto> page = result.Value;
    
            while (true)
            {
                List<ProjectDto> projectsToDelete = page.Items
    .Where(x => x.State == ProjectDto.ProjectState.Finished).ToList();
    
                // Delete finished projects
                foreach (ProjectDto project in projectsToDelete)
                {
                    page.Items.Remove(project);
                }
    
                List<ProjectDto> projectsWithWrongTimestamp = page.Items
    .Where(x => x.Updated < x.Created).ToList();
    
                // Fix Updated timestamp
                projectsWithWrongTimestamp.ForEach(x => x.Updated = x.Created);
    
                if (page.IsEndPage())
                    break;
    
                Result<TransactedPagedResult<ProjectDto>> 
    nextPageResult = await page.NextPage();
    
                if(nextPageResult.IsFailure)
                    throw new Exception($"Failed to get next page: {nextPageResult.Error}");
    
                page = nextPageResult.Value;
            }
    
            Result saveChangesResult = await page.SaveChanges();
    
            if (saveChangesResult.IsFailure)
            {
                throw new Exception($"Failed to save changes: {saveChangesResult.Error}");
            }
        }
    }
    

     

    Advertisements

Written by vsukhachev

January 29, 2018 at 3:09 am

Posted in Development

Tagged with

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: