
This article is part of a series called Apizr:
- Apizr – Part 1: A Refit based web api client, but resilient
- Apizr – Part 2: Resilient core features
- Apizr – Part 3: More advanced features
- Apizr – Part 4: Requesting with Mediator pattern
- Apizr – Part 5: Requesting with Optional pattern (this one)
Apizr offers an integration with OptionalAsync, following the Optional pattern, for those of you guys using the extended approach with MediatR integration activated.
OptionalAsync offers a strongly typed alternative to null values that lets you:
- Avoid those pesky null-reference exceptions
- Signal intent and model your data more explicitly
- Cut down on manual null checks and focus on your domain
- It allows you to chain Task<Option<T>> and Task<Option<T, TException>> without having to use await
As there will be a dedicated Playground blog post about it, I won’t discuss further the what and why here.
In order to use it, please install its dedicated NuGet package called Apizr.Integrations.Optional.
Then tell it to Apizr by calling:
builder => builder.WithOptionalMediation()
and don’t forget to register MediatR itself as usual:
services.AddMediatR(typeof(Startup));
Everything you need to do is sending your request calling:
var result = await _mediator.Send(YOUR_REQUEST_HERE);
Where YOUR_REQUEST_HERE could be:
Classic apis:
ExecuteOptionalRequest<TWebApi>
: execute any method from TWebApi defined by an expression parameter witch returns Option<Unit, ApizrException>ExecuteOptionalRequest<TWebApi, TApiResponse>
: execute any method from TWebApi defined by an expression parameter witch returns Option<TApiResponse, ApizrException<TApiResponse>>ExecuteOptionalRequest<TWebApi, TModelResponse, TApiResponse>
: execute any method from TWebApi defined by an expression parameter witch returns Option<TModelResponse, ApizrException<TModelResponse>> where TModelResponse mapped* from TApiResponse
* mapped means data mapped with AutoMapper. Please refer to Part 2 blog post.
witch ends to something like:
var optionalUserList = await _mediator.Send(new ExecuteOptionalRequest<IReqResService, UserList>(api => api.GetUsersAsync()));
Then you’ll be able to play with the optional result like:
optionalUserList.Match(userList => { if (userList.Data != null && userList.Data.Any()) Users = new ObservableCollection<User>(userList.Data); }, e => { if (e.CachedResult?.Data != null && e.CachedResult.Data.Any()) Users = new ObservableCollection<User>(e.CachedResult.Data); });
CRUD apis:
ReadOptionalQuery<T>
: get the T entity with int and returns Option<T, ApizrException<T>>ReadOptionalQuery<T, TKey>
: get the T entity with TKey and returns Option<T, ApizrException<T>>ReadAllOptionalQuery<TReadAllResult>
: get TReadAllResult with IDictionary<string, object> optional query parameters and returns Option<TReadAllResult, ApizrException<TReadAllResult>>ReadAllOptionalQuery<TReadAllParams, TReadAllResult>
: get TReadAllResult with TReadAllParams optional query parameters and returns Option<TReadAllResult, ApizrException<TReadAllResult>>CreateOptionalCommand<T>
: create a T entity and returns Option<Unit, ApizrException>UpdateOptionalCommand<T>
: update the T entity with int and returns Option<Unit, ApizrException>UpdateOptionalCommand<TKey, T>
: update the T entity with TKey and returns Option<Unit, ApizrException>DeleteOptionalCommand<T>
: delete the T entity with int and returns Option<Unit, ApizrException>DeleteOptionalCommand<T, TKey>
: delete the T entity with TKey and returns Option<Unit, ApizrException>
witch ends to something like:
var optionalPagedUsers = await _mediator.Send(new ReadAllOptionalQuery<PagedResult<User>>());
optionalPagedUsers.Match(pagedUsers => { if (pagedUsers.Data != null && pagedUsers.Data.Any()) Users = new ObservableCollection<User>(pagedUsers.Data); }, e => { if (e.CachedResult?.Data != null && e.CachedResult.Data.Any()) Users = new ObservableCollection<User>(e.CachedResult.Data); });
There’s also a typed optional mediator available for each api interface (classic or CRUD), to help you write things shorter.
With classic apis, resolving/injecting IOptionalMediator<TWebApi> gives you access to:
SendFor
: send an ExecuteOptionalRequest<TWebApi> for youSendFor<TApiResponse>
: send an ExecuteOptionalRequest<TWebApi, TApiResponse> for youSendFor<TModelResponse, TApiResponse>
: send an ExecuteOptionalRequest<TWebApi, TModelResponse, TApiResponse> for you
witch ends to something as shorter as:
var optionalUserList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync());
With CRUD apis, resolving/injecting ICrudMediator<TApiEntity, TApiEntityKey, TReadAllResult, TReadAllParams> gives you access to:
SendReadOptionalQuery(TApiEntityKey key)
: send a ReadOptionalQuery<TApiEntity, TApiEntityKey> for youSendReadOptionalQuery<TModelEntity>(TApiEntityKey key)
: send a ReadOptionalQuery<TModelEntity, TApiEntityKey> for you, with TModelEntity mapped with TApiEntitySendReadAllOptionalQuery()
: send a ReadAllOptionalQuery<TReadAllResult> for youSendReadAllOptionalQuery<TModelEntityReadAllResult>()
: send a ReadAllOptionalQuery<TModelEntityReadAllResult> for you, with TModelEntityReadAllResult mapped with TReadAllResultSendCreateOptionalCommand(TApiEntity payload)
: send a CreateOptionalCommand<TApiEntity> for youSendCreateOptionalCommand<TModelEntity>(TModelEntity payload)
: send a CreateOptionalCommand<TModelEntity> for you, with TModelEntity mapped* with TApiEntitySendUpdateOptionalCommand(TApiEntityKey key, TApiEntity payload)
: send an UpdateOptionalCommand<TApiEntityKey, TApiEntity> for youSendUpdateOptionalCommand<TModelEntity>(TApiEntityKey key, TModelEntity payload)
: send an UpdateOptionalCommand<TApiEntityKey, TModelEntity> for you, with TModelEntity mapped* with TApiEntitySendDeleteOptionalCommand(TApiEntityKey key)
: send a DeleteOptionalCommand<TApiEntity, TApiEntityKey> for you
* mapped means data mapped with AutoMapper. Please refer to Part 2 blog post.
witch ends to something as shorter as:
var optionalPagedUsers = await _userOptionalMediator.SendReadAllOptionalQuery();
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 execution
- Func<TResult, ApizrException<TResult>, bool> onResult: this function will be invoked with the returned result and potential occurred exception
- Func<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 (if configured). 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; });
Of course, remember to catch your throwing exceptions at least globaly (look at AsyncErrorHandler).
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 with onException action and return empty cache result (default: False)
This one returns result from fetch or cache (if configured), no matter of potential exception handled on the other side by an action callback
var userList = 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 like to inform the user call just failed
- Apizr will return the previous result from cache
- There’s no cached data yet!
- letThrowOnExceptionWithEmptyCache is True? (witch is the case here)
- Apizr will throw the inner exception that will be catched further by AsyncErrorHander (this is its normal behavior)
- letThrowOnExceptionWithEmptyCache is False! (default)
- Apizr will return the empty cache data (null) witch has to be handled further
- letThrowOnExceptionWithEmptyCache is True? (witch is the case here)
One line of code to get all the thing done safely and shorter than ever!
public class MyViewModel { private readonly IMediator _mediator; private readonly IOptionalMediator<IReqResService> _reqResOptionalMediator; private readonly ICrudOptionalMediator<User, int, PagedResult<User>, IDictionary<string, object>> _userOptionalMediator; public MyViewModel(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 is a dummy example presenting all the ways to play with Optional // You should choose only one of it private async Task GetUsersAsync() { ////////////////// // CLASSIC API ////////////////// // 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 => { if (e.CachedResult?.Data != null && e.CachedResult.Data.Any()) Users = new ObservableCollection<User>(e.CachedResult.Data); }); // The classic api interface way with typed optional mediator and OnResultAsync result handling extension await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).OnResultAsync(userList => { if (userList.Data != null && userList.Data.Any()) Users = new ObservableCollection<User>(userList.Data); }); // The classic api interface way with typed optional mediator and CatchAsync exception handling extension var userList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()).CatchAsync(AsyncErrorHandler.HandleException, true); if (userList.Data != null && userList.Data.Any()) Users = new ObservableCollection<User>(userList.Data); ////////////////// // CRUD API ////////////////// // 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 => { if (e.CachedResult?.Data != null && e.CachedResult.Data.Any()) Users = new ObservableCollection<User>(e.CachedResult.Data); }); // The crud api interface way with typed optional mediator and OnResultAsync result handling extension await _userOptionalMediator.SendReadAllOptionalQuery().OnResultAsync(pagedUsers => { if (pagedUsers.Data != null && pagedUsers.Data.Any()) Users = new ObservableCollection<User>(pagedUsers.Data); }); // The crud api interface way with typed optional mediator and CatchAsync exception handling extension var pagedUsers = await _userOptionalMediator.SendReadAllOptionalQuery().CatchAsync(AsyncErrorHandler.HandleException, true); if (pagedUsers.Data != null && pagedUsers.Data.Any()) Users = new ObservableCollection<User>(pagedUsers.Data); } }
In this article we’ve seen how Apizr could work with OptionalAsync.