Scroll Top

Apizr v5.0, resilient api client manager – What’s new

APIZR-V5

The goal of Apizr v5.0 is still the same: getting all the things ready to use for web API client requests, with the more resiliency we can, but without the boilerplate.

It’s still based on Refit, so what we can do with Refit could still be done with Apizr too.

But Apizr push it further by adding more and more capabilities (retry, connectivity, cache, auth, log, priority, transfer…).

All the slices of the cake I needed until now to make my projects tasty, well, resilient actually 🙂

Let’s take a look!

Some of its main features until now with v4:

  • Working offline with cache management with several cache providers
  • Handling errors with retry pattern and global catching with Polly
  • Handling request priority with Fusillade
  • Checking connectivity
  • Tracing HTTP traffic with HttpTracer
  • Handling authentication
  • Mapping model with DTO with AutoMapper
  • Using Mediator pattern with MediatR
  • Using Optional pattern with Optional.Async

Plus some of it from now with v5:

  • Share some common options by grouping APIs configurations
  • Define many options at the very final request time
  • Manage request headers dynamicaly and fluently at registration time
  • Map data with Mapster
  • Manage files transfers
  • Generate almost everything from Swagger with NSwag

Anyway, this post is about changes only, but if you want to go straight to the kitchen, we published a series about Apizr already which you can find here (quite old now so to cross with documentation reading).

A new video series will come, getting all the things together from the start to the end, including its brand new features.

If you want to get started right now cooking your own cake, please read the updated documentation:

Read - Documentation

Anytime, feel free to browse code and samples and maybe to submit PRs or ideas:

Browse - Source

The menu

Ok, your hungry I can see it. Let’s see what’s on menu.

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 but here is a sneak peek:

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 NuGet
Apizr.Extensions.Microsoft.DependencyInjection NuGet
Caching

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

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

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

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

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

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

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

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

Install this dotnet CLI tool if you want it all generated and configurated from a swagger url.

Project NuGet
Apizr.Tools.NSwag NuGet

The meal

Ok but what does it look like out of the oven?

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

Here is what it looks like:

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

[assembly:Policy("TransientHttpError")]
namespace Apizr.Sample
{
    [WebApi("https://reqres.in/"), Cache, Log]
    public interface IReqResService
    {
        [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 more configuration:

// Define some policies
var registry = new PolicyRegistry
{
    {
        "TransientHttpError", 
        HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        })
    }
};

// Get your manager instance
var reqResManager = ApizrBuilder.Current.CreateManagerFor<IReqResService>(
    options => options
        .WithPriority()
        .WithPolicyRegistry(registry)
        .WithAkavacheCacheHandler());

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 with the defined retry policies, data cached, prioritized and logged with HTTP traces.

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

The recipe

Let’s focus now on new ingredients coming with v5. And no more cooking things, I promise.

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

registry Groups

Apizr v4 introduced the registry feature to share common configurations between API registrations. It could be useful when defining a common base address to all registered APIs for example. But what if half of it get a common base path too and the other half another base path? Now with v5 we can address more complexe scenarios, sharing things deeper and deeper, creating groups and groups into groups and so on, so that you can share configurations at any level:

var apizrRegistry = ApizrBuilder.Current.CreateRegistry(
    registry => registry
        .AddGroup(
            group => group
                .AddManagerFor<IReqResUserService>(config => config.WithBasePath("users"))
                .AddManagerFor<IReqResResourceService>(config => config.WithBasePath("resources")),
            config => config.WithBaseAddress("https://reqres.in/api"))

        .AddManagerFor<IHttpBinService>(config => config.WithBaseAddress("https://httpbin.org")),

    config => config
        .WithAkavacheCacheHandler()
        .WithLogging(HttpTracerMode.ExceptionsOnly, HttpMessageParts.ResponseAll, LogLevel.Error)
);

Here is what I’m saying in this example:

