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:
options => options.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:
- With no api result:
ExecuteOptionalUnitRequest<TWebApi>
: execute any method from TWebApi and returns Option<Unit, ApizrException>ExecuteOptionalUnitRequest<TWebApi, TModelData, TApiData>
: execute any method from TWebApi with TModelData mapped* with TApiData and returns Option<Unit, ApizrException>
- With api result:
ExecuteOptionalResultRequest<TWebApi, TApiData>
: execute any method from TWebApi with a TApiData result and returns Option<TApiData, ApizrException<TApiData>>ExecuteOptionalResultRequest<TWebApi, TModelData, TApiData>
: execute any method from TWebApi with a TApiData mapped* to a TModelData result and returns Option<TModelData, ApizrException<TModelData>>ExecuteOptionalResultRequest<TWebApi, TModelResultData, TApiResultData, TApiRequestData, TModelRequestData>
: execute any method from TWebApi, sending TApiRequestData mapped from TModelRequestData, then returning TModelResultData mapped from TApiResultData and returns Option<TModelResultData, ApizrException<TModelResultData>>
* mapped means data mapped with AutoMapper. Please refer to Part 2 blog post.
witch ends to something like:
var optionalUserList = await _mediator.Send(new ExecuteOptionalResultRequest<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 two typed optional mediators available for each api interface (classic or CRUD), to help you write things shorter.
With classic apis, resolving/injecting IApizrOptionalMediator<TWebApi> gives you access to something shorter like:
var optionalUserList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync());
With CRUD apis, resolving/injecting IApizrCrudOptionalMediator<TApiEntity, TApiEntityKey, TReadAllResult, TReadAllParams> gives you access to something shorter like:
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) which 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 IApizrOptionalMediator _apizrOptionalMediator; private readonly IApizrOptionalMediator<IReqResService> _reqResOptionalMediator; private readonly IApizrCrudOptionalMediator _apizrCrudOptionalMediator; private readonly IApizrCrudOptionalMediator<User, int, PagedResult<User>, IDictionary<string, object>> _userOptionalMediator; public MyViewModel(IMediator mediator, IApizrOptionalMediator apizrOptionalMediator, IApizrOptionalMediator<IReqResService> reqResOptionalMediator, IApizrCrudOptionalMediator apizrCrudOptionalMediator, IApizrCrudOptionalMediator<User, int, PagedResult<User>, IDictionary<string, object>> userOptionalMediator) { _mediator = mediator; _apizrOptionalMediator = apizrOptionalMediator; _reqResOptionalMediator = reqResOptionalMediator; _apizrCrudOptionalMediator = apizrCrudOptionalMediator _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); // Or the classic api interface way with Apizr mediator and optional request var optionalUserList = await _apizrMediator.SendFor<IReqResService>(api => api.GetUsersAsync()); // Or 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); }); // Or 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); // Or the crud api interface way with Apizr crud optional mediator var optionalPagedResult = await _apizrCrudOptionalMediator.SendReadAllOptionalQuery<PagedResult<User>>(); // Or 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); }); // Or 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.