Apizr
Refit based web api client, but resilient (retry, connectivity, cache, auth, log, priority, etc...)
You'll find a blog post series here about Apizr.
Libraries
Apizr v3+ introduced some breaking changes as it relies on Refit v6+ which actually introduced breaking changes and the fact that Fusillade has been moved from core package to a dedicated one. Please take a look at the changelog and this updated readme.
Install the NuGet package of your choice:
- Apizr package comes with the For and CrudFor static instantiation approach (which you can register in your DI container then)
- Apizr.Extensions.Microsoft.DependencyInjection package extends your IServiceCollection with AddApizrFor and AddApizrCrudFor registration methods (ASP.Net Core, etc)
- Apizr.Integrations.Shiny package brings ICacheHandler, ILogHandler and IConnectivityHandler method mapping implementations for Shiny, extending your IServiceCollection with a UseApizr and UseApizrCrudFor registration methods
- Apizr.Integrations.Fusillade package enables request priority management using Fusillade
- Apizr.Integrations.Akavache package brings an ICacheHandler method mapping implementation for Akavache
- Apizr.Integrations.MonkeyCache package brings an ICacheHandler method mapping implementation for MonkeyCache
- Apizr.Integrations.MediatR package enables request auto handling with mediation using MediatR
- Apizr.Integrations.Optional package enables Optional result from mediation requests (requires MediatR integration) using Optional.Async
- Apizr.Integrations.AutoMapper package enables auto mapping for mediation requests (requires MediatR integration and could work with Optional integration) using AutoMapper
Apizr core package make use of well known nuget packages to make the magic appear:
Package | Features |
---|---|
Refit | Auto-implement web api interface and deal with HttpClient |
Polly | Apply some policies like Retry, CircuitBreaker, etc... |
HttpTracer | Trace Http(s) request/response traffic to log it |
It also comes with some handling interfaces to let you provide your own services for:
- Caching with ICacheHandler, which comes with its default VoidCacheHandler (no cache), but also with:
- AkavacheCacheHandler: Akavache method mapping interface (Integration package referenced above)
- MonkeyCacheHandler: MonkeyCache method mapping interface (Integration package referenced above)
- ShinyCacheHandler: Shiny chaching method mapping interface (Integration package referenced above)
- Logging with ILogHandler, which comes with its default DefaultLogHandler (Console and Debug), but also with:
- ShinyLogHandler: Shiny logging method mapping interface (Integration package referenced above)
- Connectivity with IConnectivityHandler, which comes with its default VoidConnectivityHandler (no connectivity check), but also with:
- ShinyConnectivityHandler: Shiny connectivity method mapping interface (Integration package referenced above)
- Mapping with IMappingHandler, which comes with its default VoidMappingHandler (no mapping conversion), but also with:
- AutoMapperMappingHandler: AutoMapper mapping method mapping interface (Integration package referenced above)
How to:
Intro
Clearly inspired by Refit.Insane.PowerPack but extended with a lot more features, the goal of Apizr is to get all ready to use for web api requesting, with the more resiliency we can, but without the boilerplate.
Apizr v3+ relies on Refit v6+ witch makes System.Text.Json the default JSON serializer instead of Newtonsoft.Json.
If you'd like to continue to use Newtonsoft.Json, add the Refit.Newtonsoft.Json NuGet package and set your ContentSerializer to NewtonsoftJsonContentSerializer on your RefitSettings instance. You can do it by calling the WithRefitSettings(...)
options builder method.
Examples here are based on a Xamarin.Forms app working with Shiny. You'll find a sample Xamarin.Forms app browsing code, implementing Apizr with Shiny, Prism and MS DI all together. You'll find another sample app but .Net Core console this time, implementing Apizr without anything else (static) and also with MS DI (extensions).
So please, take a look at the samples :)
Classic APIs:
Defining:
We could define our web api service just like:
[assembly:Policy("TransientHttpError")]
namespace Apizr.Sample.Api
{
[WebApi("https://reqres.in/"), CacheIt, LogIt]
public interface IReqResService
{
[Get("/api/users")]
Task<UserList> GetUsersAsync(CancellationToken cancellationToken);
[Get("/api/users/{userId}")]
Task<UserDetails> GetUserAsync([CacheKey] int userId, CancellationToken cancellationToken);
[Post("/api/users")]
Task<User> CreateUser(User user, CancellationToken cancellationToken);
}
}
And that's all.
Every attributes here will inform Apizr on how to manage each web api request. No more boilerplate.
Registering:
As it's not mandatory to register anything in a container for DI purpose (you can use a static instance directly), I'll describe here how to use it with DI.
Static approach:
Somewhere where you can add services to your container, add the following:
// Some policies
var registry = new PolicyRegistry
{
{
"TransientHttpError", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
})
}
};
// Apizr registration
myContainer.SomeInstanceRegistrationMethod(Apizr.For<IReqResService>(optionsBuilder => optionsBuilder.WithPolicyRegistry(registry)
.WithCacheHandler(new AkavacheCacheHandler())));
I provided a policy registry and a cache handler here as I asked for it with cache and policy attributes in my web api example.
Extensions approach:
For this one, two options :
- Manually: register calling
AddApizrFor<TWebApi>
service collection extension method or overloads for each service you want to manage - Automatically: decorate your services with WebApiAttribute and let Apizr auto register it all for you
Manually:
Here is an example:
public override void ConfigureServices(IServiceCollection services)
{
var registry = new PolicyRegistry
{
{
"TransientHttpError", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
})
}
};
services.AddPolicyRegistry(registry);
// Apizr registration
services.AddApizrFor<IReqResService>(optionsBuilder => optionsBuilder.WithCacheHandler<AkavacheCacheHandler>());
// Or if you use Shiny
//services.UseApizrFor<IReqResService>();
}
Automatically:
Decorate your api services like we did before (but with your own settings):
[assembly:Policy("TransientHttpError")]
namespace Apizr.Sample.Api
{
[WebApi("https://reqres.in/"), CacheIt, LogIt]
public interface IReqResService
{
[Get("/api/users")]
Task<UserList> GetUsersAsync(CancellationToken cancellationToken);
[Get("/api/users/{userId}")]
Task<UserDetails> GetUserAsync([CacheKey] int userId, CancellationToken cancellationToken);
[Post("/api/users")]
Task<User> CreateUser(User user, CancellationToken cancellationToken);
}
}
Then, register in your Startup class like so:
public override void ConfigureServices(IServiceCollection services)
{
// Apizr registration
services.AddApizrFor(typeof(AnyClassFromServicesAssembly));
// Or if you use Shiny
//services.UseApizrFor(typeof(AnyClassFromServicesAssembly));
}
There are 4 AddApizrFor/UseApizrFor flavors for classic automatic registration, depending on what you want to do and provide. This one is the simplest.
Using:
Sending web request from your app - e.g. using Apizr in a Xamarin.Forms mobile app.
Inject IApizrManager<YourWebApiInterface>
where you need it - e.g. into your ViewModel constructor
public class YourViewModel
{
private readonly IApizrManager<IReqResService> _reqResManager;
public YouViewModel(IApizrManager<IReqResService> reqResManager)
{
_reqResManager = reqResManager;
}
public ObservableCollection<User>? Users { get; set; }
private async Task GetUsersAsync()
{
IList<User>? users;
try
{
var userList = await _reqResManager.ExecuteAsync((ct, api) => api.GetUsersAsync(ct), CancellationToken.None);
users = userList.Data;
}
catch (ApizrException<UserList> e)
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
users = e.CachedResult?.Data;
}
if(users != null)
Users = new ObservableCollection<User>(users);
}
}
I catch execution into an ApizrException as it will contain the original inner exception, but also the previously cached result if some. If you provided an IConnectivityHandler implementation and there's no network connectivity before sending request, Apizr will throw with an IO inner exception without sending the request.
CRUD APIs:
When playing with RESTful CRUD api, you've got a couple of options:
- Define a web api interface like we just did before with each crud method (each entity into one interface or one interface for each entity)
- Use the built-in ICrudApi
As the first option is described already, here we'll talk about the ICrudApi option
Defining:
As we'll use the built-in yet defined ICrudApi, there's no more definition to do.
Here is what it looks like then:
public interface ICrudApi<T, in TKey, TReadAllResult, in TReadAllParams> where T : class
{
[Post("")]
Task<T> Create([Body] T payload);
[Post("")]
Task<T> Create([Body] T payload, CancellationToken cancellationToken);
[Get("")]
Task<TReadAllResult> ReadAll();
[Get("")]
Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams);
[Get("")]
Task<TReadAllResult> ReadAll([Property("Priority")] int priority);
[Get("")]
Task<TReadAllResult> ReadAll(CancellationToken cancellationToken);
[Get("")]
Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property("Priority")] int priority);
[Get("")]
Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, CancellationToken cancellationToken);
[Get("")]
Task<TReadAllResult> ReadAll([Property("Priority")] int priority, CancellationToken cancellationToken);
[Get("")]
Task<TReadAllResult> ReadAll([CacheKey] TReadAllParams readAllParams, [Property("Priority")] int priority, CancellationToken cancellationToken);
[Get("/{key}")]
Task<T> Read([CacheKey] TKey key);
[Get("/{key}")]
Task<T> Read([CacheKey] TKey key, [Property("Priority")] int priority);
[Get("/{key}")]
Task<T> Read([CacheKey] TKey key, CancellationToken cancellationToken);
[Get("/{key}")]
Task<T> Read([CacheKey] TKey key, [Property("Priority")] int priority, CancellationToken cancellationToken);
[Put("/{key}")]
Task Update(TKey key, [Body] T payload);
[Put("/{key}")]
Task Update(TKey key, [Body] T payload, CancellationToken cancellationToken);
[Delete("/{key}")]
Task Delete(TKey key);
[Delete("/{key}")]
Task Delete(TKey key, CancellationToken cancellationToken);
}
We can see that it comes with some CacheKey and Priority attribute decorations, but it won't do anything until you ask Apizr to. Caching, Logging, Policing, Prioritizing... everything is activable fluently with the options builder.
About generic types:
- T and TKey (optional - default:
int
) meanings are obvious - TReadAllResult (optional - default:
IEnumerable<T>
) is there to handle cases where ReadAll doesn't return anIEnumerable<T>
or derived, but a paged result with some statistics - TReadAllParams (optional - default:
IDictionary<string, object>
) is there to handle cases where you don't want to provide anIDictionary<string, object>
for a ReadAll reaquest, but a custom class
But again, nothing to do around here.
Registering:
Static approach:
Somewhere where you can add services to your container, add the following:
// Apizr registration
myContainer.SomeInstanceRegistrationMethod(Apizr.CrudFor<T, TKey, TReadAllResult, TReadAllParams>(optionsBuilder => optionsBuilder.WithBaseAddress("your specific T entity crud base uri")));
T must be a class.
TKey must be primitive. If you don't provide it here, it will be defined as int
.
TReadAllResult must inherit from IEnumerable<>
or be a class.
If you don't use paged result, just don't provide any TReadAllResult here and it will be defined as IEnumerable<T>
.
TReadAllParams must be a class.
If you don't use a custom class holding your query parameters, just don't provide any TReadAllParams here and it will be defined as IDictionary<string, object>
.
You have to provide the specific entity crud base uri with the options builder.
There are 5 CrudFor flavors, depending on what you want to do and provide.
One of it is the simple Apizr.CrudFor<T>()
, which as you can expect, define TKey as int
, TReadAllResult as IEnumerable<T>
and TReadAllParams as IDictionary<string, object>
.
Extensions approach:
Ok, for this one, two options again:
- Manually: register calling AddApizrCrudFor<T, TKey, TReadAllResult, TReadAllParams> service collection extension method or overloads for each entity you want to manage
- Automatically: decorate your entities with CrudEntityAttribute and let Apizr auto register it all for you
Manually:
In your Startup class, add the following:
public override void ConfigureServices(IServiceCollection services)
{
// Apizr registration
services.AddApizrCrudFor<T, TKey, TReadAllResult, TReadAllParams>(optionsBuilder => optionsBuilder.WithBaseAddress("your specific T entity crud base uri"));
// Or if you use Shiny
//services.UseApizrCrudFor<T, TKey, TReadAllResult, TReadAllParams>(optionsBuilder => optionsBuilder.WithBaseAddress("your specific T entity crud base uri"));
}
Again, T must be a class.
TKey must be primitive. If you don't provide it here, it will be defined as int
.
TReadAllResult must inherit from IEnumerable<>
or be a class.
If you don't use paged result, just don't provide any TReadAllResult here and it will be defined as IEnumerable<T>
.
TReadAllParams must be a class.
If you don't use a custom class holding your query parameters, just don't provide any TReadAllParams here and it will be defined as IDictionary<string, object>
.
You have to provide the specific entity crud base uri with the options builder.
There are 10 AddApizrCrudFor/UseApizrCrudFor flavors for crud manual registration, depending on what you want to do and provide.
One of it is the simple services.AddApizrCrudFor<T>()
or services.UseApizrCrudFor<T>()
, which as you can expect, define TKey as int
, TReadAllResult as IEnumerable<T>
and TReadAllParams as IDictionary<string, object>
.
Automatically:
You need to have access to your entity model classes for this option.
Decorate your crud entities like so (but with your own settings):
[CrudEntity("https://myapi.com/api/myentity", typeof(int), typeof(PagedResult<>), typeof(ReadAllUsersParams))]
public class MyEntity
{
[JsonPropertyName("id")]
public int Id { get; set; }
...
}
Thanks to this attribute:
- (Mandatory) We have to provide the specific entity crud base uri
- (Optional) We can set TKey type to any primitive type (default to int)
- (Optional) We can set TReadAllResult to any class or must inherit from
IEnumerable<>
(default toIEnumerable<T>
) - (Optional) We can set TReadAllParams to any class (default to
IDictionary<string, object>
)
Then, register in your Startup class like so:
public override void ConfigureServices(IServiceCollection services)
{
// Apizr registration
services.AddApizrCrudFor(typeof(MyEntity));
// Or if you use Shiny
//services.UseApizrCrudFor(typeof(MyEntity));
}
There are 4 AddApizrCrudFor/UseApizrCrudFor flavors for crud automatic registration, depending on what you want to do and provide. This is the simplest.
Using:
Sending web request from your app - e.g. using Apizr in a Xamarin.Forms mobile app.
Inject IApizrManager<ICrudApi<T, TKey, TReadAllResult, TReadAllParams>>
where you need it - e.g. into your ViewModel constructor
public class YourViewModel
{
private readonly IApizrManager<ICrudApi<User, int, PagedResult<User>, ReadAllUsersParams>> _userCrudManager;
public YouViewModel(IApizrManager<ICrudApi<User, int, PagedResult<User>, ReadAllUsersParams>> userCrudManager)
{
_userCrudManager = userCrudManager;
}
public ObservableCollection<User>? Users { get; set; }
private async Task GetUsersAsync()
{
IList<User>? users;
try
{
var pagedUsers = await _userCrudManager.ExecuteAsync((ct, api) => api.ReadAll(ct), CancellationToken.None);
users = pagedUsers.Data?.ToList();
}
catch (ApizrException<PagedResult<User>> e)
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
users = e.CachedResult?.Data;
}
if(users != null)
Users = new ObservableCollection<User>(users);
}
}
I catch execution into an ApizrException as it will contain the original inner exception, but also the previously cached result if some. If you provided an IConnectivityHandler implementation and there's no network connectivity before sending request, Apizr will throw with an IO inner exception without sending the request.
Advanced configurations:
There're some advanced scenarios where you want to adjust some settings and behaviors. This is where the options builder comes in. Each registration approach comes with its optionsBuilder optional parameter:
optionsBuilder => optionsBuilder.SomeOptionsHere(someParametersThere)
Service handlers:
The options builder let you provide your own method mapping implementations for:
- ICacheHandler (thanks to WithCacheHandler)
- ILogHandler (thanks to WithLogHandler)
- IConnectivityHandler (thanks to WithConnectivityHandler)
- IMappingHandler (thanks to WithMappingHandler).
Authentication DelegatingHandler:
For autorized request calls, you can provide some properties and/or methods (thanks to WithAuthenticationHandler) to help Apizr to authenticate user when needed.
Custom DelegatingHandler:
The options builder let you add any custom delegating handler thanks to AddDelegatingHandler method
Refit settings:
You can adjust some specific Refit settings providing an instance of RefitSettings (thanks to WithRefitSettings). Note that for this one, only constructor parameters will be used (IContentSerializer, IUrlParameterFormatter and IFormUrlEncodedParameterFormatter).
Please don't use AuthorizationHeaderValueGetter, AuthorizationHeaderValueWithParamGetter and HttpMessageHandlerFactory, as they'll be ignored.
Prefer using WithAuthenticationHandler builder method to manage request authorization and AddDelegatingHandler builder method to add some other custom delegating handlers.
Policy registry:
If you plan to use the PoliciesAttribute, Apizr needs to know where to find your policy registry:
-
With static instantiation, you have to provide it thanks to WithPolicyRegistry builder method.
-
With extensions registration, you have to register it thanks to AddPolicyRegistry service collection extension method.
In any case, you may want to log what's going on during policies excecution. To do so, there's an OnRetry helper action which provide your ILogHandler method mapping implementation to Polly.
Here's how to use it:
// Some policies
var registry = new PolicyRegistry
{
{
"TransientHttpError", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}, LoggedPolicies.OnLoggedRetry).WithPolicyKey("TransientHttpError")
}
};
LoggedPolicies.OnLoggedRetry could also execute your own specific action if needed.
HttpClient:
With extensions registration, you can adjust some more HttpClient settings thanks to ConfigureHttpClientBuilder builder method. This one could interfere with all Apizr http client auto configuration, so please use it with caution.
External integrations:
Shiny:
If you're a Shiny user, with the right extension package installed, just register Apizr calling UseApizr
instead of AddApizr
.
Then, everything will be in place, ready to use, relying on Shiny features (Logging, Caching, Connectivity).
MonkeyCache:
If you're a MonkeyCache user, with the right extension package installed:
Set the Barrel's ApplicationId:
Barrel.ApplicationId = "YOUR_APPLICATION_ID";
Then tell Apizr you want to use MonkeyCache as caching layer:
optionsBuilder => optionsBuilder.WithCacheHandler(() => new MonkeyCacheHandler(Barrel.Current))
Akavache:
If you're an Akavache user, with the right extension package installed:
Just tell Apizr you want to use Akavahe as caching layer:
optionsBuilder => optionsBuilder.WithCacheHandler(() => new AkavacheCacheHandler())
Fusillade:
Starting Apizr v3, Fusillade has been moved from core project to its dedicated integration package. If you plan to use it, you now have to install this package.
Once installed, you should be able to activate it fluently with the provided extension:
optionsBuilder => optionsBuilder.WithPriorityManagement()
From there, everything will be user initiated. When you need to specify another priority, what you need to do is just adding the priority parameter into your api interface method definition:
[Get("/api/users")]
Task<UserList> GetUsersAsync([Priority] int priority, CancellationToken cancellationToken);
Mediation:
In extensions registration approach and with the dedicated integration nuget package referenced, the options builder let you enable mediation by calling:
optionsBuilder => optionsBuilder.WithMediation()
Don't forget to register MediatR itself as usual:
services.AddMediatR(typeof(Startup));
When activated, you don't have to inject/resolve anything else than an IMediator
instance, in order to play with your api services (both classic and crud).
Everything you need to do then, is sending your request calling:
var result = await _mediator.Send(YOUR_REQUEST_HERE);
Where YOUR_REQUEST_HERE could be, with classic api interfaces:
ExecuteRequest<TWebApi>
: execute any method fromTWebApi
defined by an expression parameterExecuteRequest<TWebApi, TApiResponse>
: execute any method fromTWebApi
with aTApiResponse
result and defined by an expression parameterExecuteRequest<TWebApi, TModelResponse, TApiResponse>
: execute any method fromTWebApi
with aTApiResponse
mapped to aTModelResponse
result and defined by an expression parameter
NOTE - Mapping: When I say "mapped", I talk about the mapping integration feature
Please refer to AutoMapper section for more info
Or with crud api interfaces:
ReadQuery<T>
: get the T entity with intReadQuery<T, TKey>
: get the T entity with TKeyReadAllQuery<TReadAllResult>
: get TReadAllResult with IDictionary<string, object> optional query parametersReadAllQuery<TReadAllParams, TReadAllResult>
: get TReadAllResult with TReadAllParams optional query parametersCreateCommand<T>
: create a T entityUpdateCommand<T>
: update the T entity with intUpdateCommand<TKey, T>
: update the T entity with TKeyDeleteCommand<T>
: delete the T entity with intDeleteCommand<T, TKey>
: delete the T entity with TKey
There's also a typed mediator available for each api interface (classic or crud), to help you write things shorter.
With classic api interfaces, resolving IMediator<TWebApi>
give you access to:
SendFor(YOUR_API_METHOD_EXPRESSION)
: send anExecuteRequest<TWebApi>
for youSendFor<TApiResponse>(YOUR_API_METHOD_EXPRESSION)
: send anExecuteRequest<TWebApi, TApiResponse>
for youSendFor<TModelResponse, TApiResponse>(YOUR_API_METHOD_EXPRESSION)
: send anExecuteRequest<TWebApi, TModelResponse, TApiResponse>
for you
With crud api interfaces, resolving ICrudMediator<TApiEntity, TApiEntityKey, TReadAllResult, TReadAllParams>
give you access to:
SendReadQuery(TApiEntityKey key)
: send aReadQuery<TApiEntity, TApiEntityKey>
for youSendReadQuery<TModelEntity>(TApiEntityKey key)
: send aReadQuery<TModelEntity, TApiEntityKey>
for you, withTModelEntity
mapped withTApiEntity
SendReadAllQuery()
: send aReadAllQuery<TReadAllResult>
for youSendReadAllQuery<TModelEntityReadAllResult>()
: send aReadAllQuery<TModelEntityReadAllResult>
for you, withTModelEntityReadAllResult
mapped withTReadAllResult
SendCreateCommand(TApiEntity payload)
: send aCreateCommand<TApiEntity>
for youSendCreateCommand<TModelEntity>(TModelEntity payload)
: send aCreateCommand<TModelEntity>
for you, withTModelEntity
mapped withTApiEntity
SendUpdateCommand(TApiEntityKey key, TApiEntity payload)
: send anUpdateCommand<TApiEntityKey, TApiEntity>
for youSendUpdateCommand<TModelEntity>(TApiEntityKey key, TModelEntity payload)
: send anUpdateCommand<TApiEntityKey, TModelEntity>
for you, withTModelEntity
mapped withTApiEntity
SendDeleteCommand(TApiEntityKey key)
: send aDeleteCommand<TApiEntity, TApiEntityKey>
for you
Most of all requests get some overloads to provide some more parameters.
Apizr will intercept your request and handle it to send the result back to you, thanks to MediatR.
From there, our ViewModel can look like (only one interface necessary in real world):
public class YourViewModel
{
private readonly IMediator _mediator;
private readonly IMediator<IReqResService> _reqResMediator;
private readonly ICrudMediator<User, int, PagedResult<User>, IDictionary<string, object>> _userMediator;
public YouViewModel(IMediator mediator,
IMediator<IReqResService> reqResMediator,
ICrudMediator<User, int, PagedResult<User>, IDictionary<string, object>> userMediator)
{
_mediator = mediator;
_reqResMediator = reqResMediator;
_userMediator = userMediator;
}
public ObservableCollection<User>? Users { get; set; }
// This won't compile obviously
// It's an example presenting all ways to play with MediatR
// You should choose one of these ways
private async Task GetUsersAsync()
{
IList<User>? users;
try
{
// The classic api interface way
var userList = await _mediator.Send(new ExecuteRequest<IReqResService, UserList>((ct, api) => api.GetUsersAsync(ct)), CancellationToken.None);
users = userList.Data;
// The classic api interface way with typed mediator
var userList = await _reqResMediator.SendFor(api => api.GetUsersAsync());
users = userList.Data;
// The crud api interface way
var pagedUsers = await _mediator.Send(new ReadAllQuery<PagedResult<User>>(), CancellationToken.None);
users = pagedUsers.Data?.ToList();
// The crud api interface way with typed mediator
var pagedUsers = await _userMediator.SendReadAllQuery();
users = pagedUsers.Data?.ToList();
}
catch (ApizrException<PagedResult<User>> e)
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
users = e.CachedResult?.Data;
}
if(users != null)
Users = new ObservableCollection<User>(users);
}
}
Optional:
In extensions registration approach and with the dedicated integration nuget package referenced, the options builder let you enable mediation with Optional result by calling:
optionsBuilder => optionsBuilder.WithOptionalMediation()
Again, don't forget to register MediatR itself as usual :
services.AddMediatR(typeof(Startup));
When activated, you don't have to inject/resolve anything else than an IMediator
instance, in order to play with your api services (both classic and crud).
Everything you need to do then, is sending your request calling:
var result = await _mediator.Send(YOUR_REQUEST_HERE);
Where YOUR_REQUEST_HERE could be, with classic api interfaces:
ExecuteOptionalRequest<TWebApi>
: execute any method fromTWebApi
defined by an expression parameter which returnsOption<Unit, ApizrException>
ExecuteOptionalRequest<TWebApi, TApiResponse>
: execute any method fromTWebApi
defined by an expression parameter which returnsOption<TApiResponse, ApizrException<TApiResponse>>
ExecuteOptionalRequest<TWebApi, TModelResponse, TApiResponse>
: execute any method fromTWebApi
defined by an expression parameter which returnsOption<TModelResponse, ApizrException<TModelResponse>>
whereTModelResponse
mapped fromTApiResponse
NOTE - Mapping: When I say "mapped", I talk about the mapping integration feature
Please refer to AutoMapper section for more info
Or with crud api interfaces:
ReadOptionalQuery<T>
: get the T entity with int and returnsOption<T, ApizrException<T>>
ReadOptionalQuery<T, TKey>
: get the T entity with TKey and returnsOption<T, ApizrException<T>>
ReadAllOptionalQuery<TReadAllResult>
: get TReadAllResult with IDictionary<string, object> optional query parameters and returnsOption<TReadAllResult, ApizrException<TReadAllResult>>
ReadAllOptionalQuery<TReadAllParams, TReadAllResult>
: get TReadAllResult with TReadAllParams optional query parameters and returnsOption<TReadAllResult, ApizrException<TReadAllResult>>
CreateOptionalCommand<T>
: create a T entity and returnsOption<Unit, ApizrException>
UpdateOptionalCommand<T>
: update the T entity with int and returnsOption<Unit, ApizrException>
UpdateOptionalCommand<TKey, T>
: update the T entity with TKey and returnsOption<Unit, ApizrException>
DeleteOptionalCommand<T>
: delete the T entity with int and returnsOption<Unit, ApizrException>
DeleteOptionalCommand<T, TKey>
: delete the T entity with TKey and returnsOption<Unit, ApizrException>
There's also a typed optional mediator available for each api interface (classic or crud), to help you write things shorter.
With classic api interfaces, resolving IOptionalMediator<TWebApi>
give you access to:
SendFor(YOUR_API_METHOD_EXPRESSION)
: send anExecuteOptionalRequest<TWebApi>
for youSendFor<TApiResponse>(YOUR_API_METHOD_EXPRESSION)
: send anExecuteOptionalRequest<TWebApi, TApiResponse>
for youSendFor<TModelResponse, TApiResponse>(YOUR_API_METHOD_EXPRESSION)
: send anExecuteOptionalRequest<TWebApi, TModelResponse, TApiResponse>
for you
With crud api interfaces, resolving ICrudOptionalMediator<TApiEntity, TApiEntityKey, TReadAllResult, TReadAllParams>
give you access to:
SendReadOptionalQuery(TApiEntityKey key)
: send aReadOptionalQuery<TApiEntity, TApiEntityKey>
for youSendReadOptionalQuery<TModelEntity>(TApiEntityKey key)
: send aReadOptionalQuery<TModelEntity, TApiEntityKey>
for you, withTModelEntity
mapped withTApiEntity
SendReadAllOptionalQuery()
: send aReadAllOptionalQuery<TReadAllResult>
for youSendReadAllOptionalQuery<TModelEntityReadAllResult>()
: send aReadAllOptionalQuery<TModelEntityReadAllResult>
for you, withTModelEntityReadAllResult
mapped withTReadAllResult
SendCreateOptionalCommand(TApiEntity payload)
: send aCreateOptionalCommand<TApiEntity>
for youSendCreateOptionalCommand<TModelEntity>(TModelEntity payload)
: send aCreateOptionalCommand<TModelEntity>
for you, withTModelEntity
mapped withTApiEntity
SendUpdateOptionalCommand(TApiEntityKey key, TApiEntity payload)
: send anUpdateOptionalCommand<TApiEntityKey, TApiEntity>
for youSendUpdateOptionalCommand<TModelEntity>(TApiEntityKey key, TModelEntity payload)
: send anUpdateOptionalCommand<TApiEntityKey, TModelEntity>
for you, withTModelEntity
mapped withTApiEntity
SendDeleteOptionalCommand(TApiEntityKey key)
: send aDeleteOptionalCommand<TApiEntity, TApiEntityKey>
for you
Apizr will intercept it and handle it to send the result back to you, thanks to MediatR and Optional.
From there, our ViewModel can look like (only one interface necessary in real world):
public class YourViewModel
{
private readonly IMediator _mediator;
private readonly IOptionalMediator<IReqResService> _reqResOptionalMediator;
private readonly ICrudOptionalMediator<User, int, PagedResult<User>, IDictionary<string, object>> _userOptionalMediator;
public YouViewModel(IMediator mediator,
IOptionalMediator<IReqResService> reqResOptionalMediator,
ICrudOptionalMediator<User, int, PagedResult<User>, IDictionary<string, object>> userOptionalMediator)
{
_mediator = mediator;
_reqResOptionalMediator = reqResOptionalMediator;
_userOptionalMediator = userOptionalMediator;
}
public ObservableCollection<User>? Users { get; set; }
// This won't compile obviously
// It's an example presenting all ways to play with Optional
// You should choose one of these ways
private async Task GetUsersAsync()
{
// The classic api interface way with mediator and optional request
var optionalUserList = await _mediator.Send(new ExecuteOptionalRequest<IReqResService, UserList>((ct, api) => api.GetUsersAsync(ct)), CancellationToken.None);
// The classic api interface way with typed optional mediator (the same but shorter)
var optionalUserList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync());
// Handling the optional result for both previous ways
optionalPagedResult.Match(userList =>
{
if (userList.Data != null && userList.Data.Any())
Users = new ObservableCollection<User>(userList.Data);
}, e =>
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
if (e.CachedResult?.Data != null && e.CachedResult.Data.Any())
Users = new ObservableCollection<User>(e.CachedResult.Data);
});
// The crud api interface way with mediator and optional request
var optionalPagedResult = await _mediator.Send(new ReadAllOptionalQuery<PagedResult<User>>(), CancellationToken.None);
// The crud api interface way with typed crud optional mediator
var optionalPagedResult = await _userOptionalMediator.SendReadAllOptionalQuery();
// Handling the optional result for both previous ways
optionalPagedResult.Match(pagedUsers =>
{
if (pagedUsers.Data != null && pagedUsers.Data.Any())
Users = new ObservableCollection<User>(pagedUsers.Data);
}, e =>
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
if (e.CachedResult?.Data != null && e.CachedResult.Data.Any())
Users = new ObservableCollection<User>(e.CachedResult.Data);
});
}
}
Same advantages than classic mediation but with exception handling. Both "classic" and "optional" mediation are compatibles with each other. It means that if you call both methods during registration, both request collection will be available, so you can decide which one suits to you when you need it.
Optional helper extentions:
Optional and MediatR are pretty cool.
But even if we use the typed optional mediator or typed crud optional mediator to get things shorter, we still have to deal with the result boilerplate:
// The classic api interface way with typed optional mediator (the same but shorter)
var optionalUserList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync());
// Handling the optional result for both previous ways
optionalPagedResult.Match(userList =>
{
if (userList.Data != null && userList.Data.Any())
Users = new ObservableCollection<User>(userList.Data);
}, e =>
{
var message = e.InnerException is IOException ? "No network" : (e.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
if (e.CachedResult?.Data != null && e.CachedResult.Data.Any())
Users = new ObservableCollection<User>(e.CachedResult.Data);
});
Let's cut down the optional result handling thing, to get something as short as we can.
OnResultAsync
and CatchAsync
are extension methods to handle optional result fluently.
OnResultAsync:
OnResultAsync
ask you to provide one of these parameters:
Action<TResult> onResult
: this action will be invoked just before throwing any exception that might have occurred during request executionFunc<TResult, ApizrException<TResult>, bool> onResult
: this function will be invoked with the returned result and potential occurred exceptionFunc<TResult, ApizrException<TResult>, Task<bool>> onResult
: this function will be invoked async with the returned result and potential occurred exception
All give you a result returned from fetch if succeed, or cache if failed. The main goal here is to set any binded property with the returned result (fetched or cached), no matter of exceptions. Then the Action will let the exception throw, where the Func will let you decide to throw manually or return a success boolean flag.
Here is what our final request looks like with Action (auto throwing after invocation on excpetion):
await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).OnResultAsync(userList => { users = userList?.Data; });
Or with Func and throw:
await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).OnResultAsync((userList, exception) =>
{
users = userList?.Data;
if(exception != null)
throw exception;
return true;
});
Or with Func and success flag:
var success = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).OnResultAsync((userList, exception) =>
{
users = userList?.Data;
return exception != null;
});
We could combine the first two with AsyncErrorHandler, to catch them all globally and show any information dialog to the user, like:
public static class AsyncErrorHandler
{
public static void HandleException(Exception exception)
{
var message = exception is IOException || exception.InnerException is IOException ? "No network" : (exception.Message ?? "Error");
UserDialogs.Instance.Toast(new ToastConfig(message) { BackgroundColor = Color.Red, MessageTextColor = Color.White });
Log.Write(exception);
}
}
CatchAsync:
CatchAsync
let you provide these parameters:
Action<Exception> onException
: this action will be invoked just before returning the result from cache if fetch failed. Useful to inform the user of the api call failure and that data comes from cache.letThrowOnExceptionWithEmptyCache
: True to let it throw the inner exception in case of empty cache, False to handle it withonException
action and return empty cache result (default: False)
This one is to return result from fetch or cache, no matter of execption handled on the other side by an action callback to inform the user
var users = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).CatchAsync(AsyncErrorHandler.HandleException, true);
Here we ask the api to get users and if it fails:
- There's some cached data?
- AsyncErrorHandler will handle the exception to inform the user call just failed
- Apizr will return the previous result from cache
- There's no cached data yet!
letThrowOnExceptionWithEmptyCache
is True? (which is the case here)- Apizr will throw the inner exception that will be catched further by AsyncErrorHander (this is its normal Fody usage)
letThrowOnExceptionWithEmptyCache
is False! (default)- Apizr will return the empty cache data (null) which has to be handled further
Safe and shorter than ever!
AutoMapper:
You can define your own model entities and then, your AutoMapper mapping profiles between api entities and model entities.
Then, you have to tell Apizr which entities must use the mapping feature.
AutoMapper with Crud apis:
Manually:
services.AddApizrCrudFor<MappedEntity<TModelEntity, TApiEntity>>(optionsBuilder =>
optionsBuilder.WithBaseAddress("https://myapi.com/api/myentity")
.WithMediation()
.WithMappingHandler<AutoMapperMappingHandler>());
Manual registration makes use of MappedEntity<TModelEntity, TApiEntity> just in place of our usual T. You'll have to enable one or both mediation feature to handle requests (classic and/or optional) and provide a mapping handler. You'll have to repeat this registration for each crud mapping.
Don't forget to register AutoMapper itself as usual :
services.AddAutoMapper(typeof(Startup));
Automatically:
Why not let Apizr do it for you? To do so, you have do decorate one of those two entities (api vs model) with corresponding attribute:
CrudEntityAttribute
above the api entity, withmodelEntityType
parameter set to the mapped model entity typeMappedCrudEntityAttribute
above the model entity, withapiEntityType
parameter set to the mapped api entity type
If you get access to both entities, it doesn't matter which one you decorate, just do it for one of it (if you decorate both, it will take the first found).
If you don't get any access to the api entities, just decorate your model one with the MappedCrudEntityAttribute
From here, let's write:
services.AddApizrCrudFor(optionsBuilder => optionsBuilder
.WithMediation()
.WithMappingHandler<AutoMapperMappingHandler>(),
typeof(AnyTApiEntity), typeof(AnyTModelEntity));
In this example, I provided both api entity and model entity assemblies to the attribute scanner, but actually you just have to provide the one containing your attribute decorated entities (api or model, depending of your scenario/access rights).
Don't forget to register AutoMapper itself as usual :
services.AddAutoMapper(typeof(Startup));
Using:
Nothing different here but direct using of your model entities when sending mediation requests, like:
var createdModelEntity = await _mediator.Send(new CreateCommand<TModelEntity>(myModelEntity), CancellationToken.None);
Apizr will map myModelEntity to TApiEntity, send it to the server, map the result to TModelEntity and send it back to you. And yes, it works also with Optional.
AutoMapper with classic apis:
You have do decorate one among the api method, the model entity or the api entity with MappedWithAttribute
, with mappedWithType
set to the other mapped entity.
From here, let's write:
services.AddApizrFor(optionsBuilder => optionsBuilder
.WithMediation()
.WithMappingHandler<AutoMapperMappingHandler>(),
typeof(AnyTApiEntity), typeof(AnyTModelEntity), typeof(AnyTWebApi));
Actually, the number of typeof
depends on where your attribute decorations are defined.
Don't forget to register AutoMapper itself as usual :
services.AddAutoMapper(typeof(Startup));
Using:
Nothing different here but direct using of your model entities when sending mediation requests, like:
// Classic auto mapped result only
var userInfos = await _mediator.Send(new ExecuteRequest<IReqResService, UserInfos, UserDetails>((ct, api) =>
api.GetUserAsync(userChoice, ct)), CancellationToken.None);
Apizr will send the request to the server, map the api result from UserDetails
to UserInfos
and send it back to you.
You can also map the request before being sent, like so:
// Classic auto mapped request and result
var minUser = new MinUser {Name = "John"};
var createdMinUser = await _mediator.Send(
new ExecuteRequest<IReqResService, MinUser, User>((ct, api, mapper) =>
api.CreateUser(mapper.Map<MinUser, User>(minUser), ct)), CancellationToken.None);
minUser
will be mapped from MinUser
to User
just before being sent, then Apizr will map the api result back from User
to MinUser
and send it back to you.
And yes, all the mapping feature works also with Optional.