Scroll Top

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

apizr1_full

This article is part of a series called Apizr:

Read - Documentation  Browse - Source

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, which 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
  • 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.

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.Extensions.Microsoft.Caching 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 AddApizr, AddApizrFor and AddApizrCrudFor registration methods
  • Apizr.Extensions.Microsoft.Caching package brings an ICacheHandler method mapping implementation for MS Extensions Caching
  • 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.Fusillade package enables request priority management using Fusillade
  • 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…
Microsoft.Extensions.Logging.Abstractions Delegate logging layer to MS Extensions Logging

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:
    • InMemoryCacheHandler & DistributedCacheHandler: MS Extensions Caching methods mapping interface (Integration package referenced above), meaning you can provide any compatible caching engine
    • AkavacheCacheHandler: Akavache methods mapping interface (Integration package referenced above)
    • MonkeyCacheHandler: MonkeyCache methods mapping interface (Integration package referenced above)
  • Logging As Apizr relies on official MS ILogger interface, you may want to provide any compatible logging engine (built-in DebugLogger activated by default)
  • Connectivity with IConnectivityHandler, which comes with its default VoidConnectivityHandler (no connectivity check)
  • Mapping with IMappingHandler, which comes with its default VoidMappingHandler (no mapping conversion), but also with:
    • AutoMapperMappingHandler: AutoMapper mapping methods 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.

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, which 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.), which 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.CreateFor<IReqResService>(); or the extended way from the Startup’s ConfigureServices method:

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

    // Or Apizr auto registration by assembly scanning
    //services.AddApizrManagerFor(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 this!

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 many more things you can do with couple of attributes, please read the Refit documentation and the Apizr documentation to get a picture.

>> Doc article <<

CRUD

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

Well, 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.

But here it is FYI:

public interface ICrudApi<T, in TKey, TReadAllResult, in TReadAllParams> where T : class
{
    #region Create

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

    [Post("")]
    Task<T> Create([Body] T payload, [Context] Context context);

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

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

    #endregion

    #region ReadAll

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

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

    [Get("")]
    Task<TReadAllResult> ReadAll([Property(Constants.PriorityKey)] int priority);

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

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

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

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

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

    [Get("")]
    Task<TReadAllResult> ReadAll([Property(Constants.PriorityKey)] int priority, [Context] Context context);

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

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

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property(Constants.PriorityKey)] int priority, [Context] Context context);

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

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

    [Get("")]
    Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property(Constants.PriorityKey)] int priority, [Context] Context context, CancellationToken cancellationToken);

    #endregion

    #region Read

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

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

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

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

    [Get("/{key}")]
    Task<T> Read([CacheKey] TKey key, [Property(Constants.PriorityKey)] int priority, [Context] Context context);

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

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

    #endregion

    #region Update

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

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

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

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

    #endregion

    #region Delete

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

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

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

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

    #endregion
}

Forget about CacheKey, Property or Conetxt 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.CreateCrudFor<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.AddApizrCrudManagerFor<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]CrudManagerFor<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.

>> Doc article <<

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.AddApizrCrudManagerFor(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.

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

Comments (3)

Hi,
Excellent article!

But I have a question, when I configure services with “public override void ConfigureServices(IServiceCollection services)” and the I try to inject it in my viewmodel with IApizrManager, doesn’t work and an exception UnableToResolveFromRegisteredServices DryIoc.ContainerException is thrown.

When I do it statically, with Apizr.Apizr.For it works.

Any suggestions?

Hi, glad if it’s useful.

Well, this article is now a bit old as many dependencies like Shiny, Prism and DryIoc has been updated with some breaking changes.
Anyway, I would suggest you to open an issue on the github repo so we could help you and others that may get the same exception.
Please provide a full stacktrace and/or a simple reproduction project.
Also, note that Apizr v4 should be released in this falling 2021 with some bug fixes, improvements and new features (and breaking changes), less and up to date dependencies and more registration scenarios.

Thank you very much for your so quick answer.

As soon as I get some time I will open an issue on the github repo.

Leave a comment