Apizr – Part 5: Requesting with Optional pattern

apizr5

This article is part of a series called Apizr:

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:

builder => builder.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:

  • 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>>());
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 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 you
  • SendFor<TApiResponse>: send an ExecuteOptionalRequest<TWebApi, TApiResponse> for you
  • SendFor<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 you
  • SendReadOptionalQuery<TModelEntity>(TApiEntityKey key): send a ReadOptionalQuery<TModelEntity, TApiEntityKey> for you, with TModelEntity mapped with TApiEntity
  • SendReadAllOptionalQuery(): send a ReadAllOptionalQuery<TReadAllResult> for you
  • SendReadAllOptionalQuery<TModelEntityReadAllResult>(): send a ReadAllOptionalQuery<TModelEntityReadAllResult> for you, with TModelEntityReadAllResult mapped with TReadAllResult
  • SendCreateOptionalCommand(TApiEntity payload): send a CreateOptionalCommand<TApiEntity> for you
  • SendCreateOptionalCommand<TModelEntity>(TModelEntity payload): send a CreateOptionalCommand<TModelEntity> for you, with TModelEntity mapped* with TApiEntity
  • SendUpdateOptionalCommand(TApiEntityKey key, TApiEntity payload): send an UpdateOptionalCommand<TApiEntityKey, TApiEntity> for you
  • SendUpdateOptionalCommand<TModelEntity>(TApiEntityKey key, TModelEntity payload): send an UpdateOptionalCommand<TApiEntityKey, TModelEntity> for you, with TModelEntity mapped* with TApiEntity
  • SendDeleteOptionalCommand(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();
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) witch 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:
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.

You’ll find all sources and samples on GitHub.

JeremyBP

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

eleven + 6 =