MVC3 和 WebApi 错误处理

MVC3 and WebApi Error Handling

我正在编辑一个使用 MVC3 的旧项目。 它有一个 Global.asax 文件来处理这样的错误:

protected void Application_Error(object sender, EventArgs e)
{
    var currentController = " ";
    var currentAction = " ";
    var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(Context));

    if (currentRouteData != null)
    {
        if (currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
            currentController = currentRouteData.Values["controller"].ToString();

        if (currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
            currentAction = currentRouteData.Values["action"].ToString();
    }

    var ex = Server.GetLastError();
    var controller = new ErrorController();
    var routeData = new RouteData();
    var action = "Index";

    var code = (ex is HttpException) ? (ex as HttpException).GetHttpCode() : 500;

    switch (code)
    {
        case 400:
            action = "BadRequest";
            break;
        case 401:
            action = "Unauthorized";
            break;
        case 403:
            action = "Forbidden";
            break;
        case 404:
            action = "NotFound";
            break;
        case 500:
            action = "InternalServerError";
            break;
        default:
            action = "Index";
            break;
    }

    Server.ClearError();
    Response.Clear();
    Response.StatusCode = code;
    Response.TrySkipIisCustomErrors = true;

    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = action;

    controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
    ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}

当我的 MVC 项目中出现错误时,这可以正常工作。 还有一个基础 class 可以像这样调用外部 API:

/// <summary>
/// Used to make a Get request to a specified url
/// </summary>
/// <param name="url">The target url</param>
/// <returns>Returns a string</returns>
public async Task<string> MakeApiCall(string url)
{
    return await MakeApiCall(url, HttpMethod.GET, null);
}

/// <summary>
/// Used to make a Post request to a specified url
/// </summary>
/// <param name="url">The target url</param>
/// <param name="method">The Http method</param>
/// <param name="data">The object to send to the api</param>
/// <returns>Returns a string</returns>
public async Task<string> MakeApiCall(string url, HttpMethod method, object data)
{

    // Create our local variables
    var client = new HttpClient();
    var user = Session["AccessToken"];
    var authenticating = user == null;

    // If we are not authenticating, set our auth token
    if (!authenticating)
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Session["AccessToken"].ToString());

    // Check to see what HTTP method is being used
    switch (method)
    {
        case HttpMethod.POST:

            // If we are POSTing, then perform a post request
            return await PostRequest(client, url, data, authenticating);
        default:

            // If we are GETing, then perform a get request
            return await GetRequest(client, url);
    }
}

#region Helper methods

/// <summary>
/// Posts data to a specifed url
/// </summary>
/// <param name="client">The HttpClient used to make the api call</param>
/// <param name="url">The target url</param>
/// <param name="data">The object to send to the api</param>
/// <param name="authenticating">Used to set the content type when authenticating</param>
/// <returns>Returns a string</returns>
private async Task<string> PostRequest(HttpClient client, string url, object data, bool authenticating)
{

    // If the data is a string, then do a normal post, otherwise post as json
    var response = (data is string) ? await client.PostAsync(this.apiUrl + url, new StringContent(data.ToString())) : await client.PostAsJsonAsync(this.apiUrl + url, data);

    // If we are authenticating, set the content type header
    if (authenticating == true)
        response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");

    // Handle our response
    return await HandleResponse(response);
}

/// <summary>
/// Gets data from a specifed url
/// </summary>
/// <param name="client">The HttpClient used to make the api call</param>
/// <param name="url">The target url</param>
/// <returns>Returns a string</returns>
private async Task<string> GetRequest(HttpClient client, string url)
{

    // Perform the get request
    var response = await client.GetAsync(this.apiUrl + url);

    // Handle our response
    return await HandleResponse(response);
}

/// <summary>
/// Used to handle the api response
/// </summary>
/// <param name="response">The HttpResponseMessage</param>
/// <returns>Returns a string</returns>
private async Task<string> HandleResponse(HttpResponseMessage response)
{

    // Read our response content
    var result = await response.Content.ReadAsStringAsync();

    // If there was an error, throw an HttpException
    if (response.StatusCode != HttpStatusCode.OK)
        throw new HttpException((int)response.StatusCode, result);

    // Return our result if there are no errors
    return result;
}

#endregion

我对这种方法的问题是 HandleResponse 方法。 当进行 API 调用时,如果调用失败,它将落在这一行:

// If there was an error, throw an HttpException
if (response.StatusCode != HttpStatusCode.OK)
    throw new HttpException((int)response.StatusCode, result);

又被 Application_Error 方法在 Global.asax 中捕获。这个问题是,因为这是一个 API 调用,控制器无法重定向到 ErrorController...

所以我的问题是:

  1. 我能以某种方式忽略 Global.asax 错误处理,而只是 return JSON 以便我的 JavaScript 可以决定什么处理错误 OR
  2. 有更好的方法吗?

如有任何问题,欢迎提问。我试图确保 post 不仅仅是一堵文字墙。

更新 1

所以,我尝试使用 AttributeFilter 来帮助解决这个问题。 我使用了 2 位用户建议的 2 种方法。首先,我创建了一个自定义 Exception,如下所示:

/// <summary>
/// Custom Api Exception
/// </summary>
public class ApiException : Exception
{

