Exception handling in C#Bot server-side
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 centralised 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 an 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();
}
Customised 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"
}
]
}
Related articles and links
Was this article helpful?