saule icon indicating copy to clipboard operation
saule copied to clipboard

Customize the exception handling?

Open NullVoxPopuli opened this issue 9 years ago • 7 comments

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

NullVoxPopuli avatar Apr 13 '16 16:04 NullVoxPopuli

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.

joukevandermaas avatar Apr 13 '16 21:04 joukevandermaas

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

NullVoxPopuli avatar Apr 14 '16 11:04 NullVoxPopuli

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.

joukevandermaas avatar Apr 14 '16 14:04 joukevandermaas

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.

NullVoxPopuli avatar Apr 14 '16 15:04 NullVoxPopuli

ref: https://github.com/joukevandermaas/saule/pull/93

NullVoxPopuli avatar Apr 14 '16 16:04 NullVoxPopuli

More advanced error handling is a must.... particularly for validation errors (as mentioned above). Has any progress been made on this? or workarounds?

goo32 avatar Jul 20 '16 19:07 goo32

Does #172 address this issue (at least partially)?

joukevandermaas avatar Sep 17 '17 12:09 joukevandermaas