Scroll Top

Apizr – Part 2: Resilient core features

apizr2

This article is part of a series called Apizr:

Read - Documentation  Browse - Source

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.CreateManagerFor<IReqResService>(options => options.SomeOptions(someParameters));
  • The extended way: services.AddApizrManagerFor<IReqResService>(options => options.SomeOptions(someParameters));

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

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, which 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: options => options.WithConnectivityHandler(new MyConnectivityHandler())
  • The extended way*: options => options.WithConnectivityHandler<MyConnectivityHandler>()

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

BONUS
You may want to provide just a simple boolean value to check connectivity.
Something like:
options => options.WithConnectivityHandler(() => YourConnectivityBoolean)
AUTHENTICATING
As Apizr is based on Refit, you can decorate your authenticated apis like so (here with bearer authorization):
[WebApi("https://httpbin.org/")]
public interface IHttpBinService
{
    [Get("/bearer")]
    [Headers("Authorization: Bearer")]
    Task<HttpResponseMessage> AuthBearerAsync();
}

Then the authentication worflow could be handled by the provided AuthenticationHandler

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

    var context = request.GetOrBuildPolicyExecutionContext();
    if (!context.TryGetLogger(out var logger, out var logLevel, out _, out _))
    {
        logger = _logger;
        logLevel = _apizrOptions.LogLevel;
    }

    // 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
        logger?.Log(logLevel, $"{context.OperationKey}: Authorization required with scheme {auth.Scheme}");
        token = GetToken();
        if (!string.IsNullOrWhiteSpace(token))
        {
            // We have one, then clone the request in case we need to re-issue it with a refreshed token
            logger?.Log(logLevel, $"{context.OperationKey}: Saved token will be used");
            clonedRequest = await this.CloneHttpRequestMessageAsync(request);
        }
        else
        {
            // Refresh the token
            logger?.Log(logLevel, $"{context.OperationKey}: 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);
        logger?.Log(logLevel, $"{context.OperationKey}: Authorization header has been set");
    }

    // Send the request
    logger?.Log(logLevel, $"{context.OperationKey}: 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)
    {
        logger?.Log(logLevel, $"{context.OperationKey}: Unauthorized !");

        // Refresh the token
        logger?.Log(logLevel, $"{context.OperationKey}: Refreshing token...");
        token = await this.RefreshTokenAsync(request).ConfigureAwait(false);

        // Set the authentication header with refreshed token 
        clonedRequest.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
        logger?.Log(logLevel, $"{context.OperationKey}: Authorization header has been set with refreshed token");

        // Send the request
        logger?.Log(logLevel, $"{context.OperationKey}: 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;
        logger?.Log(logLevel, $"{context.OperationKey}: Unauthorized ! Token has been cleared");
    }

    // Save the refreshed token if succeed or clear it if not
    this.SetToken(token);
    logger?.Log(logLevel, $"{context.OperationKey}: 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 right option:

  • The static way: options => options.WithAuthenticationHandler<MySettingsService, MySignInService>(Settings.Current, settings => settings.Token, new MySignInService(), signInService => signInService.SignInAsync)
  • The extended way*: options => options.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).

>> Doc article <<

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: options => options.WithPolicyRegistry(registry)
  • The extended way*: services.AddPolicyRegistry(registry);

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

>> Doc article <<

PRIORITIZING

With the right package installed, Apizr could use Fusillade to offer some api priority management on 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 defining your api interfaces using Apizr to send a request, you can add an int property param decorated with the provided Property attribute to your methods like:

[WebApi("https://reqres.in/api")]
    public interface IReqResService
    {
        [Get("/users")]
        Task<UserList> GetUsersAsync([Priority] int priority);
    }

You also have to tell Apizr to manage priority with options => options.WithPriorityManagement()

Then, you’ll be good to go with something like: var result = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync((int)Priority.Background));

By default, everything is UserInitiated.

>> Doc article <<

CACHING

Apizr comes with a Cache attribute which 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"), Cache(CacheMode.GetAndFetch, "01:00:00")]
        Task<UserList> GetUsersAsync();

        [Get("/users/{userId}"), Cache(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 which is parsed then. Its optional and if you don’t provide it, the default cache provider settings will be applied.

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 that 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 which do it for you. There’s now Apizr.Extensions.Microsoft.Caching which comes with an implementation for MS Extensions Caching features and all compatible caching engines.

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

The static way:

// MonkeyCache
options => options.WithCacheHandler(() => new MonkeyCacheHandler(Barrel.Current))(1) 

// OR Akavache
options => options.WithCacheHandler(() => new AkavacheCacheHandler())

The extended way:

// MonkeyCache
options => options.WithCacheHandler<MonkeyCacheHandler>()(1)(2) 

// OR Akavache
options => options.WithAkavacheCacheHandler()

// OR MS Caching InMemory
options => options.WithInMemoryCacheHandler()

// OR MS Caching Distributed
options => options.WithDistributedCacheHandler<string>()

(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 before its programmed expiration with one of this method:

// Clear on call to force fetch and update cache
var userList = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync(), true);

// Clear a specific request cache
var succeed = await _reqResManager.ClearCacheAsync(api => api.GetUsersAsync());

// Clear all cache
var succeed = await _reqResManager.ClearCacheAsync();
MAPPING

We first 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))
            .ReverseMap();
    }
}

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: options => options.WithAutoMapperMappingHandler(config.CreateMapper())

