Updated Resilient Networking with Xamarin

Rob Gibbons wrote a fantastic blog post back in 2015 on how best to write network requests layers for your Xamarin Apps. I’ve personally used this approach many times, but I felt that it needed updating for 2018, so here it is. A slightly updated approach to resilient networking services with Xamarin. And when I say ‘slightly update’, I honestly mean it’s a minor change!

we-dont-throw

Refit

For those of you who are familiar with Rob’s approach, he uses pulls together a few libraries to create a robust networking layer. One of the critical elements of his strategy is the use of Refit. Refit is a REST library which allows us to interact with remote APIs with minimal boiler-plate code. It makes heavy use of generics and abstractions to define our REST API calls as a C# Interfaces which are then used with am instance HTTPClient to handle all the requests. All serialisation is dealt with for us! I still believe Refit to be a great library to use so we’ll keep this as the core of this pattern.

Let’s have a look at an example interface for use with Refit.

public interface IBeerServiceAPI`
{
    [Get("/beer/")]
    Task GetBeers();
}

We use attributes to define the request type as well as its path (relative to the HTTPClients base URL).

We then define what we expect back from the API and leave Refit to handle making the call, deserialising the response and handing it back to us as a concrete type.

To expand on this, we can add many more types of requests.

[Get("/beer/{id}/")]
Task GetBeerById(string id);

[Post("/beer/")]
Task CreateBeer([Body] Beer beer);

[Delete("/beer/{id}/")]
Task DeleteBeer(string id);

[Put("/beer/{id}/")]
Task UpdateBeer(string id, [Body] Beer beer);

We can now use the interface to make calls to our remote endpoint. I usually place these methods within a class that is unique to the service I’m calling. I’m this example; it’d be a “BeersService.”

//Create new beer item
public async Task<Beer> CreateBeerAsync(Beer beer)
 {
    var apiInstance = RestService.For<IBeerServiceAPI>(Helpers.Constants.BaseUrl);
    return await apiInstance.CreateBeer(beer);
}

//Get by ID
public async Task<Beer> GetBeerByIdAsync(string id)
{
    var apiInstance = RestService.For<IBeerServiceAPI>(Helpers.Constants.BaseUrl);
    return await apiInstance.GetBeerById(id);
}

That’s all it takes for us to starts interacting with a remote API. If you’re wondering how to test this, it’s incredibly easy to swap out implementations with mock services when using this architecture!

Resiliency

Building a resilient networking service requires a few things. We need to understand what our current connectivity looks like, as well as find a solution for caching data locally to ensure our app still ‘works’ in offline situations.

We can achieve both of these tasks by leveraging packages from Motz. He’s created a plugin for checking connectivity status as well as developed a library for caching.

Lets first take a look at connectivity status.

You’ll want to add the Connectivity Plugin nuget package to every client project in the solution as well as the PCL. The following platforms are supported:

  • Xamarin.iOS
  • tvOS (Xamarin)
  • Xamarin.Android
  • Windows 10 UWP
  • Xamarin.Mac
  • .NET 4.5/WPF
  • .NET Core
  • Samsung Tizen

To use the connectivity plugin, we can simple make the following call:

var isConnected = CrossConnectivity.Current.IsConnected;

Caching

Now that we can check for connectivity, we detect that we’re offline. Let’s have a look at how to implement that.

public async Task<List<Beer>> GetBeersAsync()
{
    Handle online/offline scenario
    if (!CrossConnectivity.Current.IsConnected)
    {
        //If no connectivity, we need to fail... :(
        throw new Exception("No connectivity");
    }
    //Create an instance of the Refit RestService for the beer interface.
    var apiInstance = RestService.For<IBeerServiceAPI>(Helpers.Constants.BaseUrl);
    var beers = await apiInstance.GetBeers());

    return beers;
}

Returning no results for most requests isn’t a great solution. We can dramatically improve the user experience by keeping a cache of data to show in offline situations. To implement that, we’re going to use Monkey Cache. To use Monkey Cache, we have to first configure the ApplicationId.  A folder created for your app on disk with the ApplicationId, so you should avoid changing it.

Barrel.ApplicationId = "your_unique_name_here";

Adding Monkey Cache is super simple. First, of, we want to define a key. Think of this as the collection (barrel) name. After that, we implement the necessary logic to handle caching.

public async Task<List<Beer>> GetBeersAsync()
{
    var key = "Beers";

    Handle online/offline scenario
    if (!CrossConnectivity.Current.IsConnected && Barrel.Current.Exists(key))
    {
        //If no connectivity, we'll return the cached beers list.
        return Barrel.Current.Get<List<Beer>>(key);
    }

    //If the data isn't too old, we'll go ahead and return it rather than call the backend again.
    if (!Barrel.Current.IsExpired(key) && Barrel.Current.Exists(key))
    {
        return Barrel.Current.Get<List<Beer>>(key);
    }            

    //Create an instance of the Refit RestService for the beer interface.
    var apiInstance = RestService.For<IBeerServiceAPI>(Helpers.Constants.BaseUrl);
    var beers = await apiInstance.GetBeers());

    //Save beers into the cache
    Barrel.Current.Add(key: key, data: beers, expireIn: TimeSpan.FromHours(5));

    return beers;
}

Polly

Returning to Rob’s original post, we’ll want to add Polly. Polly helps us handle network requests sanely. It allows us to retry, and process failures robustly.

We’re going to use Polly to define a retry logic that forces the service to retry five times, each time waiting twice as long as before.

public async Task<List<Beer>> GetBeersAsync()
{
    var key = "Beers";

    Handle online/offline scenario
    if (!CrossConnectivity.Current.IsConnected && Barrel.Current.Exists(key))
    {
        //If no connectivity, we'll return the cached beers list.
        return Barrel.Current.Get<List<Beer>>(key);
    }

    //If the data isn't too old, we'll go ahead and return it rather than call the backend again.
    if (!Barrel.Current.IsExpired(key) && Barrel.Current.Exists(key))
    {
        return Barrel.Current.Get<List<Beer>>(key);
    }            

    //Create an instance of the Refit RestService for the beer interface.
    var apiInstance = RestService.For<IBeerServiceAPI>(Helpers.Constants.BaseUrl);

    //Use Polly to handle retrying (helps with bad connectivity) 
    var beers = await Policy
        .Handle<WebException>()
        .Or<HttpRequestException>()
        .Or<TimeoutException>()
        .WaitAndRetryAsync
        (
            retryCount: 5,
            sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
        ).ExecuteAsync(async () => await apiInstance.GetBeers());


    //Save beers into the cache
    Barrel.Current.Add(key: key, data: beers, expireIn: TimeSpan.FromSeconds(5));

    return beers;
}

Wrapping Up

This is a great way to implement your networking layer within your apps as it can sit within a .NET Standard library and be used in all your client apps.

If you’d like to see a more real-world example of this approach, then check out the Mobile Cloud Workshop I created with Robin-Manuel. The Xamarin.Forms app uses this approach and, it’s been working very well for us!

Big thanks to Rob for the original post and documenting such a simple solution to complex problem!

Leave a Reply