Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize the exception handling? #91

Open
NullVoxPopuli opened this issue Apr 13, 2016 · 7 comments
Open

Customize the exception handling? #91

NullVoxPopuli opened this issue Apr 13, 2016 · 7 comments

Comments

@NullVoxPopuli
Copy link

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

@joukevandermaas
Copy link
Owner

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.

@NullVoxPopuli
Copy link
Author

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

@joukevandermaas
Copy link
Owner

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.

@NullVoxPopuli
Copy link
Author

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
Copy link
Author

ref: #93

@goo32
Copy link
Contributor

goo32 commented Jul 20, 2016

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

@joukevandermaas
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants