saule
saule copied to clipboard
Customize the exception handling?
Lets say we get a Db validation error
We get an error message like:
Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
So, EntityValidationErrors should be rendered as an error object: http://jsonapi.org/format/#error-objects
Thanks for making this issue. Can you explain what you currently get and how this is different from what you expect? What specifically would you like to customize? Please provide some examples if you can.
I tried a couple things:
My own error rendering:
[HttpPost]
[ReturnsResource(typeof (EngineResource))]
[Route("engines/")]
public object Create([FromBody] JObject engineData)
{
try
{
var engine = CurrentUser.Engines.Create(engineData);
return engine;
}
catch (System.Data.Entity.Validation.DbEntityValidationException ex)
{
var errorHandler = new DbEntityValidationExceptionHandler(ex);
var obj = errorHandler.AsErrors();
// TODO: no 422?
var response = Request.CreateResponse(HttpStatusCode.BadRequest, obj);
return response;
}
}
but when I passed invalid data I got this as a response:
{
"errors": [
{
"title": "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/vnd.api+json'. Resources must have an id.",
"code": "System.InvalidOperationException"
}
]
}
Which makes sense, I guess, cause Saule is handelling more of the response serializing than I thought.
Here is my DbEntityValidationExceptionHandler class
using System;
using System.Collections.Generic;
using System.Data.Entity.Validation;
using System.Linq;
using System.Web;
namespace API.ExceptionHandlers
{
public class DbEntityValidationExceptionHandler : ExceptionHandler
{
private DbEntityValidationException _exception;
public DbEntityValidationExceptionHandler(DbEntityValidationException e)
: base(e)
{
_exception = e;
}
public object AsErrors()
{
var errors = new List<object>();
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var dbError in _exception.EntityValidationErrors)
{
foreach (var dbErrorEntry in dbError.ValidationErrors)
{
var error = AsError(dbError, dbErrorEntry);
errors.Add(error);
}
}
var obj = new {errors = errors.ToArray()};
return obj;
}
private object AsError(DbEntityValidationResult dbError, DbValidationError dbErrorEntry)
{
var status = "422";
var detail = dbErrorEntry.ErrorMessage;
var source = new {pointer = "/data/attributes/" + dbErrorEntry.PropertyName};
return new {status = status, detail = detail, source = source};
}
}
}
Now if I remove the try/catch:
[HttpPost]
[ReturnsResource(typeof (EngineResource))]
[Route("engines/")]
public object Create([FromBody] JObject engineData)
{
var engine = CurrentUser.Engines.Create(engineData);
return engine;
}
And still post an incomplete object, I get this response:
{
"errors": [
{
"title": "Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.",
"detail": " at System.Data.Entity.Internal.InternalContext.SaveChanges()\r\n at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()\r\n at System.Data.Entity.DbContext.SaveChanges()\r\n at DAL.Models.Association.Base.Create(Object parameters)\r\n at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)\r\n at API.Controllers.EnginesController.Create(JObject engineData) in API\\Library\\Controllers\\EnginesController.cs:line 51\r\n at lambda_method(Closure , Object , Object[] )\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters)\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n at System.Web.Http.Filters.ActionFilterAttribute.<CallOnActionExecutedAsync>d__5.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at System.Web.Http.Filters.ActionFilterAttribute.<ExecuteActionFilterAsyncCore>d__0.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()\r\n at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()",
"code": "System.Data.Entity.Validation.DbEntityValidationException"
}
]
}
What I expect an entity errors object to look like is this:
{
"errors": [
{
"status": 422,
"source": { "pointer": "/data/attributes/property-name" },
"details": "The property-name field is required"
},
{
"status": 422,
"source": { "pointer": "/data/attributes/other-property-name" },
"details": "The other-property-name field is required"
}
]
}
This data is retrieved from
- exception of type
DbEntityValidationException- EntityValidationErrors[]
- ValidationErrors[]
- ErrorMessage
- PropertyName
- ValidationErrors[]
- EntityValidationErrors[]
Thanks for the detailed response. Indeed, Saule currently only supports single exceptions and doesn't do any advanced handling of them. I will see if this support can be extended somehow.
Maybe different error renderers could be added to be used in the ApiError class? so instead of doing
var json = JObject.FromObject(
error,
new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore
});
in ErrorSerializer,
You'd just do
public JObject Serialize(ApiError error)
{
var result = error.ToJObject();
return new JObject { ["errors"] = new JArray { result } };
}
and ToJObject could try different renderers based on the exception class.
Thoughts?
I can try this out in a PR, if you're interested.
ref: https://github.com/joukevandermaas/saule/pull/93
More advanced error handling is a must.... particularly for validation errors (as mentioned above). Has any progress been made on this? or workarounds?
Does #172 address this issue (at least partially)?