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:
Anytime, feel free to browse code and samples and maybe to submit PRs or ideas:
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.
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.
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.
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 filesoutput
: 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 🙂