He pasado algunas horas resolviendo este problema. Mi solución se basa en los siguientes deseos / requisitos:
- No tenga un código repetitivo de manejo de errores repetitivo en todas las acciones del controlador JSON.
- Conservar los códigos de estado HTTP (error). ¿Por qué? Porque las preocupaciones de nivel superior no deberían afectar la implementación de nivel inferior.
- Poder obtener datos JSON cuando ocurra un error / excepción en el servidor. ¿Por qué? Porque es posible que desee información detallada sobre errores. Por ejemplo, mensaje de error, código de estado de error específico del dominio, seguimiento de pila (en entorno de depuración / desarrollo).
- Facilidad de uso del lado del cliente: es preferible utilizar jQuery.
Creo un HandleErrorAttribute (consulte los comentarios del código para obtener una explicación de los detalles). Se han omitido algunos detalles, incluidos los "usos", por lo que es posible que el código no se compile. Agrego el filtro a los filtros globales durante la inicialización de la aplicación en Global.asax.cs así:
GlobalFilters.Filters.Add(new UnikHandleErrorAttribute());
Atributo:
namespace Foo
{
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class FooHandleErrorAttribute : HandleErrorAttribute
{
private readonly TraceSource _TraceSource;
public FooHandleErrorAttribute(TraceSource traceSource)
{
if (traceSource == null)
throw new ArgumentNullException(@"traceSource");
_TraceSource = traceSource;
}
public TraceSource TraceSource
{
get
{
return _TraceSource;
}
}
public FooHandleErrorAttribute()
{
var className = typeof(FooHandleErrorAttribute).FullName ?? typeof(FooHandleErrorAttribute).Name;
_TraceSource = new TraceSource(className);
}
public override void OnException(ExceptionContext filterContext)
{
var actionMethodInfo = GetControllerAction(filterContext.Exception);
if(actionMethodInfo == null) return;
var controllerName = filterContext.Controller.GetType().FullName;
var actionName = actionMethodInfo.Name;
var traceMessage = string.Format(@"Unhandled exception from {0}.{1} handled in {2}. Exception: {3}", controllerName, actionName, typeof(FooHandleErrorAttribute).FullName, filterContext.Exception);
_TraceSource.TraceEvent(TraceEventType.Error, TraceEventId.UnhandledException, traceMessage);
if (actionMethodInfo.ReturnType != typeof(JsonResult)) return;
var jsonMessage = FooHandleErrorAttributeResources.Error_Occured;
if (filterContext.Exception is MySpecialExceptionWithUserMessage) jsonMessage = filterContext.Exception.Message;
filterContext.Result = new JsonResult
{
Data = new
{
message = jsonMessage,
stacktrace = MyEnvironmentHelper.IsDebugging ? filterContext.Exception.StackTrace : null
},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
base.OnException(filterContext);
}
private static MethodInfo GetControllerAction(Exception exception)
{
var stackTrace = new StackTrace(exception);
var frames = stackTrace.GetFrames();
if(frames == null) return null;
var frame = frames.FirstOrDefault(f => typeof(IController).IsAssignableFrom(f.GetMethod().DeclaringType));
if (frame == null) return null;
var actionMethod = frame.GetMethod();
return actionMethod as MethodInfo;
}
}
}
Desarrollé el siguiente complemento jQuery para facilitar el uso del lado del cliente:
(function ($, undefined) {
"using strict"
$.FooGetJSON = function (url, data, success, error) {
/// <summary>
/// **********************************************************
/// * UNIK GET JSON JQUERY PLUGIN. *
/// **********************************************************
/// This plugin is a wrapper for jQuery.getJSON.
/// The reason is that jQuery.getJSON success handler doesn't provides access to the JSON object returned from the url
/// when a HTTP status code different from 200 is encountered. However, please note that whether there is JSON
/// data or not depends on the requested service. if there is no JSON data (i.e. response.responseText cannot be
/// parsed as JSON) then the data parameter will be undefined.
///
/// This plugin solves this problem by providing a new error handler signature which includes a data parameter.
/// Usage of the plugin is much equal to using the jQuery.getJSON method. Handlers can be added etc. However,
/// the only way to obtain an error handler with the signature specified below with a JSON data parameter is
/// to call the plugin with the error handler parameter directly specified in the call to the plugin.
///
/// success: function(data, textStatus, jqXHR)
/// error: function(data, jqXHR, textStatus, errorThrown)
///
/// Example usage:
///
/// $.FooGetJSON('/foo', { id: 42 }, function(data) { alert('Name :' + data.name)
/// </summary>
// Call the ordinary jQuery method
var jqxhr = $.getJSON(url, data, success)
// Do the error handler wrapping stuff to provide an error handler with a JSON object - if the response contains JSON object data
if (typeof error !== "undefined") {
jqxhr.error(function(response, textStatus, errorThrown) {
try {
var json = $.parseJSON(response.responseText)
error(json, response, textStatus, errorThrown)
} catch(e) {
error(undefined, response, textStatus, errorThrown)
}
})
}
// Return the jQueryXmlHttpResponse object
return jqxhr
}
})(jQuery)
¿Qué obtengo de todo esto? El resultado final es que
- Ninguna de las acciones de mi controlador tiene requisitos en HandleErrorAttributes.
- Ninguna de las acciones de mi controlador contiene un código repetitivo de manejo de errores de la placa de la caldera.
- Tengo un solo punto de código de manejo de errores que me permite cambiar fácilmente el registro y otras cosas relacionadas con el manejo de errores.
- Un requisito simple: las acciones del controlador que devuelven JsonResult deben tener el tipo de retorno JsonResult y no algún tipo base como ActionResult. Razón: Ver comentario de código en FooHandleErrorAttribute.
Ejemplo del lado del cliente:
var success = function(data) {
alert(data.myjsonobject.foo);
};
var onError = function(data) {
var message = "Error";
if(typeof data !== "undefined")
message += ": " + data.message;
alert(message);
};
$.FooGetJSON(url, params, onSuccess, onError);
¡Los comentarios son bienvenidos! Probablemente algún día escribiré en un blog sobre esta solución ...