Scroll Top

Apizr v6.0, Refit’s resilient manager – What’s new

APIZR-V5

Apizr has been updated to v6.0, still getting all the things ready to use for web API client requests, with the more resilience we can, but without the boilerplate.

It relies on Refit, pushing things further by adding a lot more capabilities like fault handling, connectivity check, cache management, authentication handling, log tracing, priority management, file transfer management and so on…

This new v6.0 release brings many enhancements, new features and some fixes, so let’s take a look!

First, let me remind you some of its main features until now with previous v5:

  • Defining configurations at design, registration or request time
  • Working offline with cache management with several cache providers
  • Handling request faults with Polly
  • Managing request priority with Fusillade
  • Checking connectivity
  • Tracing HTTP traffic with HttpTracer
  • Handling authentication
  • Mapping model with DTO with several mapping providers
  • Using Mediator pattern with MediatR
  • Using Optional pattern with Optional.Async
  • Managing files transfers
  • Generating almost everything from Swagger with NSwag

Today, let me introduce you some of its main new features from now with the fresh v6:

  • Handling IApiResponse safe response with data origin information (network or cache), instead of catching exceptions
  • Handling fault exceptions with Microsoft Resilience/Polly Extensions v8+, instead of former Polly v7- Policies
  • Storing headers for further key match use, refreshing its values at request time, redacting its sensitive values while logging
  • Configuring almost everything by providing an IConfiguration instance from Microsoft.Extensions (e.g. from appsetings.json)
  • Generating almost everything thanks to Refitter v1.2+, instead of now deprecated Apizr.Tools.NSwag

If you want it all or just getting started, please read the updated documentation:

Read - Documentation

Feel free to browse code, tests and samples and maybe to submit PRs or ideas:

Browse - Source

Don’t forget the brand new YouTube Channel Playlist about Apizr:

Watch - Tutorials

Starting with this Getting Started video:

Anyway, this post is all about changes only, so let’s see what’s new!

REMEMBER WHERE TO FIND

Apizr features are still provided by several NuGet packages.

It let you the choice to pick those you want, depending on what you need, what you want to do, and how you want to achieve it.

All of it are deeply documented into the doc website.

Managing (Core)

This is the core of Apizr. The first package comes for all static lovers, the second for MS extended dudes. Just pick the one suits to you.

Project NuGet
Apizr
Apizr.Extensions.Microsoft.DependencyInjection
Caching

Pick the caching provider of your choice if you need to cache data.

Project NuGet
Apizr.Extensions.Microsoft.Caching
Apizr.Integrations.Akavache
Apizr.Integrations.MonkeyCache
Handling

Pick some handling packages if you plan to play with its features.

Project NuGet
Apizr.Integrations.Fusillade
Apizr.Integrations.MediatR
Apizr.Integrations.Optional
Mapping

Pick the mapping package of your choice if you want to map data.

Project NuGet
Apizr.Integrations.AutoMapper
Apizr.Integrations.Mapster
Transferring

Pick the transfer package you want if you plan to ulpoad or download files.

Project NuGet
Apizr.Integrations.FileTransfer
Apizr.Extensions.Microsoft.FileTransfer
Apizr.Integrations.FileTransfer.MediatR
Apizr.Integrations.FileTransfer.Optional
Generating

Pick one if you want it all generated and configurated from a swagger url.

Project NuGet
Refitter NuGet
Refitter.SourceGenerator NuGet

refresh HOW TO USE

Apizr could listen to you at design time (definition), registration time (configuration) and request time (execution).

Here is what it looks like now with the updated v6:

At design time, a pretty dummy API definition with some attributes asking things:

// (Polly) Define a resilience pipeline key
// OR use Microsoft Resilience instead
[assembly:ResiliencePipeline("TransientHttpError")]
namespace Apizr.Sample
{
    // (Apizr) Define your web api base url and ask for cache and logs
    [BaseAddress("https://reqres.in/"), 
    Cache(CacheMode.GetAndFetch, "01:00:00"), 
    Log(HttpMessageParts.AllButBodies)]
    public interface IReqResService
    {
        // (Refit) Define your web api interface methods
        [Get("/api/users")]
        Task<UserList> GetUsersAsync([RequestOptions] IApizrRequestOptions options);

