Configuring Exception handling
Using Try/Catch
Here is an example of how to send a web request from an app - e.g. using Apizr in a Xamarin.Forms mobile app.
Inject IApizrManager<IYourDefinedInterface>
where you need it - e.g. into your ViewModel constructor
IList<User>? users;
try
{
var userList = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync());
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);
We catch any 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 an IO inner exception without sending the request.
Using IApizrResponse
or IApizrResponse<T>
Refit has different exception handling behavior depending on if your Refit interface methods return Task<T>
or if they return Task<IApiResponse>
, Task<IApiResponse<T>>
, or Task<ApiResponse<T>>
.
When returning Task<IApiResponse>
, Task<IApiResponse<T>>
, or Task<ApiResponse<T>>
(not Apizr
but Api
),
Refit traps any ApiException
raised by the ExceptionFactory
when processing the response, and any errors that occur when attempting to deserialize the response to ApiResponse<T>
, and populates the exception into the Error
property on ApiResponse<T>
without throwing the exception.
Then, Apizr will wrap the ApiResponse<T>
into an ApizrResponse<T>
plus some cached data if any and return it as a final response.
You can then decide what to do like so:
// Here we wrap the response into an IApiResponse<T> provided by Refit
[WebApi("https://reqres.in/api")]
public interface IReqResService
{
[Get("/users")]
Task<IApiResponse<UserList>> GetUsersAsync();
}
...
// Then we can handle the IApizrResponse<T> response comming from Apizr
var response = await _reqResManager.ExecuteAsync(api => api.GetUsersAsync());
// Log potential errors and maybe inform the user about it
if(!response.IsSuccess)
{
_logger.LogError(response.Exception);
Alert.Show("Error", response.Exception.Message);
}
// Use the data, no matter the source
if(response.Result?.Data?.Any() == true)
{
Users = new ObservableCollection<User>(response.Result.Data);
// Inform the user that data comes from cache if so
if(response.DataSource == ApizrResponseDataSource.Cache)
Toast.Show("Data comes from cache");
}
Using Action<Exception>
Instead of trycatching all the things, you may want to provide an exception handling action, thanks to WithExCatching
builder option, available at both register and request time.
You can set it thanks to this option:
// direct configuration
options => options.WithExCatching(OnException)
Configuring an exception handler at register time allows you to get some Global Exception Handling concepts right in place.
WithExCatching
builder option is available with or without using registry.
It means that you can share your exception handler globally by setting it at registry level and/or set some specific one at api level.
Here is a quite simple scenario:
var reqResUserManager = ApizrBuilder.Current.CreateManagerFor<IReqResUserService>(options => options
.WithExCatching(OnException));
private void OnException(ApizrException ex)
{
// this is a global exception handler
// called back in case of exception thrown
// while requesting with IReqResUserService managed api
}
And here is a pretty complexe scenario:
var apizrRegistry = ApizrBuilder.Current.CreateRegistry(registry => registry
.AddGroup(group => group
.AddManagerFor<IReqResUserService>(options => options
.WithExCatching(OnReqResUserException, strategy: ApizrDuplicateStrategy.Add))
.AddManagerFor<IReqResResourceService>(),
options => options.WithExCatching(OnReqResException, strategy: ApizrDuplicateStrategy.Add))
.AddManagerFor<IHttpBinService>()
.AddCrudManagerFor<User, int, PagedResult<User>, IDictionary<string, object>>(),
options => options.WithExCatching(OnException, strategy: ApizrDuplicateStrategy.Add));
private void OnException(ApizrException ex)
{
// this is a global exception handler
// called back in case of exception thrown
// while requesting with any managed api from the registry
}
private void OnReqResException(ApizrException ex)
{
// this is a group exception handler
// called back in case of exception thrown
// while requesting with any managed api from the group
}
private void OnReqResUserException(ApizrException ex)
{
// this is a dedicated exception handler
// called back in case of exception thrown
// while requesting with a specific managed api
}
Here I'm telling Apizr to:
- Call back all exception handlers in case of any exception thrown while requesting with
IReqResUserService
api - Call back
OnReqResException
andOnException
handlers in case of any exception thrown while requesting withIReqResResourceService
api - Call back only
OnException
handler in case of any exception thrown while requesting withIHttpBinService
api orUser
CRUD api
Feel free to configure your exception handlers at the level of your choice, depending on your needs. You definitly can mix it all with request option exception handling.
Note that you can mix it too with previous IApizrResponse handling.
You may notice that:
strategy
parameter let you adjust the behavior in case of mixing (default:Replace
):Ignore
: if there's another handler yet configured, ignore this oneAdd
: add/queue this handler, no matter of yet configured onesReplace
: replace all yet configured handlers by this oneMerge
: add/queue this handler, no matter of yet configured ones
letThrowOnExceptionWithEmptyCache
parameter tells Apizr to throw the actual exception if there's no cached data to return
Using Optional.Async
Here is how we could handle exceptions using Optional.Async:
var optionalUserList = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync());
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);
});
Optional is pretty cool when trying to handle nullables and exceptions, but what if we still want to write it shorter to get our request done and managed with as less code as possible. 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 getting things as short as we can with exceptions handled.
With 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 executionawait _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()) .OnResultAsync(userList => { users = userList?.Data; });
Func<TResult, ApizrException<TResult>, bool> onResult
: this function will be invoked with the returned result and potential occurred exceptionawait _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()) .OnResultAsync((userList, exception) => { users = userList?.Data; if(exception != null) throw exception; return true; });
Func<TResult, ApizrException<TResult>, Task<bool>> onResult
: this function will be invoked async with the returned result and potential occurred exceptionvar success = await _reqResOptionalMediator.SendFor(api => api.GetUsersAsync()) .OnResultAsync((userList, exception) => { users = userList?.Data; return exception != null; });
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. Of course, remember to catch throwing exceptions.
With 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 that 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 behavior)
- letThrowOnExceptionWithEmptyCache is False! (default)
- Apizr will return the empty cache data (null) which has to be handled then
- letThrowOnExceptionWithEmptyCache is True? (which is the case here)
One line of code to get all the thing done safely and shorter than ever!