Apizr – Part 5: Requesting with Optional pattern

apizr5

This article is part of a series called Apizr:

Read - Documentation  Browse - Source

OPTIONAL PATTERN

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.

SETUP

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));
USING

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>>());
Then you’ll be able to play with the optional result like:
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);
});
BONUS 1

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();
BONUS 2
Optional is pretty cool when trying to handle nullables and exceptions, but  I still want to write it shorter to get my request done and managed with as less code as possible.
I mean, 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 matching boilerplate.
Fortunately, Apizr provides some dedicated extensions to help the lazy guy I am getting things as short as we can:
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 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

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

One line of code to get all the thing done safely and shorter than ever!

CONCLUSION
When Apizr works all together with MediatR and OptionalAsync, we can write things much shorter than ever, cleaner and consistent, keeping it all loosely coupled from data to views and all safe.
As a reminder, here are all the ways of using OptionalAsync and MediatR with Apizr (choose the one you want):
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.

You’ll find all sources and samples on GitHub.

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

twenty + 9 =