        [Get("/api/users/{userId}")]
        Task<UserDetails> GetUserAsync([CacheKey] int userId, [RequestOptions] IApizrRequestOptions options);

        [Post("/api/users")]
        Task<User> CreateUser(User user, [RequestOptions] IApizrRequestOptions options);
    }
}

At registration time, some resilience strategies (if not using Microsoft Resilience)

// (Polly) Create a resilience pipeline (if not using Microsoft Resilience)
var resiliencePipelineBuilder = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(
        new RetryStrategyOptions<HttpResponseMessage>
        {
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(response =>
                    response.StatusCode is >= HttpStatusCode.InternalServerError
                        or HttpStatusCode.RequestTimeout),
            Delay = TimeSpan.FromSeconds(1),
            MaxRetryAttempts = 3,
            UseJitter = true,
            BackoffType = DelayBackoffType.Exponential
        });

An instance of this managed api, the static way:

// (Polly) Add the resilience pipeline with its key to a registry
var resiliencePipelineRegistry = new ResiliencePipelineRegistry<string>();
resiliencePipelineRegistry.TryAddBuilder<HttpResponseMessage>("TransientHttpError", 
    (builder, _) => builder.AddPipeline(resiliencePipelineBuilder.Build()));

// (Apizr) Get your manager instance
var reqResManager = ApizrBuilder.Current.CreateManagerFor<IReqResService>(
    options => options
        // With a logger
        .WithLoggerFactory(LoggerFactory.Create(loggingBuilder =>
            loggingBuilder.Debug()))
        // With the defined resilience pipeline registry
        .WithResiliencePipelineRegistry(resiliencePipelineRegistry)
        // And with a cache handler
        .WithAkavacheCacheHandler());

or the same, the extended way:

// (Logger) Configure logging the way you want, like
services.AddLogging(loggingBuilder => loggingBuilder.AddDebug());

// (Apizr) Add an Apizr manager for the defined api to your container
services.AddApizrManagerFor<IReqResService>(
    options => options
        // With a cache handler
        .WithAkavacheCacheHandler()
        // If using Microsoft Resilience
        .ConfigureHttpClientBuilder(builder => builder
            .AddStandardResilienceHandler()));

// (Polly) Add the resilience pipeline (if not using Microsoft Resilience)
services.AddResiliencePipeline<string, HttpResponseMessage>("TransientHttpError",
    builder => builder.AddPipeline(resiliencePipelineBuilder.Build()));
...

// (Apizr) Get your manager instance the way you want, like
var reqResManager = serviceProvider.GetRequiredService<IApizrManager<IReqResService>>();

Then at request time, the very final tunning:

var userList = await reqResManager.ExecuteAsync((opt, api) => api.GetUsersAsync(opt), options => options.WithPriority(Priority.UserInitiated));

This request will be managed by Apizr with the defined resilience strategies, data cached, prioritized and logged with HTTP traces.

Apizr has a lot more to offer, just read the doc!

DISCOVER WHAT IS NEW

Let’s focus now on some new features coming with v6.

For readability, I’m mostly showing code from the static approach, but you’ll find the same with the service collection extensions approach in the documentation.

handling exceptions

Now we can handle Refit’s IApiResponse safe response instead of catching exceptions.

By designing your api interface methods with Task<IApiResponse>Task<IApiResponse<T>>, or Task<ApiResponse<T>>, Refit traps any ApiException raised by the ExceptionFactory when processing the response, and any errors that occur when attempting to deserialize the response to ApiResponse<T>, and populates the exception into the Error property on ApiResponse<T> without throwing the exception.

