There are probably some other ways to do this (I don't claim to be an ASP.Net Core expert) but I have solved this problem the following way. First, define a custom exception class. The purpose is that you can actually throw this without regard to any controller method return type. Also, throwing exceptions makes control flow a lot more structured.
public class CustomApiException : Exception
{
/// <summary>
/// Optional application-specific error code returned to the client.
/// </summary>
public int? ApplicationErrorCode { get; private set; } = null;
/// <summary>
/// HTTP status code returned to the client.
/// </summary>
public HttpStatusCode HttpStatusCode { get; private set; } = HttpStatusCode.BadRequest;
public CustomApiException() : base() { }
public CustomApiException(string message) : base(message) { }
public CustomApiException(string message, HttpStatusCode httpStatusCode) : base(message)
{
HttpStatusCode = httpStatusCode;
}
public CustomApiException(string message, HttpStatusCode httpStatusCode, int? applicationErrorCode) : base(message)
{
HttpStatusCode = httpStatusCode;
ApplicationErrorCode = applicationErrorCode;
}
public CustomApiException(string message, int? applicationErrorCode) : base(message)
{
ApplicationErrorCode = applicationErrorCode;
}
}
Then define a custom ExceptionFilterAttribute. Please note that this copy/pasted snippet does a bit more than what you have been asking for. E.g. depending on the development vs. production it will include the entire stack trace of the exception (of any exception actually, not just CustomApiException).
// todo: turn into async filter.
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly ILogger<ApiExceptionFilterAttribute> _logger;
private readonly IHostingEnvironment _env;
public ApiExceptionFilterAttribute(ILogger<ApiExceptionFilterAttribute> logger, IHostingEnvironment env)
{
_logger = logger;
_env = env;
}
public override void OnException(ExceptionContext context)
{
_logger.LogError(new EventId(0), context.Exception, context.Exception.Message);
dynamic errObject = new JObject();
HttpStatusCode statusCode = HttpStatusCode.InternalServerError; // use 500 unless the API says it's a client error
if (context.Exception.GetType() == typeof(CustomApiException))
{
CustomApiException customEx = (CustomApiException)context.Exception;
if (customEx.ApplicationErrorCode != null) errObject.errorCode = customEx.ApplicationErrorCode;
errObject.errorMessage = customEx.Message;
statusCode = customEx.HttpStatusCode;
}
if (_env.IsDevelopment())
{
errObject.errorMessage = context.Exception.Message;
errObject.type = context.Exception.GetType().ToString();
errObject.stackTrace = context.Exception.StackTrace;
}
JsonResult result = new JsonResult(errObject);
result.StatusCode = (int?)statusCode;
context.Result = result;
}
}
Finally, add the custom ExceptionFilterAttribute to the global ConfigureServices method.
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//...
// Add framework services.
services.AddMvc(options =>
{
options.Filters.Add(typeof(ApiExceptionFilterAttribute));
});
}
// ...
}
It's a little bit of work but only one-off work, and pretty powerful once you have added it. If I remember correctly, my solution is based on this MS page Exception Handling. This may be of help if you have further questions.