Apizr – Part 1: A Refit based web api client, but resilient

apizr1_full

This article is part of a series called Apizr:

Into this Apizr blog post series, I’ll try to demonstrate all features offered by the Apizr library, starting from core and going further with integration packages.

Apizr project was motivated by this 2015 famous blog post about resilient networking with Xamarin. You should read it if you didn’t yet, because it’s old but still valid.

My focus with Apizr was to address at least everything explained into this old article, witch mean:

  • Easy access to restful services
  • Work offline with cache management
  • Handle errors with retry pattern and global catching
  • Handle request priority
  • Check connectivity
  • Fast development time
  • Easy maintenance
  • Reuse existing libraries

But also, some more core features like:

  • Trace http traffic
  • Handle authentication

And more integration/extension independent optional features like:

  • Choose cache, log and connectivity providers
  • Register it as an MS DI extension
  • Integrate with Shiny
  • Map model with DTO
  • Use Mediator pattern
  • Use Optional pattern

This list is not exhaustive, there’s more in Apizr, but what I want is playing with all of it with as less code as we can. I mean I don’t want to think about plumbing things, I just want to plug and play, being sure everything is wired and handled by design or almost.

Inspired by Refit.Insane.PowerPack, I wanted to make it simple to use, mixing attribute decorations and fluent configuration.

Also, I built this lib to make it work with any .Net Standard 2.0 compliant platform, with no references to Xamarin, so we could use it seamlessly from Xamarin.Forms as from .Net Core apps or whatever, with DI goodness or not, with Shiny or not.

LIBRAIRIES

Apizr features are provided by several NuGet packages, depending on what you need, what you want to do, and how you want to achieve this.

Project NuGet
Apizr NuGet
Apizr.Extensions.Microsoft.DependencyInjection NuGet
Apizr.Integrations.Shiny NuGet
Apizr.Integrations.Fusillade NuGet
Apizr.Integrations.Akavache NuGet
Apizr.Integrations.MonkeyCache NuGet
Apizr.Integrations.MediatR NuGet
Apizr.Integrations.Optional NuGet
Apizr.Integrations.AutoMapper NuGet

 

Install the NuGet package of your choice:

  • Apizr package comes with the For and CrudFor static instantiation approach (which you can register in your DI container then)
  • Apizr.Extensions.Microsoft.DependencyInjection package extends your IServiceCollection with AddApizrFor and AddApizrCrudFor registration methods (ASP.Net Core, etc)
  • Apizr.Integrations.Shiny package brings ICacheHandler, ILogHandler and IConnectivityHandler method mapping implementations for Shiny, extending your IServiceCollection with a UseApizr and UseApizrCrudFor registration methods
  • Apizr.Integrations.Fusillade package enables request priority management using Fusillade
  • Apizr.Integrations.Akavache package brings an ICacheHandler method mapping implementation for Akavache
  • Apizr.Integrations.MonkeyCache package brings an ICacheHandler method mapping implementation for MonkeyCache
  • Apizr.Integrations.MediatR package enables request auto handling with mediation using MediatR
  • Apizr.Integrations.Optional package enables Optional result from mediation requests (requires MediatR integration) using Optional.Async
  • Apizr.Integrations.AutoMapper package enables auto mapping for mediation requests (requires MediatR integration and could work with Optional integration) using AutoMapper

Apizr core package make use of well known nuget packages to make the magic appear:

Package Features
Refit Auto-implement web api interface and deal with HttpClient
Polly Apply some policies like Retry, CircuitBreaker, etc…
HttpTracer Trace Http(s) request/response traffic to log it

It also comes with some handling interfaces to let you provide your own services for:

  • Caching with ICacheHandler, which comes with its default VoidCacheHandler (no cache), but also with:
    • AkavacheCacheHandler: Akavache method mapping interface (Integration package referenced above)
    • MonkeyCacheHandler: MonkeyCache method mapping interface (Integration package referenced above)
    • ShinyCacheHandler: Shiny chaching method mapping interface (Integration package referenced above)
  • Logging with ILogHandler, which comes with its default DefaultLogHandler (Console and Debug), but also with:
    • ShinyLogHandler: Shiny logging method mapping interface (Integration package referenced above)
  • Connectivity with IConnectivityHandler, which comes with its default VoidConnectivityHandler (no connectivity check), but also with:
    • ShinyConnectivityHandler: Shiny connectivity method mapping interface (Integration package referenced above)
  • Mapping with IMappingHandler, which comes with its default VoidMappingHandler (no mapping conversion), but also with:
    • AutoMapperMappingHandler: AutoMapper mapping method mapping interface (Integration package referenced above)