Then, Apizr will wrap the Refit’s ApiResponse<T> into its own ApizrResponse<T>, add some cached data to it if any, then return it as the final response.

Here is an example:

// We wrap by design the response into an IApiResponse<T> provided by Refit
[BaseAddress("https://reqres.in/api")]
public interface IReqResService
{
    [Get("/users")]
    Task<IApiResponse<UserList>> GetUsersAsync([RequestOptions] IApizrRequestOptions options);
}

...

// Then we can handle the IApizrResponse<T> response comming from Apizr
var apizrResponse = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync());

// Log potential errors and maybe inform the user about it
if(!apizrResponse.IsSuccess)
{
   _logger.LogError(apizrResponse.Exception);
    Alert.Show("Error", apizrResponse.Exception.Message);
}

// Use the data, no matter the source
if(apizrResponse.Result?.Data?.Any() == true)
{
    Users = new ObservableCollection<User>(apizrResponse.Result.Data);

    // Inform the user that data comes from cache if so
    if(apizrResponse.DataSource == ApizrResponseDataSource.Cache)
        Toast.Show("Data comes from cache");
}
Handling request faults

Apizr v6 now supports only Polly Extensions/Microsoft Resilience v8+ with Strategies/Pipelines/Registry, instead of former Polly v7- with Policies. That’s a breaking change you should be aware of if you’re coming from Apizr v5-, meaning that upgrading to Apizr v6+ with existing Polly v7- Policies in place involves refactoring Policies to Strategies. You’ll get more details about it into the breaking change docmentation page.

  • With extended registration only and once referenced Microsoft.Extensions.Http.Resilience optional package,  you can tell Apizr to let Microsoft Resilience handle request faults for you.

In such scenario, you just have to register it like so:

services.AddApizrManagerFor<IReqResService>(
    options => options.ConfigureHttpClientBuilder(builder => builder
        .AddStandardResilienceHandler()));

This will handle common http errors with a pre-configured Polly pipeline. You can even adjust its default settings if needed with some provided overloads.

  • With both extended and static registrations, witout any additional package, you can still handle errors the tunned way you want and not only for http errors:
  1. We’re now talking about ResiliencePipeline attribute, instead of the former Policy one:
// (Polly) Define a resilience pipeline key 
// OR use Microsoft Resilience instead
[assembly:ResiliencePipeline("TransientHttpError")]
namespace Apizr.Sample
{
    [BaseAddress("https://reqres.in/")]
    public interface IReqResService
    {
        [Get("/api/users")]
        Task<UserList> GetUsersAsync([RequestOptions] IApizrRequestOptions options);
    }
}

2. The same with the Policy builder itself replaced by the Pipeline builder:

// (Polly) Add some strategies to a pipeline
var resiliencePipelineBuilder = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
        MaxRetryAttempts = 3,
        Delay = TimeSpan.Zero,
    });

3.a. (Static only) And then, registering pipelines with the same attribute key, then providing the registry:

// (Polly) Add the pipeline to the registry
var resiliencePipelineRegistry = new ResiliencePipelineRegistry<string>();
resiliencePipelineRegistry.TryAddBuilder<HttpResponseMessage>("TransientHttpError", 
    (builder, _) => builder.AddPipeline(resiliencePipelineBuilder.Build()));

...

// (Apizr) Add the registry to the configuration 
options => options.WithResiliencePipelineRegistry(resiliencePipelineRegistry)

3.b. (Extended only) And then, registering the pipeline:

services.AddResiliencePipeline<string, HttpResponseMessage>("TransientHttpError",
    builder => builder.AddPipeline(resiliencePipelineBuilder.Build()));
managing Headers
  • We can now redact header sensitive values from logs:
    • At design time by sourrounding values with stars:
[BaseAddress("https://reqres.in/api"), 
Headers("testKey1: *testValue1*")] 
public interface IReqResService 
{     
    [Get("/users"), Headers("testKey2: *testValue2*")]     
    Task<IApiResponse<UserList>> GetUsersAsync([RequestOptions] IApizrRequestOptions options); 
}
    • Or at register time with dedicated fluent options:
// by name
options => options.WithLoggedHeadersRedactionNames(["testKey2"])

// OR with condition
options => options.WithLoggedHeadersRedactionRule(header => header == "testKey2") 

// OR directly from key/value
options => options.WithHeaders(["testKey2: *testValue2*"])

That means that Apizr will write something like testKey2: * into your logs, while sending the right clear value into the request.

  • We can now ask Apizr to refresh a header value on every request call:
// direct configuration only
options => options.WithHeaders<IOptions<HeaderSettings>>([settings => settings.Value.TestHeader], scope: ApizrLifetimeScope.Request)

Useful with scenarios where the header value may have changed between requests.

  • We can now choose to set headers values straight to the request or store it for further headers attribute key match use:

If you choose to store it instead of setting it, Apizr will actually add the header only if the header attribute key matches with one from its store.

So first define your api with your header key and {0} placeholder value where you need to add it:

[BaseAddress("https://reqres.in/api"), 
Headers("headerKey1: {0}")]
public interface IReqResService
{
    [Get("/users"), Headers("headerKey2: *{0}*")]
    Task<UserList> GetUsersAsync([RequestOptions] IApizrRequestOptions options);

    [Post("/users")]
    Task<User> CreateUserAsync(User user, [RequestOptions] IApizrRequestOptions options);
}

As you can see we can mix with the star redact symbols which tell Apizr both to set header from store and redact it into logs.

Then register your headers with values into the store:

// Example using IOptions
options => options.WithHeaders<IOptions<HeaderSettings>>([settings => settings.Value.Header1, settings => settings.Value.Header2], mode: ApizrRegistrationMode.Store)

Here, for example, HeaderSettings may be loaded and bound from an appsetting.json file with “headerKey1: headerValue1” and “headerKey2: headerValue2”.

Then Apizr will add the headers 1 and 2 while calling GetUsersAsync beacause of the key match, but only 1 for CreateUserAsync.

Note that you definitly can mix LiftimeScope with RegistrationMode, depending on what you need to achieve.

Configuring by appsettings

Apizr v6 lets you configure almost everything by providing an IConfiguration instance from Microsoft.Extensions.Configuration (e.g. from appsetings.json).

Here is an example of an appsettings.json file with some of the settings that could be set:

{
    "Logging": {
        "LogLevel": { // No provider, LogLevel applies to all the enabled providers.
            "Default": "Trace", // Default, application level if no other level applies
            "Microsoft": "Warning", // Log level for log category which starts with text 'Microsoft' (i.e. 'Microsoft.*')
            "Microsoft.Extensions.Http.DefaultHttpClientFactory": "Information"
        }
    },
    "ResilienceOptions": { // Root Microsoft Resilience configuration section key
      "Retry": { // Retry configuration section key
        "BackoffType": "Exponential",
        "UseJitter": true,
        "MaxRetryAttempts": 3
      }
    },
    "Apizr": { // Root Apizr configuration section key
        "CommonOptions": { // Common options shared by all apis
            "Logging": { // Common logging settings
                "HttpTracerMode": "Everything",
                "TrafficVerbosity": "All",
                "LogLevels": ["Trace", "Information", "Critical"]
            },
            "OperationTimeout": "00:00:10", // Common operation timeout
            "LoggedHeadersRedactionNames": ["testSettingsKey1"], // Headers to common redact in logs
            "ResilienceContext": { // Common resilience context settings
                "ContinueOnCapturedContext": false,
                "ReturnContextToPoolOnComplete": true
            },
            "Headers": [// Common headers applied to all apis
                "testSettingsKey6: testSettingsValue6.1"
            ],
            "ResiliencePipelineOptions": { // Common resilience pipeline applied to all apis
                "HttpGet": ["TestPipeline3"]// Resilience pipelines scoped to specific request method group
            },
            "Caching": { // Common caching settings
                "Mode": "GetAndFetch",
                "LifeSpan": "00:15:00",
                "ShouldInvalidateOnError": false
            },
            "Priority": "UserInitiated"
        },
        "ProperOptions": { // Options specific to some apis
            "IReqResSimpleService": { // Options specific to IReqResSimpleService api
                "BaseAddress": "https://reqres.in/api", // Specific base address
                "RequestTimeout": "00:00:03", // Specific request timeout
                "Headers": [// Specific headers applied to the IReqResSimpleService api
                    "testSettingsKey2: testSettingsValue2.1", // Clear static header
                    "testSettingsKey3: *testSettingsValue3.1*", // Redacted static header
                    "testSettingsKey4: {0}", // Clear runtime header
                    "testSettingsKey5: *{0}*" // Redacted runtime header
                ],
                "Caching": { // Specific caching settings overriding common ones
                    "Mode": "GetAndFetch",
                    "LifeSpan": "00:12:00",
                    "ShouldInvalidateOnError": true
                },
                "ResiliencePipelineKeys": ["TestPipeline3"], // Specific resilience pipelines applied to all IReqResSimpleService api methods
                "RequestOptions": { // Options specific to some IReqResSimpleService api methods
                    "GetUsersAsync": { // Options specific to GetUsersAsync method
                        "Caching": {
                            "Mode": "GetAndFetch",
                            "LifeSpan": "00:10:00",
                            "ShouldInvalidateOnError": false
                        },
                        "Headers": [
                            "testSettingsKey7: testSettingsValue7.1"
                        ],
                        "Priority": "Speculative"
                    }
                },
                "Priority": "Background"
            },
            "User": { // Options specific to User CRUD api
                "BaseAddress": "https://reqres.in/api/users", // Specific base address
                "RequestTimeout": "00:00:05", // Specific request timeout
                "Headers": [// Specific headers applied to the User CRUD api
                    "testSettingsKey8: testSettingsValue8.1" // Clear static header
                ],
                "Priority": 70
            }
        }
    }
}

As you can see, many things could be set by appsettings configuration and you can do it at common level (to all apis), specific level (dedicated to a named api) or even request level (dedicated to a named api’s method).

Once you’re done, don’t forget to provide it to Apizr like so:

options => options.WithConfiguration(The_Configuration_Instance_Here)
generating with refitter

Refitter can generate the Refit interface and contracts from OpenAPI specifications.

Refitter v1.2+ ([Documentation](https://refitter.github.io/) | [GitHub](https://github.com/christianhelle/refitter)) can now format the generated Refit interface to be managed by Apizr v6+ and generate some registration helpers too.

You can enable Apizr code generation either:

  • With the `–use-apizr` command line argument
  • By setting the `apizrSettings` section in the `.refitter` settings file

About the `apizrSettings` section, here is what you can configure:

"apizrSettings": {
  "withRequestOptions": true, // Recommended to include an Apizr request options parameter to Refit interface methods
  "withRegistrationHelper": true, // Mandatory to actually generate the Apizr registration extended method
  "withCacheProvider": "InMemory", // Optional, default is None
  "withPriority": true, // Optional, default is false
  "withMediation": true, // Optional, default is false
  "withOptionalMediation": true, // Optional, default is false
  "withMappingProvider": "AutoMapper", // Optional, default is None
  "withFileTransfer": true // Optional, default is false
}

Please read the documentation to get a full picture of it.

That said, former Apizr.Tools.NSwag has been deprecated to be replaced by Refitter.

get more of it

Apizr v6.0 brings many more new features! You should browse to the Changelog to get all the 40+ main changes.

If you’re coming from v5, please heads to the breaking change docmentation page.

Feel free to ask me anything on twitter, or opening a discussion, issue or PR on GitHub.

Again, it’s basically all for my own use and motivated by my needs. But it’s pushed on Nuget and opened to all so feel free to contribute.

As always, I still get ideas for what’s next 🙂

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