The extended way:

  • First register AutoMapper as you used to: services.AddAutoMapper(myAssemblyContainingProfiles);
  • Then provide the mapping handler: options => options.WithAutoMapperMappingHandler()

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

var result = await reqResManager.ExecuteAsync<MinUser, User>((api, user) => 
    api.CreateUser(user, CancellationToken.None), minUser);

Here we give a MinUser typed object to Apizr, which will be mapped to User type just before sending it. Then Apizr will map the User typed result back to MinUser type just before returning it.

>> Doc article <<

LOGGING

Apizr comes with a Log attribute which 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"), Log(HttpTracerMode.ExceptionsOnly, HttpMessageParts.All, LogLevel.Trace)]
public interface IReqResService
{
    [Get("/users")]
    Task<Result> GetUsersAsync();
}

or, while initializing with the fluent builder:

options => options.WithLogging(HttpTracerMode.ExceptionsOnly, HttpMessageParts.All, LogLevel.Trace)

Here I’m asking the logger to log only when an exception occurs, writting all http traffic traces at Trace log level.

Also, if you do not provide any logger but ask for logging anyway, everything will be console logged.

To register a logger, simply use the dedicated option while initializing Apizr.

The static way:

options => options.WithLoggerFactory(LoggerFactory.Create(loggingBuilder =>
{
    loggingBuilder.AddConsole();
    loggingBuilder.AddDebug();
}))

LoggerFactory.Create method is provided by Microsoft.Extensions.Logging which lets you add any compatible logger.

The extended way:

As you use to do it actualy.

loggingBuilder.AddConsole()

wherever in your app you get access to ILoggingBuilder

>> Doc article <<

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.

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 (2)

Hi,
I would like to ask you something about Policing. I tried your example but in the log I read the message:
[Apizr – ICategoryApi.GetCategoriesAsync: Executing request without specific policies]

When I use the attribute on the method, I get the message:
[Apizr – ICategoryApi.GetCategoriesAsync: Found a policy with key TransientHttpError]
[Apizr – ICategoryApi.GetCategoriesAsync: Policy with key TransientHttpError is not of Polly.IAsyncPolicy`1[App3.Models.PagedDtoCollection`1[App3.Models.Category]] type and will be ignored]

Could you please tell me where to find more examples about policing with Apizr ?

Hi,

I have to admit that “Executing request without specific policies” is not really meaningful. I managed to change this useless and misunderstanding message in the upcoming V4.
In v3, actually, it just tells you that it will be executed without any policy dedicated to the current api method (no policy attribute decorating the method). But, it could apply some global one, depending if you asked Apizr to use it, and the Transient is one of it as it handle HttpResponseMessage and not your method return type.
You can check if your global retry policy (aka the Transient one) has been applied by… testing it 🙂 I mean playing request with no internet access and see your policy retries logs into the output.
Check out the Sample from the GitHub repo to get an example.

Leave a comment