GETTING STARTED

This post is an introduction to Apizr so we’ll try to keep it simple.

Please install the Apizr NuGet package or the Apizr.Extensions.Microsoft.DependencyInjection one, depending if you plan to use it the static or the extended way. You definitely can install Apizr.Integrations.Shiny instead if you guys are using Shiny like I do into Xamarin projects.

Now imagine we want to play with the https://reqres.in/ api (ReqRes is a hosted REST Api).

In this scenario, we’d like to get a list of users, calling the /users GET endpoint.

I first copy the sample but real json response from their website, witch is

{
    "page": 2,
    "per_page": 6,
    "total": 12,
    "total_pages": 2,
    "data": [
        {
            "id": 7,
            "email": "michael.lawson@reqres.in",
            "first_name": "Michael",
            "last_name": "Lawson",
            "avatar": "https://reqres.in/img/faces/7-image.jpg"
        },
        {
            "id": 8,
            "email": "lindsay.ferguson@reqres.in",
            "first_name": "Lindsay",
            "last_name": "Ferguson",
            "avatar": "https://reqres.in/img/faces/8-image.jpg"
        },
        {
            "id": 9,
            "email": "tobias.funke@reqres.in",
            "first_name": "Tobias",
            "last_name": "Funke",
            "avatar": "https://reqres.in/img/faces/9-image.jpg"
        },
        {
            "id": 10,
            "email": "byron.fields@reqres.in",
            "first_name": "Byron",
            "last_name": "Fields",
            "avatar": "https://reqres.in/img/faces/10-image.jpg"
        },
        {
            "id": 11,
            "email": "george.edwards@reqres.in",
            "first_name": "George",
            "last_name": "Edwards",
            "avatar": "https://reqres.in/img/faces/11-image.jpg"
        },
        {
            "id": 12,
            "email": "rachel.howell@reqres.in",
            "first_name": "Rachel",
            "last_name": "Howell",
            "avatar": "https://reqres.in/img/faces/12-image.jpg"
        }
    ],
    "support": {
        "url": "https://reqres.in/#support-heading",
        "text": "To keep ReqRes free, contributions towards server costs are appreciated!"
    }
}

and create my C# model classes, pasting it into Quicktype (e.g.), witch gives me after some adjustments:

public class PagedResult<T> where T : class
{
    [JsonProperty("page")]
    public long Page { get; set; }

    [JsonProperty("per_page")]
    public long PerPage { get; set; }

    [JsonProperty("total")]
    public long Total { get; set; }

    [JsonProperty("total_pages")]
    public long TotalPages { get; set; }

    [JsonProperty("data")]
    public IEnumerable<T> Data { get; set; }

    [JsonProperty("support")]
    public Support Support { get; set; }
}

public class User
{
    [JsonProperty("id")]
    public long Id { get; set; }

    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("first_name")]
    public string FirstName { get; set; }

    [JsonProperty("last_name")]
    public string LastName { get; set; }

    [JsonProperty("avatar")]
    public Uri Avatar { get; set; }
}

public class Support
{
    [JsonProperty("url")]
    public Uri Url { get; set; }

    [JsonProperty("text")]
    public string Text { get; set; }
}

Now we get our model in place, we have to define our api interface like so:

namespace YourNamespace
{
    [WebApi("https://reqres.in/api")]
    public interface IReqResService
    {
        [Get("/users")]
        Task<PagedResult<User>> GetUsersAsync();
    }
}

And that’s all!

No interface implementation as Refit will do it for us.

Ok I know there are no specific advantages in this example to use Apizr instead of Refit directly, but I told you: it’s the first most simple example I can write (plus the paging thing), then we’ll add many other things to ensure everything is resilient.

So now what we have to do obviously is to get an instance of our auto-implemented and managed IReqResService.

You can do it the static way by calling var reqResManager = Apizr.For<IReqResService>(); or the extended way from the Startup’s ConfigureServices method:

