目前,在Web API中没有简单的方法来记录或处理全局异常(webapi1中)。一些未处理的异常可以通过exception filters进行处理,但是有许多情况exception filters无法处理。例如:
1.从控制器构造函数中抛出的异常。
2.从message handlers中抛出的异常。
3.路由过程中抛出的异常。
4.响应内容序列化过程中引发的异常。
我们希望提供一种简单、一致的方法来记录和处理这些异常(如果可能的话)。
处理异常主要有两种情况,一种是我们可以发送错误响应,另一种是我们只能记录异常。
后一种情况的一个例子是在流式响应内容中间抛出异常时;在这种情况下,发送新的响应消息太晚了,因为状态代码、头部和部分内容已经边界,所以我们只能简单地中止连接。即使无法处理异常以产生新的响应消息,我们应该仍然支持记录异常。在我们能够检测到错误的情况下,我们可以返回一个适当的错误响应,如下所示:
public IHttpActionResult GetProduct(int id)
{
var product = products.FirstOrDefault((p) => p.Id == id);
if (product == null) { return NotFound(); } return Ok(product);
}
现有选项
除了现有的exception filters之外,message handlers现在还可以用来观察所有500-level responses,但是对这些响应采取行动是困难的,因为它们缺乏关于原始错误的上下文。message handlers对于它们能够处理的情况也有一些与exception filters相同的限制。虽然Web API确实具有捕获错误条件的跟踪基础结构,但是跟踪基础结构用于诊断目的,并且不是为生产环境设计也不适合在生产环境中运行。全局exception handling and logging应该才是可以在生产期间运行并插入现有监视解决方案(例如,ELMAH)的服务。
新的解决方案概述(webapi2)
我们提供两个新的用户可替换的服务,即 IExceptionLogger and IExceptionHandler,用来记录和处理未处理的异常。这两个服务非常相似,有两个主要区别:
1、我们支持注册多个exception loggers,但只支持一个exception handler。
2、Exception loggers总是被调用,即使我们即将中止连接。只有当我们仍然能够选择要发送的响应消息时,才会调用exception handler。
两个服务都提供对异常上下文的访问,该异常上下文包含从检测到异常的点开始的相关信息,特别是HttpRequestMessage、HttpRequestContext、抛出的异常和异常源(以下详细信息)。
一、使用原则
因为在小版本中添加了该功能,所以没有破坏性的改变,影响解决方案的一个重要约束是,没有任何破坏性的改变,无论是合同类型还是行为方式。这个约束排除了我们希望针对将异常转换为500个响应的现有捕获块进行的一些清理。这个额外的清理是我们可以考虑的后续重大释放。如果这对你很重要,请在ASP.NET Web API用户语音上投票。
保持与Web API构造的一致性是Web API的过滤器流水线处理横切关注点的好方法,该关注点具有在操作特定的、控制器特定的或全局范围内应用逻辑的灵活性。
包括exception filters在内的筛选器总是具有action和controller上下文,甚至在全局范围内注册时也是如此。该契约对于过滤器(filters)是有意义的,但是它意味着exception filters,甚至是全局范围的exception filters,对于某些异常处理情况(如message handlers中的异常,其中不存在action或controller上下文)并不适合。
如果我们想使用过滤器(filters)提供的灵活范围来进行异常处理,我们仍然需要exception filters。但是,如果我们需要在controller context之外处理异常,那么我们还需要一个单独的结构用于完全的全局异常处理(一些没有控controller context和action context约束的构造)。
二、使用场景
1、Exception loggers
是解决Web API捕获的所有未处理异常的解决方案。
2、Exception handlers
是为Web API捕获的未处理异常定制所有可能的响应的解决方案。
3、Exception filters
是处理与特定action或controller相关的子集未处理异常的最简单的解决方案。
三、使用详情
ExceptionLogger和ExceptionHandler接口是采用各自Context的简单异步方法:
public interface IExceptionLogger
{
Task LogAsync(ExceptionLoggerContext context,
CancellationToken cancellationToken);
}
public interface IExceptionHandler
{
Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken);
}
我们还为这两个接口提供基类。Overriding(重写)核心(sync or async)方法是在建议的时间内记录或处理所需的全部。对于日志记录,ExceptionLogger基类将确保每个异常只调用一次核心logging方法(即使稍后它在调用堆栈上进一步传播并再次被捕获)。ExceptionHandler基类将只针对调用堆栈顶部的异常调用核心处理方法,忽略遗留嵌套的捕获块。(这些基类的简化版本在下面的附录中。)IExceptionLogger和IExceptionHandler都通过ExceptionContext接收关于异常的信息。
public class ExceptionContext
{
public Exception Exception { get; set; }
public HttpRequestMessage Request { get; set; }
public HttpRequestContext RequestContext { get; set; }
public HttpControllerContext ControllerContext { get; set; }
public HttpActionContext ActionContext { get; set; }
public HttpResponseMessage Response { get; set; }
public string CatchBlock { get; set; }
public bool IsTopLevelCatchBlock { get; set; }
}
。。。。。。。。。。。。。。。。。。。。。。。。。未完。。。。。。。。。。。。。。。。。。。。。。
For both exception loggers and exception handlers, we don't do anything to recover if the logger or handler itself throws an exception. (Other than letting the exception propagate, leave feedback at the bottom of this page if you have a better approach.) The contract for exception loggers and handlers is that they should not let exceptions propagate up to their callers; otherwise, the exception will just propagate, often all the way to the host resulting in an HTML error (like the ASP.NET's yellow screen) being sent back to the client (which usually isn't the preferred option for API callers that expect JSON or XML).
对于exception loggers和exception handlers,如果logger或handler本身抛出异常,则不执行任何恢复操作(除了让这个exception传递之外)
exception loggers和handlers的约定:它们不应该让异常传递到调用者;否则,异常将只是传递,通常服务器出现错误后一个HTML的错误(如ASP.NET的黄色屏幕)会被发送回客户端(这对于希望使用JSON或XML的API调用者通常不应该这样做)。
使-用-实-例
(也可以参考GIT上作者为Tiago Santos的代码示例:https://github.com/taigosantos/AspNet-WebApi)
一、Tracing Exception Logger
下面的异常记录器将异常数据发送到配置的跟踪源(包括VisualStudio中的调试输出窗口)。
class TraceExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
Trace.TraceError(context.ExceptionContext.Exception.ToString());
}
}
二、Custom Error Message Exception Handler
下面的内容对客户端产生自定义错误响应,包括用于联系支持的电子邮件地址。
class OopsExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new TextPlainErrorResult
{
Request = context.ExceptionContext.Request,
Content = "Oops! Sorry! Something went wrong." +"Please contact support@contoso.com so we can try to fix it."
};
}
private class TextPlainErrorResult : IHttpActionResult
{
public HttpRequestMessage Request { get; set; }
public string Content { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
response.Content = new StringContent(Content);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
Registering Exception Filters
如果使用“ASP.NET MVC 4 Web应用程序”项目模板创建项目,请将Web API配置代码放在WebApiConfig类中,位于App/_Start文件夹中:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());
// Other configuration code...
}
}
附录:以下为基类的实现细节
public class ExceptionLogger : IExceptionLogger
{
public virtual Task LogAsync(ExceptionLoggerContext context,
CancellationToken cancellationToken)
{
if (!ShouldLog(context))
{
return Task.FromResult(0);
}
return LogAsyncCore(context, cancellationToken);
}
public virtual Task LogAsyncCore(ExceptionLoggerContext context,
CancellationToken cancellationToken)
{
LogCore(context);
return Task.FromResult(0);
}
public virtual void LogCore(ExceptionLoggerContext context)
{
}
public virtual bool ShouldLog(ExceptionLoggerContext context)
{
IDictionary exceptionData = context.ExceptionContext.Exception.Data;
if (!exceptionData.Contains("MS_LoggedBy"))
{
exceptionData.Add("MS_LoggedBy", new List<object>());
}
ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);
if (!loggedBy.Contains(this))
{
loggedBy.Add(this);
return true;
}
else
{
return false;
}
}
}
public class ExceptionHandler : IExceptionHandler
{
public virtual Task HandleAsync(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
if (!ShouldHandle(context))
{
return Task.FromResult(0);
}
return HandleAsyncCore(context, cancellationToken);
}
public virtual Task HandleAsyncCore(ExceptionHandlerContext context,
CancellationToken cancellationToken)
{
HandleCore(context);
return Task.FromResult(0);
}
public virtual void HandleCore(ExceptionHandlerContext context)
{
}
public virtual bool ShouldHandle(ExceptionHandlerContext context)
{
return context.ExceptionContext.IsOutermostCatchBlock;
}
}
具体请参考:https://docs.microsoft.com/en-us/aspnet/web-api/overview/error-handling/web-api-global-error-handling