
This article is part of a series called Apizr:
- Apizr – Part 1: A Refit based web api client, but resilient
- Apizr – Part 2: Resilient core features (this one)
- Apizr – Part 3: More advanced features
- Apizr – Part 4: Requesting with Mediator pattern
- Apizr – Part 5: Requesting with Optional pattern
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
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.
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
[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.
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
- Try to fetch it from web first
- 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
- Try to load it from cache first
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) orbuilder => builder.WithCacheHandler(() => new AkavacheCacheHandler())
- The extended way:
builder => builder.WithCacheHandler<MonkeyCacheHandler>()
(1)(2) orbuilder => 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.
services.UseRepositoryCache();
[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