    /// <summary>
    /// Default constructor
    /// </summary>
    public ApiException()
    {
    }

    /// <summary>
    /// Constructor with message
    /// </summary>
    /// <param name="message">The error message as a string</param>
    public ApiException(string message)
        : base(message)
    {
    }

    /// <summary>
    /// Constructor with message and inner exception
    /// </summary>
    /// <param name="message">The error message as a string</param>
    /// <param name="inner">The inner exception</param>
    public ApiException(string message, Exception inner)
        : base(message, inner)
    {
    }
}

然后我将基本控制器中的 HandleResponse 方法更新为如下所示:

/// <summary>
/// Used to handle the api response
/// </summary>
/// <param name="response">The HttpResponseMessage</param>
/// <returns>Returns a string</returns>
private async Task<string> HandleResponse(HttpResponseMessage response)
{

    // Read our response content
    var result = await response.Content.ReadAsStringAsync();

    // If there was an error, throw an HttpException
    if (response.StatusCode != HttpStatusCode.OK)
        throw new ApiException(result);

    // Return our result if there are no errors
    return result;
}

然后我创建了一个过滤器,我将其添加到 FilterConfig 中,如下所示:

public class ExceptionAttribute : IExceptionFilter
{

    /// <summary>
    /// Handles any exception
    /// </summary>
    /// <param name="filterContext">The current context</param>
    public void OnException(ExceptionContext filterContext)
    {

        // If our exception has been handled, exit the function
        if (filterContext.ExceptionHandled)
            return;

        // If our exception is not an ApiException
        if (!(filterContext.Exception is ApiException))
        {

            // Set our base status code
            var statusCode = (int)HttpStatusCode.InternalServerError;

            // If our exception is an http exception
            if (filterContext.Exception is HttpException)
            {

                // Cast our exception as an HttpException
                var exception = (HttpException)filterContext.Exception;

                // Get our real status code
                statusCode = exception.GetHttpCode();
            }

            // Set our context result
            var result = CreateActionResult(filterContext, statusCode);

            // Set our handled property to true
            filterContext.ExceptionHandled = true;
        }
    }

    /// <summary>
    /// Creats an action result from the status code
    /// </summary>
    /// <param name="filterContext">The current context</param>
    /// <param name="statusCode">The status code of the error</param>
    /// <returns></returns>
    protected virtual ActionResult CreateActionResult(ExceptionContext filterContext, int statusCode)
    {

        // Create our context
        var context = new ControllerContext(filterContext.RequestContext, filterContext.Controller);
        var statusCodeName = ((HttpStatusCode)statusCode).ToString();

        // Create our route
        var controller = (string)filterContext.RouteData.Values["controller"];
        var action = (string)filterContext.RouteData.Values["action"];
        var model = new HandleErrorInfo(filterContext.Exception, controller, action);

        // Create our result
        var view = SelectFirstView(context, string.Format("~/Views/Error/{0}.cshtml", statusCodeName), "~/Views/Error/Index.cshtml", statusCodeName);
        var result = new ViewResult { ViewName = view, ViewData = new ViewDataDictionary<HandleErrorInfo>(model) };

        // Return our result
        return result;
    }

    /// <summary>
    /// Gets the first view name that matches the supplied names
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="viewNames">A list of view names</param>
    /// <returns></returns>
    protected string SelectFirstView(ControllerContext context, params string[] viewNames)
    {
        return viewNames.First(view => ViewExists(context, view));
    }

    /// <summary>
    /// Checks to see if a view exists
    /// </summary>
    /// <param name="context">The current context</param>
    /// <param name="name">The name of the view to check</param>
    /// <returns></returns>
    protected bool ViewExists(ControllerContext context, string name)
    {
        var result = ViewEngines.Engines.FindView(context, name, null);

        return result.View != null;
    }
}

最后我从 Global.asax 中的 Application_Error 方法中删除了逻辑,希望它能起作用.但它没有。当出现 ApiException 时,我仍然得到正在 returned 的文档。

谁能帮帮我?

Can I somehow ignore the Global.asax error handling and just return JSON so that my JavaScript can decide what to do with the error

由于 Global.asax 是 ASP.NET 管道的一部分,因此没有本机方法可以忽略它。您也许可以求助于一些 hack,但如果您使用 MVC 和 WebApi 框架来解决问题而不是依赖过时的 ASP.NET 行为会更好。

Is there a better way of doing this?

您可以在两个 MVC and in WebApi 中使用异常过滤器。这些框架中的每一个都有自己单独的配置,这将允许您将每个异常过滤器堆栈的逻辑分开。

如果你想用最少的代码完成你想做的事情,那么你可以做的不是抛出一个 HttpException 你可以 return 一个序列化的 JSON以字符串形式表示您的异常(因为您的方法 returns 字符串)如下所示:

if (response.StatusCode != HttpStatusCode.OK)
    JsonConvert.SerializeObject("{ StatusCode : " + response.StatusCode.ToString() + "}");

显然,这是一种 hack,不推荐的做法,但它不会设置您的 Application_Error,您也可以回复 JSON 给您的客户端代码。

更好的选择是将您的代码重构为 return HttpResponseMessage 或使用过滤器属性等