Apizr – Part 2: Resilient core features

apizr2

This article is part of a series called Apizr:

In Part 1, we’ve seen basic requesting features offered by Apizr. Some classic and well known request designs if you get some Refit skills, plus some built-in CRUD exclusive apis.

In this Part 2, we’ll go through what exactly Apizr has to offer beyond its requesting main features, to reach our goal of something easily resilient.

Here are some of its core features:

  • Connectivity checking
  • Authenticating
  • Policing
  • Prioritizing
  • Caching
  • Mapping
  • Logging
OPTIONSBUILDER

While initializing, Apizr provides a fluent way to configure many things with something called OptionsBuilder.

Each initialization approach comes with its OptionsBuilder optional parameter, no matter if you’re a static activist or a MS DI ninja or a Shiny addict.

It will be something like:

  • The static way: var reqResManager = Apizr.For<IReqResService>(builder => builder.SomeOptions(someParameters));
  • The MS DI way: services.AddApizrFor<IReqResService>(builder => builder.SomeOptions(someParameters));
  • The Shiny way: services.UseApizrFor<IReqResService>(builder => builder.SomeOptions(someParameters));

As Apizr exposes many overrides of its builder methods, I’ll limit my examples to the most simple of it.

For Shiny users, you’d better read each Bonus section as it tells you almost everything is ready to go without doing anything more.

CONNECTIVITY

Apizr can check network connectivity for you, right before sending any request.

It will throw an ApizrException with an IOException as InnerException in case of network failure, witch you can handle globally by showing a snack bar info or whatever.

This way, your viewmodels are kept light and clear of it.

To activate this feature you’ll have to implement the IConnectivityHandler interface:

public class MyConnectivityHandler : IConnectivityHandler
{
    public bool IsConnected()
    {
        // Check connectivity here
    }
}

Then provide it to Apizr with the options builder like:

  • The static way: builder => builder.WithConnectivityHandler(new MyConnectivityHandler())
  • The extended way*: builder => builder.WithConnectivityHandler<MyConnectivityHandler>()

*You have to register your connectivity handler implementation into your container as it will be resolved as IConnectivityHandler by Apizr

BONUS
If you guys are using Shiny with Apizr.Integrations.Shiny package installed, make sure to initialize Apizr calling services.UseApizr[Whatever] instead of services.AddApizr[Whatever].
Calling services.UseApizr[Whatever] will wrap up automatically Apizr internal connectivity handler with the Shiny connectivity checker. No implementation and registration needed!
AUTHENTICATING
As Apizr is based on Refit, you can decorate your authenticated apis like so:
[WebApi("https://httpbin.org/")]
public interface IHttpBinService
{
    [Get("/bearer")]
    [Headers("Authorization: Bearer")]
    Task<HttpResponseMessage> AuthBearerAsync();
}

Then you should deal with how to provide the authentication token, following the Refit documentation and its RefitSettings.

Hopefully, Apizr provides a DelegatingHandler to manage the authentication workflow.

Here is its SendAsync method for information:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    HttpRequestMessage clonedRequest = null;
    string token = null;

    // See if the request has an authorize header
    var auth = request.Headers.Authorization;
    if (auth != null)
    {
        // Authorization required! Get the token from saved settings if available
        _logHandler.Write($"Apizr - {GetType().Name}: Authorization required with scheme {auth.Scheme}");
        token = this.GetToken();
        if (!string.IsNullOrWhiteSpace(token))
        {
            // We have one, then clone the request in case we need to re-issue it with a refreshed token
            _logHandler.Write($"Apizr - {GetType().Name}: Saved token will be used");
            clonedRequest = await this.CloneHttpRequestMessageAsync(request);
        }
        else
        {
            // Refresh the token
            _logHandler.Write($"Apizr - {GetType().Name}: No token saved yet. Refreshing token...");
            token = await this.RefreshTokenAsync(request).ConfigureAwait(false);
        }

        // Set the authentication header
        request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
        _logHandler.Write($"Apizr - {GetType().Name}: Authorization header has been set");
    }

    // Send the request
    _logHandler.Write($"Apizr - {GetType().Name}: Sending request with authorization header...");
    var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

    // Check if we get an Unauthorized response with token from settings
    if (response.StatusCode == HttpStatusCode.Unauthorized && auth != null && clonedRequest != null)
    {
        _logHandler.Write($"Apizr - {GetType().Name}: Unauthorized !");

        // Refresh the token
        _logHandler.Write($"Apizr - {GetType().Name}: Refreshing token...");
        token = await this.RefreshTokenAsync(request).ConfigureAwait(false);

        // Set the authentication header with refreshed token 
        clonedRequest.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
        _logHandler.Write($"Apizr - {GetType().Name}: Authorization header has been set with refreshed token");

        // Send the request
        _logHandler.Write($"Apizr - {GetType().Name}: Sending request again but with refreshed authorization header...");
        response = await base.SendAsync(clonedRequest, cancellationToken).ConfigureAwait(false);
    }

    // Clear the token if unauthorized
    if (response.StatusCode == HttpStatusCode.Unauthorized)
    {
        token = null;
        _logHandler.Write($"Apizr - {GetType().Name}: Unauthorized ! Token has been cleared");
    }

    // Save the refreshed token if succeed or clear it if not
    this.SetToken(token);
    _logHandler.Write($"Apizr - {GetType().Name}: Token saved");

    return response;
}

The workflow:

  • We check if the request needs to be authenticated
  • If so, we try to load a previously saved token
    • If there’s one, we clone the request in case we need to re-issue it with a refreshed token (as token could be rejected server side)
    • If there’s not, we ask for a refreshed one (launching your signin feature and waiting for the resulting token)
  • We set the authentication header with the token
  • We finally send the request
  • We check if we get an Unauthorized response
    • If so and if it was sent with a saved token, we ask for a refreshed one (launching your signin feature and waiting for the resulting token)
    • We set the authentication header of the cloned request with the refreshed token
    • We send the cloned request
  • We save the token if succeed or clear it if not
  • We return the response

To activate this feature, you have to tell it to Apizr with the options builder, calling:

  • The static way: builder => builder.WithAuthenticationHandler<MySettingsService, MySignInService>(Settings.Current, settings => settings.Token, new MySignInService(), signInService => signInService.SignInAsync)
  • The extended way*: builder => builder.WithAuthenticationHandler<ISettingsService, ISignInService>(settings => settings.Token, signInService => signInService.SignInAsync)

*You have to register both settings and signin/token services into your container as it will be resolved by Apizr

settings.Token should be a public string property, saved locally on device.

signInService.SignInAsync should be a method taking an HttpRequestMessage parameter and returning a refreshed access token (basically your login flow).

POLICING

Apizr comes with a Policy attribute to apply some policies on apis, handled by Polly.

You’ll find also policy attributes dedicated to CRUD apis like CreatePolicy, ReadPolicy and so on…

Polly will help you to manage some retry scenarios but can do more. Please refer to its official documentation if you’d like to know more about it.

Here is a simple example of using it:

[assembly:Policy("TransientHttpError")]
namespace Apizr.Sample.Api
{
    [WebApi("https://reqres.in/api")]
    public interface IReqResService
    {
        [Get("/users")]
        Task<UserList> GetUsersAsync();
    }
}

Here I’m using it at assembly level, telling Apizr to apply TransiantHttpError policy on every apis.

Then we define what actually is the TransiantHttpError policy:

var registry = new PolicyRegistry
{
    {
        "TransientHttpError", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        }, LoggedPolicies.OnLoggedRetry).WithPolicyKey("TransientHttpError")
    }
};

We have to register our policy with the same name used by the Policy attribute decorating our apis.

TransiantHttpError policy is actually provided by Polly itself, so we jsut call its HttpPolicyExtensions.HandleTransientHttpError() method.

I’m also giving here an OnLoggedRetry method provided by Apizr, so I coud get some logging outputs when Polly comes in the party in case of handled failures.

PolicyRegistry is where you register all your named policies to be used by Polly thanks to attribute decoration, TransiantHttpError is just an example.

Then you have to provide the registry to Apizr:

  • The static way: builder => builder.WithPolicyRegistry(registry)
  • The extended way*: services.AddPolicyRegistry(registry);

*Just register your registry as usual, outside of  any Apizr initialization. Apizr will resolve it.

PRIORITIZING

Apizr use Fusillade to offer some api priority management on every calls.

To be short, Fusillade is about:

  • Auto-deduplication of relevant requests
  • Request Limiting
  • Request Prioritization
  • Speculative requests

Please refer to its official documentation if you’d like to know more about it.

While sending a request managed by Apizr, it optionally ask you about a priority level to apply.

Something like: var myResult = await _myApiManager.ExecuteAsync(api => api.GetSomethingAsync(), Priority.Background);

By default, everything is UserInitiated.

CACHING

Apizr comes with a CacheIt attribute witch activate result data caching at any level (all Assembly apis, interface apis or specific api method).

namespace Apizr.Sample.Api
{
    [WebApi("https://reqres.in/api")]
    public interface IReqResService
    {
        [Get("/users"), CacheIt(CacheMode.GetAndFetch, "01:00:00")]
        Task<UserList> GetUsersAsync();

        [Get("/users/{userId}"), CacheIt(CacheMode.GetOrFetch, "1.00:00:00")]
        Task<UserDetails> GetUserAsync([CacheKey] int userId, CancellationToken cancellationToken);
    }
}

You’ll find also cache attributes dedicated to CRUD apis like CacheRead and CacheReadAll, so you could define cache settings at any level (all Assembly apis, interface apis or specific CRUD method).

namespace Apizr.Sample.Api.Models
{
    [CrudEntity("https://reqres.in/api/users", typeof(int), typeof(PagedResult<>))]
    [CacheReadAll(CacheMode.GetAndFetch, "01:00:00")]
    [CacheRead(CacheMode.GetOrFetch, "1.00:00:00")]
    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; }
    }
}

Both approches here (classic and CRUD) define the same thing about cache life time and cache mode.

Life time is actually a TimeSpan string representation witch is parsed then. Its optional and if you don’t provide it, the default cache provider settings will be applyed.

Cache mode could be set to:

  • GetAndFetch (default): the result is returned from request if it succeed, otherwise from cache if there’s some data already cached. In this specific case of request failing, cached data will be wrapped with the original exception into an ApizrException thrown by Apizr, so don’t forget to catch it.
  • GetOrFetch: the result is returned from cache if there’s some data already cached, otherwise from the request.

In both cases, cached data is updated after each successful request call.

You also can define global caching settings by decorating the assembly or interface, then manage specific scenarios at method level. Apizr will apply the lower level settings it could find.

Back to my example, I’m saying:

  • When getting all users, let’s admit we could have many new users registered each hour, so:
    • Try to fetch it from web first
      • if fetch failed, try to load it from previous cached result
      • if fetch succeed, update cached data but make it expire after 1 hour
  • When getting a specific user, let’s admit its details won’t change so much each day, so:
    • Try to load it from cache first
      • if no previous cached data or cache expired after 1 day, fetch it and update cached data but make it expire after 1 day

Ok so after api attribute decorations, you also have to provide an implementation of ICacheHandler interface to activate the caching feature.

Fortunately, Apizr offers some integration NuGet packages like Apizr.Integrations.MonkeyCache and Apizr.Integrations.Akavache witch do it for you.

After installing one of it, you should be able to provide it to Apizr:

  • The static way: builder => builder.WithCacheHandler(() => new MonkeyCacheHandler(Barrel.Current))(1) or  builder => builder.WithCacheHandler(() => new AkavacheCacheHandler())
  • The extended way: builder => builder.WithCacheHandler<MonkeyCacheHandler>()(1)(2) or builder => builder.WithCacheHandler<AkavacheCacheHandler>()

(1) With MonkeyCache, don’t forget to set a value to Barrel.ApplicationId. (2) With MonkeyCache, you also have to register Barrel.Current as IBarrel into your container.

From here, you should be good to go, like this GetAndFetch call example:

IList<User>? users = null;
try
{
    var userList = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync());
    users = userList?.Data;
}
catch (ApizrException<UserList> e)
{
    users = e.CachedResult?.Data;
}
finally
{
    if (users != null && users.Any())
        Users = new ObservableCollection<User>(users);
}

Of course you can clear your cache manually with the ClearCacheAsync method exposed by the manager.

BONUS
If you guys are using Shiny with Apizr.Integrations.Shiny package installed, make sure to initialize Apizr calling services.UseApizr[Whatever] instead of services.AddApizr[Whatever].
Calling services.UseApizr[Whatever] will wrap up automatically Apizr internal cache handler with the Shiny caching system. No implementation and registration needed, neither caching integration packages!
Well, actually you still have to tell Shiny witch cache type you want to use, like for example: services.UseRepositoryCache();
MAPPING
Apizr comes with a MappedWith attribute telling Apizr to map api object with model object, useful when you want to play with DTO design pattern.
You’ll find another MappedCrudEntity attribute dedicated to CRUD apis, coming with auto-registration capabilities in case of access restricted to only local client model for attribute decoration.
We could get a minimal DTO like:
[MappedWith(typeof(User))]
public class UserDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
}

We just mapped our DTO with our User model class thanks to the attribute.

We still have to define the mapping itself:

public class UserUserDTOProfile : Profile
{
    public UserUserDTOProfile()
    {
        CreateMap<User, UserDTO>()
            .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.FirstName));

        CreateMap<UserDTO, User>()
            .ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.Name));
    }
}

Then, to use mapping feature, we have to provide an implementation of IMappingHandler interface.

Fortunately, Apizr offers an integration NuGet package called Apizr.Integrations.AutoMapper to integrate with… AutoMapper obviously.

Once installed, we should be able to provide it to Apizr.

The static way:

  • First create a MapperConfiguration with your profiles: var config = new MapperConfiguration(cfg => { cfg.AddMaps(myAssemblyContainingProfiles); });
  • Then provide the mapping handler: builder => builder.WithMappingHandler(new AutoMapperMappingHandler(config.CreateMapper()))

The extended way:

  • First register AutoMapper as you used to: services.AddAutoMapper(myAssemblyContainingProfiles);
  • Then provide the mapping handler: builder => builder.WithMappingHandler<AutoMapperMappingHandler>()

From here you should be able to play with auto-mapped data, like for example:

var user = await _reqResManager.ExecuteAsync<User>((api, mapper) => api.GetUserAsync(1));

In this example, a UserDTO will be received by Apizr but mapped to User just before returning the result.

LOGGING

Apizr comes with a LogIt attribute witch activate some logging info about what’s happening into it like execution steps and traffic traces.

While decorating your api interface, you can also control logging verbosity like:

[WebApi("https://reqres.in/api"), LogIt(HttpMessageParts.None, ApizrLogLevel.Low)]
public interface IReqResService
{
    [Get("/users")]
    Task<Result> GetUsersAsync();
}

or while initializing with the fluent builder:

builder => builder.WithLoggingVerbosity(HttpMessageParts.None, ApizrLogLevel.Low)

Here I’m turning the logger to no traffic trace and low Apizr execution steps logging infos.

If you do not specify any verbosity, default are all traffic traces and high Apizr log level.

Also, if you do not provide any log handler but ask for logging with the attribute, everything will be console logged.

As you may understand, you can provide your own log handler, implementing the ILogHandler interface and registering it. This, is the console one registered by default:

public class DefaultLogHandler : ILogHandler
{
    public void Write(string message, string description = null, params (string Key, string Value)[] parameters)
    {
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine(message);

        if(!string.IsNullOrWhiteSpace(description))
            stringBuilder.AppendLine(description);

        foreach (var parameter in parameters)
        {
            stringBuilder.AppendLine($"{parameter.Key}: {parameter.Value}");
        }

        var builtMessage = stringBuilder.ToString();

        Console.WriteLine(builtMessage);
    }
}

To register your own implementation, simply use the fluent options builder while initializing Apizr like:

  • The static way: builder => builder.WithLogHandler(new MyLogHandler())
  • The extended way*: builder => builder.WithLogHandler<MyLogHandler>()

*You have to register your log handler implementation into your container as it will be resolved as ILogHandler by Apizr

BONUS
If you guys are using Shiny with Apizr.Integration.Shiny package installed, make sure to initialize Apizr calling services.UseApizr[Whatever] instead of services.AddApizr[Whatever].
Calling services.UseApizr[Whatever] will wrap up automatically Apizr internal logger with the Shiny logging system. No implementation and registration needed!
CONCLUSION
In this article we talked about main Apizr core features. But there are still some more.
In the next one, I’ll talk about what we could get more from Apizr in some advanced scenarios.
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

10 + 12 =