WebAPI 中的异常处理






4.88/5 (12投票s)
在 ASP.NET WebApi 中实现异常处理的 3 种不同方法
引言
在本文中,我将尝试解释在 ASP.NET WebApi 中实现异常处理的 3 种不同方法。我假设开发人员对 WebApi 的工作原理有基本的了解。我将使用 Visual Studio 2013 IDE 进行开发。WebApi 将托管在自托管环境中。为了托管 WebApi 服务,我将使用管理员模式的控制台应用程序。需要管理员模式,因为托管机制需要访问您配置的端口。演示时,我们将使用 JSON 输出。
背景
在我正在进行的项目中,服务需要实现 WebApi 中的异常处理。因此,我决定评估 WebApi 中可用的异常处理机制。花了一天时间,我找到了三种实现异常处理的方法。它们是:
- 方法内部的异常处理
- 使用属性进行异常处理
- 使用 IHttpActionInvoker 进行全局异常处理
Using the Code
首先,让我们设置必要的开发环境来开发一个包含所有可能场景的示例。以管理员模式打开 Visual Studio,然后使用控制台模板创建一个示例应用程序。要添加对所需程序集的引用,您可以通过 Nuget 包管理器轻松安装 Microsoft.AspNet.WebApi.SelfHost
。
成功安装 WebApi.SelfHost
Nuget 后,添加以下命名空间,它们是示例应用程序所必需的。
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
在不同的层,例如业务层、数据访问层,我倾向于创建自定义异常类。此类有助于我们理解错误的类型和层。当引发业务验证异常时,它也有助于我们发送自定义错误消息。让我们创建两个样本异常类(Business
和 DataAccess
),它们都派生自 CustomException
。
public class CustomException : Exception
{
public virtual string ErrorCode { get; set; }
public virtual string ErrorDescription { get; set; }
}
public class BusinessLayerException : CustomException
{
public BusinessLayerException(string errorCode, string errorDescription)
: base()
{
base.ErrorCode = errorCode;
base.ErrorDescription = errorDescription;
}
}
public class DataLayerException : CustomException
{
public DataLayerException(string errorCode, string errorDescription)
: base()
{
base.ErrorCode = errorCode;
base.ErrorDescription = errorDescription;
}
}
自定义异常类有两个属性:ErrorCode
和 ErrorDescription
,我们将在引发自定义异常时使用它们。
让我们添加一个新的控制器类,它将向外部进程公开我们的 Web 方法。出于演示目的,我们将有三个方法:
-
GetErrorMethod
将直接从方法返回自定义错误响应。 GetHandlerException
将引发业务异常,但在此处异常将由不同的机制处理。- 第三个方法
GetGlobalErrorMessage
用于创建内部服务器错误。
public class TestController : System.Web.Http.ApiController
{
public HttpResponseMessage GetMethodError()
{
try
{
throw new Exception("Get Custom error message from Method");
}
catch (Exception Ex)
{
var errorMessagError
= new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };
throw new
HttpResponseException(ControllerContext.Request.CreateErrorResponse
(HttpStatusCode.MethodNotAllowed, errorMessagError));
}
}
[MethodAttributeExceptionHandling]
public HttpResponseMessage GetHandlerException()
{
throw new
BusinessLayerException("4001",
"Your account is in negative. please recharge");
}
public HttpResponseMessage GetGlobalErrorMessage()
{
int i = 0;
var val = 10 / i;
return new
HttpResponseMessage(HttpStatusCode.OK);
}
}
GetMethodError
在内部实现 try catch
块。此方法处理内部生成的任何错误。在生成任何错误的情况下,它将返回 HttpResponseException
。建议在使用 HttpError
返回异常时也使用它。
catch (Exception Ex)
{
var errorMessagError
= new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };
throw new
HttpResponseException
(ControllerContext.Request.CreateErrorResponse(HttpStatusCode.MethodNotAllowed, errorMessagError));
}
现在我们需要托管我们的服务,以便其他进程可以访问这些方法。托管再次分为两个部分:配置和托管机制。对于配置,我创建了一个单独的 static
类,以便所有配置逻辑都属于同一代码块。
public static class WebApiConfig
{
public static void Register(System.Web.Http.HttpConfiguration config)
{
//Route Configuration
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
//Only JSON OUPUT
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault
(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
自托管将从 Program.Main
方法进行。代码如下:
public static void Main()
{
var config = new System.Web.Http.SelfHost.HttpSelfHostConfiguration("https://:8080");
WebApiConfig.Register(config);
using (var server = new System.Web.Http.SelfHost.HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
从 URL 获取的输出:https://:8080/api/test/getMethodError 如下:
{"Message":"Get Custom error message from Method","ErrorCode":405}
GetHandlerException
触发业务异常。如果您注意到,在此方法的顶部,我添加了一个属性(MethodAttributeExceptionHandling),它负责处理由此方法引发的任何异常。此属性是我们创建的继承自 ExceptionFilterAttribute
的自定义属性。我们覆盖了 ExceptionFilterAttribute
中的 OnException
方法。在引发错误后,此方法将执行。要获取异常详细信息,您可以访问对象 actionExecutedContext.Exception
。
[MethodAttributeExceptionHandling]
public HttpResponseMessage GetHandlerException()
{
throw new
BusinessLayerException("4001", "Your account is in negative. please recharge");
}
MethodAttributeExceptionHandling
的代码如下:
public class MethodAttributeExceptionHandling : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception is BusinessLayerException)
{
var businessException = actionExecutedContext.Exception as BusinessLayerException;
var errorMessagError = new System.Web.Http.HttpError(businessException.ErrorDescription) { { "ErrorCode", businessException.ErrorCode } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
}
else if (actionExecutedContext.Exception is DataLayerException)
{
var dataException = actionExecutedContext.Exception as DataLayerException;
var errorMessagError = new System.Web.Http.HttpError(dataException.ErrorDescription) { { "ErrorCode", dataException.ErrorCode } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
}
else
{
var errorMessagError = new System.Web.Http.HttpError("Oops some internal Exception. Please contact your administrator") { { "ErrorCode", 500 } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, errorMessagError);
}
}
}
需要配置 MethodAttributeException
来处理异常。这可以通过将此对象添加到配置中来完成。
config.Filters.Add(new MethodAttributeExceptionHandling());
从 URL 获取的输出:https://:8080/api/test/GetHandlerException 如下:
{"Message":"Your account is in negative. please recharge","ErrorCode":4001}
现在,如果我们想要一个全局异常包装器怎么办?这可以通过添加继承自 ApiControllerActionInvoker 的自定义类来完成。我创建了一个自定义类 CustomApiControllerActionInvoker
,它覆盖了 InvokeActionAsync
方法。这里的逻辑与我们在 MethodAttributeExceptionHandling
中所做的类似。我们需要更改访问异常及其返回类型的方式。在这里,我们将以集合的形式获得异常,并且需要以任务的形式返回 HttpResponseMessage。
public class CustomApiControllerActionInvoker : ApiControllerActionInvoker
{
public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
var result = base.InvokeActionAsync(actionContext, cancellationToken);
if (result.Exception != null && result.Exception.GetBaseException() != null)
{
var baseException = result.Exception.InnerExceptions[0];//result.Exception.GetBaseException();
if (baseException is BusinessLayerException)
{
var baseExcept = baseException as BusinessLayerException;
var errorMessagError = new System.Web.Http.HttpError(baseExcept.ErrorDescription)
{ { "ErrorCode", baseExcept.ErrorCode } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
else if (baseException is DataLayerException)
{
var baseExcept = baseException as DataLayerException;
var errorMessagError = new System.Web.Http.HttpError(baseExcept.ErrorDescription)
{ { "ErrorCode", baseExcept.ErrorCode } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
else
{
var errorMessagError = new System.Web.Http.HttpError
("Oops some internal Exception. Please contact your administrator")
{ { "ErrorCode", 500 } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
}
return base.InvokeActionAsync(actionContext, cancellationToken);
}
}
需要使用服务来配置 CustomApiControllerActionInvoker
。这可以通过从服务替换 IHttpActionInvoker 来完成。
config.Services.Replace(typeof(IHttpActionInvoker), new CustomApiControllerActionInvoker());
请注意,一旦我们替换了 IHttpActionInvoker,MethodAttributeExceptionHandling 逻辑将无法正常工作。
从 URL 获取的输出:https://:8080/api/test/GetGlobalErrorMessage 如下:
{"Message":"Oops some internal Exception. Please contact your administrator","ErrorCode":500}
完整的代码块将如下所示:
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace TempTestConsoleApplication
{
class Program
{
public static void Main()
{
var config = new System.Web.Http.SelfHost.HttpSelfHostConfiguration("https://:8080");
WebApiConfig.Register(config);
using (var server = new System.Web.Http.SelfHost.HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to quit.");
Console.ReadLine();
}
}
}
public class CustomException : Exception
{
public virtual string ErrorCode { get; set; }
public virtual string ErrorDescription { get; set; }
}
public class BusinessLayerException : CustomException
{
public BusinessLayerException(string errorCode, string errorDescription)
: base()
{
base.ErrorCode = errorCode;
base.ErrorDescription = errorDescription;
}
}
public class DataLayerException : CustomException
{
public DataLayerException(string errorCode, string errorDescription)
: base()
{
base.ErrorCode = errorCode;
base.ErrorDescription = errorDescription;
}
}
public static class WebApiConfig
{
public static void Register(System.Web.Http.HttpConfiguration config)
{
//Route Configuration
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}"
);
config.Filters.Add(new MethodAttributeExceptionHandling());
config.Services.Replace(typeof(IHttpActionInvoker), new CustomApiControllerActionInvoker());
//Only JSON OUPUT
var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.
FirstOrDefault(t => t.MediaType == "application/xml");
config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
}
}
public class MethodAttributeExceptionHandling : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception is BusinessLayerException)
{
var businessException = actionExecutedContext.Exception as BusinessLayerException;
var errorMessagError = new System.Web.Http.HttpError(businessException.ErrorDescription)
{ { "ErrorCode", businessException.ErrorCode } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
}
else if (actionExecutedContext.Exception is DataLayerException)
{
var dataException = actionExecutedContext.Exception as DataLayerException;
var errorMessagError = new System.Web.Http.HttpError(dataException.ErrorDescription)
{ { "ErrorCode", dataException.ErrorCode } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError);
}
else
{
var errorMessagError = new System.Web.Http.HttpError("Oops some internal Exception.
Please contact your administrator") { { "ErrorCode", 500 } };
actionExecutedContext.Response =
actionExecutedContext.Request.CreateErrorResponse
(HttpStatusCode.InternalServerError, errorMessagError);
}
}
}
public class CustomApiControllerActionInvoker : ApiControllerActionInvoker
{
public override Task<HttpResponseMessage>
InvokeActionAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
var result = base.InvokeActionAsync(actionContext, cancellationToken);
if (result.Exception != null && result.Exception.GetBaseException() != null)
{
var baseException = result.Exception.InnerExceptions[0];//result.Exception.GetBaseException();
if (baseException is BusinessLayerException)
{
var baseExcept = baseException as BusinessLayerException;
var errorMessagError = new System.Web.Http.HttpError
(baseExcept.ErrorDescription) { { "ErrorCode", baseExcept.ErrorCode } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
else if (baseException is DataLayerException)
{
var baseExcept = baseException as DataLayerException;
var errorMessagError = new System.Web.Http.HttpError
(baseExcept.ErrorDescription) { { "ErrorCode", baseExcept.ErrorCode } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
else
{
var errorMessagError = new System.Web.Http.HttpError
("Oops some internal Exception. Please contact your administrator")
{ { "ErrorCode", 500 } };
return Task.Run<HttpResponseMessage>(() =>
actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, errorMessagError));
}
}
return base.InvokeActionAsync(actionContext, cancellationToken);
}
}
public class TestController : System.Web.Http.ApiController
{
public HttpResponseMessage GetMethodError()
{
try
{
throw new Exception("Get Custom error message from Method");
}
catch (Exception Ex)
{
var errorMessagError
= new System.Web.Http.HttpError(Ex.Message) { { "ErrorCode", 405 } };
throw new
HttpResponseException(ControllerContext.Request.CreateErrorResponse
(HttpStatusCode.MethodNotAllowed, errorMessagError));
}
}
[MethodAttributeExceptionHandling]
public HttpResponseMessage GetHandlerException()
{
throw new
BusinessLayerException("4001",
"Your account is in negative. please recharge");
}
public HttpResponseMessage GetGlobalErrorMessage()
{
int i = 0;
var val = 10 / i;
return new
HttpResponseMessage(HttpStatusCode.OK);
}
}
}
关注点
在上面的示例中,我展示了三种处理 WebApi 中异常的不同方法。这三种方法是:
- 方法内部的异常处理
- 使用属性进行异常处理
- 使用
IHttpActionInvoker
进行全局异常处理
您还可以继续评估 Delegate Handlers 以实现异常处理。