Table of Contents

Configuring Exception handling

By Try-Catching

Here is an example of how to send a request.

Inject IApizrManager<IYourDefinedInterface> where you need it

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");
    Alert.Show("Error", message);

    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.

Note that you can mix it with other handling solutions.

By returning an Api Response

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 some more infos 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
[BaseAddress("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 unhandled exceptions and maybe inform the user about it
if(!response.IsSuccess && !response.Exception.Handled)
{
   _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");
}

Note that you can mix it with other handling solutions.

By using a handling callback

Instead of trycatching everything everywhere or even managing each Api Reponse locally, you may want to provide a handling callback, 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)

While registering

Configuring an exception handling callback 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 handling callback globally by setting it at registry level and/or set some specific one at api level.

Here is a quite simple scenario:

services.AddApizrManagerFor<IReqResUserService>(options => options
                    .WithExCatching(OnException));

private bool OnException(ApizrException ex)
{
    // this is a global exception handling callback 
    if (ex.InnerException is IOException)
	{
		// handle no network exception globally for example
        Alert.Show("No network", "Please check your connection and try again");

        // Tell other exception handling callbacks that we handled it yet
		return true;
	}

    return false;
}

And here is a more complexe scenario:

services.AddApizr(
    registry => registry
        .AddManagerFor<IHttpBinService>()
        .AddCrudManagerFor<User, int, PagedResult<User>, IDictionary<string, object>>(),
        .AddGroup(
            group => group
                .AddManagerFor<IReqResResourceService>()
                .AddManagerFor<IReqResUserService>(
                    // IReqResUserService dedicated exception handling callback
                    options => options.WithExCatching(OnReqResUserException, strategy: ApizrDuplicateStrategy.Add)),

            // Group exception handling callback common to IReqResUserService & IReqResResourceService apis
            options => options.WithExCatching(OnGroupException, strategy: ApizrDuplicateStrategy.Add))

    // Global exception handling callback common to all apis
    options => options.WithExCatching(OnGlobalException, strategy: ApizrDuplicateStrategy.Add));

private bool OnGlobalException(ApizrException ex)
{
    // this is a global exception handling callback 
    // called back in case of exception thrown 
    // while requesting with any managed api from the registry
    if (ex.InnerException is IOException)
	{
		// handle no network exception globally for example
        Alert.Show("No network", "Please check your connection and try again");

        // Tell other exception handling callbacks that we handled it yet
		return true;
	}

    return false;
}

private bool OnGroupException(IServiceProvider serviceProvider, ApizrException ex)
{
    // this is a group exception handling callback 
    // called back in case of exception thrown 
    // while requesting with any managed api from the group
    if(!ex.Handled) // Not yet handled ?
    {
		// handle it here at group level, like logging things
        var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred");

        return true;
	}

    return false;
}

private async Task<bool> OnReqResUserException(ApizrException ex)
{
    // this is a dedicated exception handling callback 
    // called back in case of exception thrown 
    // while requesting with IReqResUserService managed api
    if(!ex.Handled) // Not yet handled ?
    {
		// handle it here at api level
        await whateverService.DoSomethingAsync();
        ...
        return true;
	}

    return false;
}

Here, as I registered callbacks with Add strategy, I'm telling Apizr to:

  • Call back OnGlobalException then OnGroupException then OnReqResUserException in case of any exception thrown while requesting with IReqResUserService api
  • Call back OnGlobalException then OnGroupException in case of any exception thrown while requesting with IReqResResourceService api
  • Call back only OnGlobalException in case of any exception thrown while requesting with IHttpBinService api or User CRUD api

Feel free to configure your exception handling callbacks at the level of your choice, depending on your needs. You definitly can mix it all with request option exception handling.

As I leaved the letThrowOnHandledException parameter to its default true value, Apizr will throw back the exception in the end to let you catch it for final specific handling. But you definitly can tell Apizr to not throw the final exception if yet handled, by setting letThrowOnHandledException parameter to false and then dealing with result default value.

Note that you can mix it with other handling solutions.

While requesting

Configuring an exception handling callback at request time allows you to set it at the very end, just before sending the request, like trycatching does.

public ObservableCollection<User> Users { get; set; }
...
var reqResManager = apizrRegistry.GetManagerFor<IReqResUserService>();
...
try
{
	var users = await reqResManager.ExecuteAsync((options, api) => api.GetUsersAsync(options), 
		options => options.WithExCatching<ApiResult<User>>(OnGetUsersException, strategy: ApizrDuplicateStrategy.Add));

    Users = new ObservableCollection<User>(users);
}
catch (ApizrException<ApiResult<User>> ex)
{
	// handle it here at request level
	if(!ex.Handled) // Not yet handled ?
    {
		// handle it here at result level
        ...
	}
}
...
private async Task<bool> OnGetUsersException(ApizrException<ApiResult<User>> ex)
{
    // this is a method dedicated exception handling callback 
    // called back in case of exception thrown 
    // while requesting with a specific managed api's request
    if(!ex.Handled) // Not yet handled ?
    {
		// handle it here at request level
        await NavigationService.ShowAlertAsync("Error", ex.InnerException.Message ?? "An error occurred");

        return true;
	}

    return false;
}

Here, as I set the callback with Add strategy, I'm telling Apizr to:

  • Call back any other registered exception handling callbacks (see Registering tab)
  • Then call back OnGetUsersException (e.g. to display a dedicated message or something)
  • Then throw back the final exception to catch it for specific handling

As I leaved the letThrowOnHandledException parameter to its default true value, Apizr will throw back the exception in the end to let you catch it for final specific handling. But you definitly can tell Apizr to not throw the final exception if yet handled, by setting letThrowOnHandledException parameter to false and then dealing with result default value.

Note that you can mix it with other handling solutions.

You may notice that:

  • strategy parameter let you adjust the behavior in case of mixing (default: Replace):
    • Ignore: if there's another callback yet configured, ignore this one
    • Add: add/queue this callback, no matter of yet configured ones
    • Replace: replace all yet configured callbacks by this one
    • Merge: add/queue this callback, no matter of yet configured ones
  • letThrowOnHandledException parameter tells Apizr to throw back the final exception even if it's been handled by callbacks (default: true)

By registering an exception handler

You may want to provide an exception handler class, thanks to WithExCatching builder option, available at register time.

You can define it like so:

public class MyExHandler : IApizrExceptionHandler
{
    private readonly ILogger<MyExHandler> _logger;
    private readonly ICustomService _customService;

    public MyExHandler(ILogger<MyExHandler> logger, ICustomService customService)
	{
		_logger = logger;
        _customService = customService;
	}
    
    /// <inheritdoc />
    public Task<bool> HandleAsync(ApizrException ex)
    {
        if(!ex.Handled)
        {
            // Maybe log the exception
			_logger.LogError(ex, "An error occurred");

            // Do some other stuff here
            await _customService.DoSomethingAsync();

			return true;
		}

        return false;
    }
}

Then you can register it like so:

// static configuration
var reqResUserManager = ApizrBuilder.Current.CreateManagerFor<IReqResUserService>(options => options
                    .WithExCatching(new MyExHandler(LOGGER, CUSTOM_SERVICE)));

// OR extended configuration
services.AddApizrManagerFor<IReqResUserService>(options => options
        .WithExCatching<MyExHandler>());

services.AddSingleton<MyExHandler>();

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 one
    • Add: add/queue this handler, no matter of yet configured ones
    • Replace: replace all yet configured handlers by this one
    • Merge: add/queue this handler, no matter of yet configured ones
  • letThrowOnHandledException parameter tells Apizr to throw back the final exception even if it's been handled by callbacks (default: true)

Note that you can mix it with other handling solutions.