Start modelling your app today.

Get started for free

What's this?

Exception handling with C#Bot

This article explores how C#Bot handles exceptions


General

Exceptions in C#Bot are handled at two levels:

  • Pre-filter
  • Post-filter

Pre-filter

Pre-filter exception handling happens before authentication and before reaching GraphQL.

Exceptions that occur pre-filter are not handled by GraphQL so require custom JSON so that the client-side can respond appropriately.

Post-filter

The majority of exceptions occur post-filter. Post-filter exceptions are related to queries, mutations and all the associated underlying services and other related classes.

Post-filter exceptions are handled by GraphQL.

Post-filter Exception hierarchy

There is no centralized place where all exceptions are handled.

All post filter exceptions are currently caught in resolve functions and handled in the same way as the following code which is the file serverside\src\Graphql\Fields\CreateMutation.cs. This piece of code is an example of doing this handling:

public static Func<ResolveFieldContext<object>, Task<object>> CreateCreateMutation<TModel>(string name)
    where TModel : class, IOwnerAbstractModel, new()
{
    return async context =>
    {
        ...

        try
        {
            return await crudService.Create(models, new UpdateOptions
            {
                MergeReferences = mergeReferences
            });
        }
        catch (AggregateException exception)
        {
            context.Errors.AddRange(
                exception.InnerExceptions.Select(error => new ExecutionError(error.Message)));
            return new List<TModel>();
        }
    };
}

Within this way, the AggregateException thrown by service layer methods are caught, transformed to appropriate GraphQL errors, and added into context.Errors with type ExecutionErrors. This is done so that it can be parsed by the client-side for handling.

The GraphQlController will get these errors in the code shown below and return it directly to client-side.

public async Task<ExecutionResult> Post(
    [BindRequired, FromBody] PostBody body,
    CancellationToken cancellation)
{
    var user = await _userService.GetUserFromClaim(User);
    ExecutionResult result = await _graphQlService.Execute(body.Query, body.OperationName, body.Variables, user, cancellation);
    if (result.Errors?.Count > 0)
    {
        Response.StatusCode = (int)HttpStatusCode.BadRequest;
    }

    return result;
}

Best Practices of Exception

Exceptions should never swallowed

It is important to handle all errors and not hide them within the code.

For example, the following is called swallowing exceptions and is considered bad practice:

try 
{
    // Code that throws an exception
} catch (Exception e) 
{
    // Nothing here
}

Another example of swallowing exceptions:

try 
{
    // Code that throws an exception
} catch (Exception e) 
{
    throw new CustomException("Something went wrong");
}

This is bad because it discards all the data (i.e the stack trace) of the original exception.

The correct approach to handling exceptions is to either deal with them in the moment or delegate them to a upper level class.

An example of delegation would be as follows:

try 
{
    // Code that throws an exception
} catch (Exception e) 
{
    // Put back the original exception when throw the exception
    throw new CustomException("Something went wrong", e);
}

This allows the details of the original exception to be maintained and the actual handling of the exception to be managed by an upper level exception handler.

The correct handling of exceptions is necessary to ensure your code can be debugged easily and errors do not occur without your knowledge. It is also important to log errors as they occur.

Exceptions should be exceptional

To be proactive in preventing possible exceptions, check for issues before they can cause an exception.

Common exceptions that can be avoided include NullPointerException and IndexOutOfBoundsException.

An example of proactively avoiding exceptions is to validate assumptions before your business logic is performed.

Good example:

if (obj != null) 
{
    // Business logic on obj.
}

Bad example:

try 
{
    // Business logic on obj
} catch(NullPointerException e)
{
    ...
}

Adding custom exceptions

Base Class Exception

This class is the base class for all exceptions.

It can be directly used for throwing an general type exception, or to catch any exceptions.

try
{
    throw new Exception("You got an error");
}
catch(Exception e)
{
    var message = e.Message;
}

It can have InnerException

try
{
    throw new Exception("Entity creation failed", new Exception("Duplicated key value"));
}
catch(Exception e)
{
    var errMessage = e.Message;
    var innerErrMessage = e.InnerException?.Message;
}

Class AggregateException

This is an exception type which represents one or more errors that occur during application execution. You can throw multiple errors together.

try
{
    throw new AggregateException(errors.Select(error => new InvalidOperationException(error)));
}
catch(Exception e)
{
    var errMessage = e.Message;
    var innerErrMessageList = e.InnerExceptions?.Select(err => err.Message).ToList();
}

Customized Exceptions

You can make you own exception classes for specific categories of exceptions.

For example: IdentityOperationException is for carrying IdentityResult error information. It is the exception from .NET Identity framework operations.

public class IdentityOperationException : Exception
{
    public IdentityResult IdentityResult { get; }

    public IdentityOperationException() : base("The identity operation was invalid")
    {
        // % protected region % [Add any extra extra constructor 1 options here] off begin
        // % protected region % [Add any extra extra constructor 1 options here] end
    }

    public IdentityOperationException(string message) : base(message)
    {
        // % protected region % [Add any extra extra constructor 2 options here] off begin
        // % protected region % [Add any extra extra constructor 2 options here] end
    }

    public IdentityOperationException(string message, Exception innerException) : base(message, innerException)
    {
        // % protected region % [Add any extra extra constructor 3 options here] off begin
        // % protected region % [Add any extra extra constructor 3 options here] end
    }

    public IdentityOperationException(IdentityResult identityResult) : base("The identity operation was invalid")
    {
        IdentityResult = identityResult;
        // % protected region % [Add any extra extra constructor 4 options here] off begin
        // % protected region % [Add any extra extra constructor 4 options here] end
    }

    // % protected region % [Add any extra methods here] off begin
    // % protected region % [Add any extra methods here] end
}

Throw it when there is an exceptional result happening, for example:

var result = await _userManager.ResetPasswordAsync(user, details.Token, details.Password);
if (!result.Succeeded)
{
    throw new IdentityOperationException(result);
}

Catch and handle it, for example, convert it into the same structure as the graphQL Error result like this:

catch (IdentityOperationException e)
{
    _logger.LogError(e.ToString());
    return BadRequest(new ApiErrorResponse(e.IdentityResult.Errors.Select(ie => ie.Description)));
}

And then return to client-side as the following structure:

{
    "errors": [
        {
            "message": "Resetting Password failed"
        }
    ]
}

Start modelling your app today.