public override void ConfigureServices(IServiceCollection services)
{
    // Apizr registration
    services.AddApizrFor<IReqResService>();

    // Or Apizr auto registration by assembly scanning
    //services.AddApizrFor(typeof(AnyClassFromServicesAssembly));
    
    // Or with Shiny
    //services.UseApizrFor<IReqResService>();

    // Or with Shiny auto registration by assembly scanning
    //services.UseApizrFor(typeof(AnyClassFromServicesAssembly));
}

and then resolving/injecting IApizrManager<IReqResService> where you need it.

From here, you should be able to play with the api by calling:

var result = await reqResManager.ExecuteAsync(api => api.GetUsersAsync());

// Or if we get a CancellationToken as parameter
//var result = await reqResManager.ExecuteAsync((ct, api) => api.GetUsersAsync(ct), yourToken);

The result here will be a paged result of users. I let the paging thing for further blog post demonstrations, it’s not the point for now.

What if we need to get a specific user? Do it like so!

Copy the json result from ReqRes:

{
    "data": {
        "id": 2,
        "email": "janet.weaver@reqres.in",
        "first_name": "Janet",
        "last_name": "Weaver",
        "avatar": "https://reqres.in/img/faces/2-image.jpg"
    },
    "support": {
        "url": "https://reqres.in/#support-heading",
        "text": "To keep ReqRes free, contributions towards server costs are appreciated!"
    }
}

Create your class models from it:

public class Result<T> where T : class
{
    [JsonProperty("data")]
    public T Data { get; set; }

    [JsonProperty("support")]
    public Support Support { get; set; }
}

Actually as we get our previous model classes already, only the Result generic class was missing.

Finally, update the api interface with the new get method:

[WebApi("https://reqres.in/api")]
public interface IReqResService
{
    [Get("/users")]
    Task<PagedResult<User>> GetUsersAsync();

    [Get("/users/{userId}")]
    Task<Result<User>> GetUserAsync(int userId);
}

and you’re ready to get your specific user!

There’re some many things you can do with couple of attributes, please read the Refit documentation to get a picture.

CRUD

Actually, if we look at the ReqRes api closer, we’ll notice it’s a REST Create Read Update Delete api.

As you may know it, I hate writing code more than once. And CRUD api interface qualify to be written just once.

Of course, we still can define our api interfaces with a Create, Read, Update and Delete method, repeating this operation for each model class we want to play with, but come on, let’s be lazy again!

Fortunately, Apizr provide an ICrudApi interface, so you don’t have to write it yourself:

public interface ICrudApi<T, in TKey, TReadAllResult, in TReadAllParams> where T : class
{
    [Post("")]
    Task<T> Create([Body] T payload);

    [Post("")]
    Task<T> Create([Body] T payload, CancellationToken cancellationToken);

    [Get("")]
    Task<TReadAllResult> ReadAll();

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams);

    [Get("")]
    Task<TReadAllResult> ReadAll([Property("Priority")] int priority);

    [Get("")]
    Task<TReadAllResult> ReadAll(CancellationToken cancellationToken);

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property("Priority")] int priority);

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, CancellationToken cancellationToken);

    [Get("")]
    Task<TReadAllResult> ReadAll([Property("Priority")] int priority, CancellationToken cancellationToken);

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property("Priority")] int priority, CancellationToken cancellationToken);

    [Get("/{key}")]
    Task<T> Read([CacheKey] TKey key);

    [Get("/{key}")]
    Task<T> Read([CacheKey] TKey key, [Property("Priority")] int priority);

    [Get("/{key}")]
    Task<T> Read([CacheKey] TKey key, CancellationToken cancellationToken);

    [Get("/{key}")]
    Task<T> Read([CacheKey] TKey key, [Property("Priority")] int priority, CancellationToken cancellationToken);

    [Put("/{key}")]
    Task Update(TKey key, [Body] T payload);

    [Put("/{key}")]
    Task Update(TKey key, [Body] T payload, CancellationToken cancellationToken);

    [Delete("/{key}")]
    Task Delete(TKey key);

    [Delete("/{key}")]
    Task Delete(TKey key, CancellationToken cancellationToken);
}