  • Add a manager for IReqResUserService API interface into the registry with a common base address (https://reqres.in/api) and a specific base path (users)
  • Add a manager for IReqResResourceService API interface into the registry with a common base address (https://reqres.in/api) and a specific base path (resources)
  • Add a manager for IHttpBinService API interface into the registry with a specific base address (https://httpbin.org)
  • Apply common configuration to all managers by:
    • Providing a cache handler
    • Providing some logging settings
Base path

You can now define a base path by attribute decoration over the API interface at design time, or fluently with the dedicated option.

Attribute:

namespace Apizr.Sample
{
    [WebApi("YOUR_BASE_ADDRESS_OR_PATH")]
    public interface IReqResService
    {
        [Get("/api/users")]
        Task<UserList> GetUsersAsync();
    }
}

Fluent:

options => options.WithBasePath(YOUR_BASE_PATH)

It could be useful while sharing address segments between APIs.

Request options

We can now define or override many options at the very end, right before sending the request:

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

/!\ BREAKING /!\ All previous overrides with parameters have been deprecated and move as extension methods to the namespace Apizr.Extensions

Registry shorcuts

We can now send a request directly from an instance of a registry thanks to some shortcut extensions.

var userList = await _apizrRegistry.ExecuteAsync<IReqResService>(api => api.GetUsersAsync());

Could be useful while dealing with many registered APIs and don’t want to get it out of the registry each time you need it.

Headers

We can now set request headers fluently at registration time:

// direct configuration
options => options.AddHeaders("HeaderKey1: HeaderValue1", "HeaderKey2: HeaderValue2")

// OR factory configuration
options => options.AddHeaders(() => $"HeaderKey3: {YourHeaderValue3}")

// OR extended factory configuration with the service provider instance
options => options.AddHeaders(serviceProvider => $"HeaderKey3: {serviceProvider.GetRequiredService<IYourSettingsService>().YourHeaderValue3}")

It also could be done at request time:

// direct configuration only
options => options.AddHeaders("HeaderKey1: HeaderValue1", "HeaderKey2: HeaderValue2")
HTTPClient

We can now provide our own HttpClient implementation:

// static registration (this one is the new one)
options => options.WithHttpClient((httpMessageHandler, baseUri) => new YourOwnHttpClient(httpMessageHandler, false) {BaseAddress = baseUri});

// extended registration (already there from v1)
options => options.ConfigureHttpClientBuilder(httpClientBuilder => httpClientBuilder.WhateverOption())
Mapster

Apizr comes in v5 with a brand new integration package. You’ve got the possibility to map your data with AutoMapper since v1.3, you get the choice to do it now with Mapster.

Once the Apizr.Integrations.Mapster installed, just define your mappings as usual and then set Mapster as mapping handler.

Static registration:

// direct short configuration
options => options.WithMapsterMappingHandler(new Mapper())

// OR direct configuration
options => options.WithMappingHandler(new MapsterMappingHandler(new Mapper()))

// OR factory configuration
options => options.WithMappingHandler(() => new MapsterMappingHandler(new Mapper()))

OR Extended registration:

// First register Mapster as you used to do
var config = new TypeAdapterConfig(); // Or TypeAdapterConfig.GlobalSettings;
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();

// Then with Apizr
// direct short configuration
options => options.WithMapsterMappingHandler()

// OR closed type configuration
options => options.WithMappingHandler<MapsterMappingHandler>()

// OR parameter type configuration
options => options.WithMappingHandler(typeof(MapsterMappingHandler))

Then map at request time:

var result = await reqResManager.ExecuteAsync<SourceUser, DestUser>((api, destUser) => api.CreateUser(destUser, CancellationToken.None), sourceUser);
File transfer

Apizr v5 offers to manage files downloads and uploads, and tracking progress, with the less boilerplate we can.

Note that following exemples use the Transfer manager but you definitly can use only the Upload or the Download one instead.

Please first install one of the file transfer integration packages available, depending on your need.

Then, the registration:

// register the built-in transfer API
var transferManager = ApizrBuilder.Current.CreateTransferManager(
    options => options.WithBaseAddress("YOUR_API_BASE_ADDRESS_HERE"));

// Or register the built-in transfer API with custom types
var transferManager = ApizrBuilder.Current.CreateTransferManagerWith<MyDownloadParamType, MyUploadResultType>(
    options => options.WithBaseAddress("YOUR_API_BASE_ADDRESS_HERE"));

// OR register a custom transfer API
var transferManager = ApizrBuilder.Current.CreateTransferManagerFor<ITransferSampleApi>();

Finally, requesting:

var downloadResult = await transferManager.DownloadAsync(new FileInfo("YOUR_FILE_FULL_NAME_HERE"));

Also, if you want to track progress while transferring files, you have to activate the feature at registration time first:

var transferManager = ApizrBuilder.Current.CreateTransferManager(options => options
    .WithBaseAddress("YOUR_API_BASE_ADDRESS_HERE")
    .WithProgress());

And then provide your progress tracker at request time:

// Create a progress tracker
var progress = new ApizrProgress();
progress.ProgressChanged += (sender, args) =>
{
    // Do whatever you want when progress reported
    var percentage = args.ProgressPercentage;
};

// Track progress while transferring
var downloadResult = await transferManager.DownloadAsync(new FileInfo("YOUR_FILE_FULL_NAME_HERE"), options => options.WithProgress(progress));

There’re many more ways to play with file tranfer so please read the documentation to get started.

Also, note that it’s the first flavor of this package and there’s so much work to do to about it to get it better, like local file management, transfer queueing and resuming and so on.

NSWAG

I used to play with NSwag Studio before Apizr to create part of the boilerplate from a Swagger URI, and honestly, I was missing it with Apizr.

Here is a new package called Apizr.Tools.NSwag, a dotnet CLI tool aiming to generate all the things for your Apizr specific implementation, automagically from a Swagger URI\o/

The first time you plan to use the tool, start by installing it:

> dotnet tool install --global Apizr.Tools.NSwag

Then, jump with command line to the directory of your choice (the one where you want to generate files) and run the new command:

> apizr new

From here, you’ll get your apizr.json default configuration file into your current directory, looking like:

{
  "codeGenerators": {
    "openApiToApizrClient": {
      "registrationType": "Both",
      "withPriority": false,
      "withRetry": false,
      "withLogs": false,
      "withRequestOptions": false,
      "withCacheProvider": "None",
      "withMediation": false,
      "withOptionalMediation": false,
      "withMapping": "None",
      "className": "{controller}",
      "operationGenerationMode": "MultipleClientsFromOperationId",
      "additionalNamespaceUsages": [],
      "additionalContractNamespaceUsages": [],
      "generateOptionalParameters": false,
      "generateJsonMethods": false,
      "enforceFlagEnums": false,
      "parameterArrayType": "System.Collections.Generic.IEnumerable",
      "parameterDictionaryType": "System.Collections.Generic.IDictionary",
      "responseArrayType": "System.Collections.Generic.ICollection",
      "responseDictionaryType": "System.Collections.Generic.IDictionary",
      "wrapResponses": false,
      "wrapResponseMethods": [],
      "generateResponseClasses": true,
      "responseClass": "SwaggerResponse",
      "namespace": "MyNamespace",
      "requiredPropertiesMustBeDefined": true,
      "dateType": "System.DateTimeOffset",
      "jsonConverters": null,
      "anyType": "object",
      "dateTimeType": "System.DateTimeOffset",
      "timeType": "System.TimeSpan",
      "timeSpanType": "System.TimeSpan",
      "arrayType": "System.Collections.Generic.List",
      "arrayInstanceType": "System.Collections.Generic.List",
      "dictionaryType": "System.Collections.Generic.IDictionary",
      "dictionaryInstanceType": "System.Collections.Generic.Dictionary",
      "arrayBaseType": "System.Collections.ObjectModel.Collection",
      "dictionaryBaseType": "System.Collections.Generic.Dictionary",
      "classStyle": "Poco",
      "jsonLibrary": "NewtonsoftJson",
      "generateDefaultValues": true,
      "generateDataAnnotations": true,
      "excludedTypeNames": [],
      "excludedParameterNames": [],
      "handleReferences": false,
      "generateImmutableArrayProperties": false,
      "generateImmutableDictionaryProperties": false,
      "jsonSerializerSettingsTransformationMethod": null,
      "inlineNamedArrays": false,
      "inlineNamedDictionaries": false,
      "inlineNamedTuples": true,
      "inlineNamedAny": false,
      "generateDtoTypes": true,
      "generateOptionalPropertiesAsNullable": false,
      "generateNullableReferenceTypes": false,
      "templateDirectory": null,
      "typeNameGeneratorType": null,
      "propertyNameGeneratorType": null,
      "enumNameGeneratorType": null,
      "serviceHost": null,
      "serviceSchemes": null,
      "output": null,
      "newLineBehavior": "Auto"
    }
  },
  "runtime": "Net70",
  "defaultVariables": null,
  "documentGenerator": {
    "fromDocument": {
      "url": "http://redocly.github.io/redoc/openapi.yaml",
      "output": null,
      "newLineBehavior": "Auto"
    }
  }
}

Open that file and update its values with yours.

At least 3 of it:

  • into the openApiToApizrClient section:
    • namespace: the namespace used into generated files
    • output: a sub directory where to put generated files
  • into the fromDocument section:
    • url: the openapi json or yaml definition url

Once configuration file has been adjusted to your needs, execute the run command from the same directory where your apizr.json stands:

> apizr run

You should now get all your generated files right in place in your configured output folder.

When you’ll be including these files in your project, don’t forget to install Nuget package dependencies as listed into the generated comments.

Like the file transfer package, this NSwag tool is a kid, I mean with probably a lot more work to be be done.

conclusion

I think I’m done with highlighting Apizr v5.0 features, but you should keep eyes on the Go to the Changelog to get all the changes picture.

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 the world so feel free to contribute.

I still get some 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