Forget about CacheKey and Property attribute, it will be discussed in further blog posts. It’s there to offer well named features you may want to actually use or not.

Let’s focus on CRUD :

  • T and TKey (optional – default: int) meanings are obvious
  • TReadAllResult (optional – default: IEnumerable<T>) is there to handle cases where ReadAll doesn’t return an IEnumerable<T> or derived, but a paged result or whatever generic class
  • TReadAllParams (optional – default: IDictionary<string, object>) is there to handle cases where you don’t want to provide an IDictionary<string, object> for a ReadAll reaquest, but a custom class

All you have to do to get an instance of it about any model class is, for static lovers var userManager = Apizr.CrudFor<User, int, PagedResult<User>>(builder => builder.WithBaseAddress("https://reqres.in/api/users")); or for extensions buddies:

public override void ConfigureServices(IServiceCollection services)
{
    // Apizr CRUD registration
    services.AddApizrCrudFor<User, int, PagedResult<User>>(builder => builder.WithBaseAddress("https://reqres.in/api/users"));
    
    // Or if you use Shiny
    //services.UseApizrCrudFor<User, int, PagedResult<User>>(builder => builder.WithBaseAddress("https://reqres.in/api/users"));
}

Well, as you may notice it I’m telling Apizr to handle a paged result of user when calling ReadAll to suits our previous example with ReqRes. But if default types mentioned above suits to you, you can cut down initialization method to [Whatever]CrudFor<User>(builder => builder.WithBaseAddress("https://reqres.in/api/users")).

As we are here using the built in ICrudApi interface, we can’t obviously decorate it with the WebApi attribute to provide the base address. That’s why we provide it by fluent code at initialization time.

In any case, the instance you’ll get will be of type:IApizrManager<ICrudApi<T, TKey, TReadAllResult, TReadAllParams>> like for our User example IApizrManager<ICrudApi<User, int, PagedResult<User>, IDictionary<string, object>>>.

A long type name for a short implementation, using and maintenance amount of time. No api interface needed, keep focused on model definition and just use it!

Speaking of using:

var result = await userManager.ExecuteAsync(api => api.ReadAll()); 

// Or with a CancellationToken
//var result = await userManager.ExecuteAsync((ct, api) => api.ReadAll(ct), yourToken);

Get it? When dealing with CRUD apis, just initialize Apizr for each model class then call any CRUD method from any manager instance, with no more boilerplate.

BONUS

There’s still something boring me! What if we get a lot of model classes we want to play with the crud thing?

Don’t worry, there’s an attribute for it called CrudEntity!

/!\ This feature is only available with extensions approach, I mean using DI and not using the static one /!\

The idea here is to decorate each model class with this attribute like for our User:

[CrudEntity("https://reqres.in/api/users", typeof(int), typeof(PagedResult<>))]
public class User
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("first_name")]
    public string FirstName { get; set; }

    [JsonProperty("last_name")]
    public string LastName { get; set; }

    [JsonProperty("avatar")]
    public string Avatar { get; set; }

    [JsonProperty("email")]
    public string Email { get; set; }
}

You see? Mostly the same parameters than previously.

Repeat the operation for each your model class you want to get CRUD features with.

Then call the magic:

public override void ConfigureServices(IServiceCollection services)
{
    // Apizr CRUD auto-registration
    services.AddApizrCrudFor(typeof(User));
    
    // Or if you use Shiny
    //services.UseApizrCrudFor(typeof(User));
}

It will scan the assembly containing our User class, looking for any class decorated by the CrudEntity attribute and registering it all for us.

This way, you’ll have to call AddApizrCrudFor just once and then you’re read to use it all just like previously.

CONCLUSION
In this article we introduced Apizr with some simple scenarios thanks to its Refit reference and its core CRUD goodness.
In the next one, I’ll talk about all its core features like logging, caching, retrying, and many more.
You’ll find all sources and samples on GitHub.

JeremyBP

Specialized since 2013 in cross-platform applications development for iOS, Android and Windows, using technologies such as Microsoft Xamarin and Microsoft Azure. Initially focused, since 2005, on development, then administration of Customer Relationship Management systems, mainly around solutions such as Microsoft SharePoint and Microsoft Dynamics CRM.

Related Posts

Leave a comment

three × 2 =