如何让ELMAH与ASP.NET MVC [HandleError]属性一起工作?

572

我正在尝试使用ELMAH在我的ASP.NET MVC应用程序中记录错误,但是当我在我的控制器上使用[HandleError]属性时,ELMAH不会在发生错误时记录任何错误。

我猜这是因为ELMAH只记录未处理的错误,而[HandleError]属性正在处理错误,因此不需要记录它。

如何修改或如何修改属性,以便ELMAH可以知道发生了错误并记录它。

编辑:让我确保每个人都理解,我知道我可以修改属性,这不是我要问的问题…使用handleerror属性时,ELMAH被绕过,这意味着它不会看到有错误发生,因为该属性已经处理了它…我想知道是否有一种方法使ELMAH看到错误并记录它,即使该属性已经处理了它…我搜索了一下,没有看到任何可以调用的方法来强制记录错误…


12
哇,我希望Jeff或Jared能够回答这个问题。他们在Stackoverflow上使用ELMAH ;) - Jon Limjap
11
嗯,奇怪——我们不使用HandleErrorAttribute——Elmah是在我们的web.config的<modules>部分设置的。使用HandleErrorAttribute有什么好处吗? - Jarrod Dixon
9
@Jarrod - 希望能看到你的 ELMAH 分支有哪些“定制化”的特点。 - Scott Hanselman
3
你可以通过将web.config中的redirectMode设置为ResponseRewrite来防止重定向。参见http://blog.turlov.com/2009/01/search-engine-friendly-error-handling.html。 - Pavel Chuchuva
6
我经常看到有关 [HandleError] 属性和 Elmah 的网页文档和帖子,但我在设置虚拟案例时没有看到这个解决方案所解决的行为(例如 Elmah 没有记录“已处理”错误)。这是因为从 Elmah.MVC 2.0.x 开始,这个自定义的 HandleErrorAttribute 不再需要,因为它已经包含在 nuget 包中了。 - plyawn
显示剩余4条评论
8个回答

511

你可以通过继承 HandleErrorAttribute 并重写其 OnException 成员(不需要复制),这样它就会在 ELMAH 中记录异常,但仅在基本实现处理它的情况下。你所需要的最小代码如下:

using System.Web.Mvc;
using Elmah;

public class HandleErrorAttribute : System.Web.Mvc.HandleErrorAttribute
{
    public override void OnException(ExceptionContext context)
    {
        base.OnException(context);
        if (!context.ExceptionHandled) 
            return;
        var httpContext = context.HttpContext.ApplicationInstance.Context;
        var signal = ErrorSignal.FromContext(httpContext);
        signal.Raise(context.Exception, httpContext);
    }
}

首先会调用基本实现,让它有机会将异常标记为已处理。然后才会发出异常信号。上述代码很简单,但如果在没有HttpContext的环境(例如测试)中使用可能会出现问题。因此,您需要更具防御性的代码(代价是稍微更长):

using System.Web;
using System.Web.Mvc;
using Elmah;

public class HandleErrorAttribute : System.Web.Mvc.HandleErrorAttribute
{
    public override void OnException(ExceptionContext context)
    {
        base.OnException(context);
        if (!context.ExceptionHandled       // if unhandled, will be logged anyhow
            || TryRaiseErrorSignal(context) // prefer signaling, if possible
            || IsFiltered(context))         // filtered?
            return;

        LogException(context);
    }

    private static bool TryRaiseErrorSignal(ExceptionContext context)
    {
        var httpContext = GetHttpContextImpl(context.HttpContext);
        if (httpContext == null)
            return false;
        var signal = ErrorSignal.FromContext(httpContext);
        if (signal == null)
            return false;
        signal.Raise(context.Exception, httpContext);
        return true;
    }

    private static bool IsFiltered(ExceptionContext context)
    {
        var config = context.HttpContext.GetSection("elmah/errorFilter")
                        as ErrorFilterConfiguration;

        if (config == null)
            return false;

        var testContext = new ErrorFilterModule.AssertionHelperContext(
                              context.Exception, 
                              GetHttpContextImpl(context.HttpContext));
        return config.Assertion.Test(testContext);
    }

    private static void LogException(ExceptionContext context)
    {
        var httpContext = GetHttpContextImpl(context.HttpContext);
        var error = new Error(context.Exception, httpContext);
        ErrorLog.GetDefault(httpContext).Log(error);
    }

    private static HttpContext GetHttpContextImpl(HttpContextBase context)
    {
        return context.ApplicationInstance.Context;
    }
}

这个第二个版本将首先尝试使用来自ELMAH的错误信号,其中包括完全配置的管道,如日志记录、邮件发送、过滤等等。如果失败,则尝试查看是否应该过滤错误。如果不能过滤,则简单地记录错误。此实现不处理电子邮件通知。如果可以发出异常信号,则会发送邮件(如果配置为这样做)。

您还需要注意,如果有多个HandleErrorAttribute实例生效,则不会发生重复记录日志,但上述两个示例应该让您开始了解。


1
太好了。我根本没有试图实现Elmah。我只是试图将我多年来使用的自己的错误报告方式与MVC良好地连接起来。你的代码给了我一个起点。+1 - Steve Wortham
19
您不需要创建HandleErrorAttribute的子类。您可以简单地实现一个IExceptionFilter,并将其与HandleErrorAttribute一起注册。此外,我不明白为什么需要在ErrorSignal.Raise(...)失败时设置后备选项。如果管道配置错误,应该进行修复。有关5个步骤的IExceptionFilter,请参考此处的第4步 - http://ivanz.com/2011/05/08/asp-net-mvc-magical-error-logging-with-elmah/。 - Ivan Zlatev
5
请问您对@IvanZlatev的回答适用性、缺点等方面能否发表评论?有人评论说他的回答更易于理解,更简洁,并且能达到与您回答相同的效果,因此应该标记为正确答案。希望您能就此发表自己的看法,以明确这些答案。 - Andrew
8
这个还有用吗,还是ELMAH.MVC已经处理了这个问题? - Romias
2
我也想知道它在今天的版本中是否仍然相关。 - refactor
显示剩余13条评论

305

很抱歉,但我认为被接受的答案有些过于繁琐。你只需要做这个:

public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
    public void OnException (ExceptionContext context)
    {
        // Log only handled exceptions, because all other will be caught by ELMAH anyway.
        if (context.ExceptionHandled)
            ErrorSignal.FromCurrentContext().Raise(context.Exception);
    }
}

然后在Global.asax.cs中注册它(顺序很重要):

public static void RegisterGlobalFilters (GlobalFilterCollection filters)
{
    filters.Add(new ElmahHandledErrorLoggerFilter());
    filters.Add(new HandleErrorAttribute());
}

3
非常好,不需要扩展HandleErrorAttribute,也不需要在BaseController上重写OnException方法。这应该是被接受的答案。 - CallMeLaNN
24
Atif Aziz 创造了 ELMAH,我会采纳他的答案。 - jamiebarrow
48
@jamiebarrow 我没有意识到这一点,但他的回答已经两年了,很可能API已经简化,以更短、更自包含的方式支持问题的使用情况。 - Ivan Zlatev
6
@Ivan Zlatev真的不能使ElmahHandledErrorLoggerFilter()工作,Elmah只记录未经处理的错误,而不是已处理的错误。我按照你提到的正确顺序注册了过滤器,你有什么想法吗? - angularrocks.com
1
@bigb 你最终弄清楚这是为什么了吗?我知道这已经过去很长时间了。 - Mark
显示剩余8条评论

16
现在NuGet中有一个ELMAH.MVC包,其中包括Atif改进的解决方案,以及一个控制器来处理MVC路由中的elmah接口(不需要再使用那个axd)。
该解决方案(以及所有这里提到的解决方案)的问题在于,无论如何elmah错误处理程序实际上都会处理错误,忽略您可能想设置为自定义错误标记或通过ErrorHandler或自己的错误处理程序设置的内容。我认为最好的解决方案是创建一个过滤器,在所有其他过滤器的末尾起作用,并记录已处理的事件。 elmah模块应该负责记录应用程序未处理的其他错误。这也将使您能够使用健康监视器和可以添加到asp.net中查看错误事件的所有其他模块。
我查看了elmah.mvc中的ErrorHandler,写下了这篇文章。
public class ElmahMVCErrorFilter : IExceptionFilter
{
   private static ErrorFilterConfiguration _config;

   public void OnException(ExceptionContext context)
   {
       if (context.ExceptionHandled) //The unhandled ones will be picked by the elmah module
       {
           var e = context.Exception;
           var context2 = context.HttpContext.ApplicationInstance.Context;
           //TODO: Add additional variables to context.HttpContext.Request.ServerVariables for both handled and unhandled exceptions
           if ((context2 == null) || (!_RaiseErrorSignal(e, context2) && !_IsFiltered(e, context2)))
           {
            _LogException(e, context2);
           }
       }
   }

   private static bool _IsFiltered(System.Exception e, System.Web.HttpContext context)
   {
       if (_config == null)
       {
           _config = (context.GetSection("elmah/errorFilter") as ErrorFilterConfiguration) ?? new ErrorFilterConfiguration();
       }
       var context2 = new ErrorFilterModule.AssertionHelperContext((System.Exception)e, context);
       return _config.Assertion.Test(context2);
   }

   private static void _LogException(System.Exception e, System.Web.HttpContext context)
   {
       ErrorLog.GetDefault((System.Web.HttpContext)context).Log(new Elmah.Error((System.Exception)e, (System.Web.HttpContext)context));
   }


   private static bool _RaiseErrorSignal(System.Exception e, System.Web.HttpContext context)
   {
       var signal = ErrorSignal.FromContext((System.Web.HttpContext)context);
       if (signal == null)
       {
           return false;
       }
       signal.Raise((System.Exception)e, (System.Web.HttpContext)context);
       return true;
   }
}

现在,在您的过滤器配置中,您想要做这样的事情:

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //These filters should go at the end of the pipeline, add all error handlers before
        filters.Add(new ElmahMVCErrorFilter());
    }

请注意,我在那里留下了一条评论,提醒人们如果他们想添加一个全局过滤器来实际处理异常,它应该放在这个最后的过滤器之前,否则您将遇到未处理的异常被 ElmahMVCErrorFilter 忽略的情况,因为它尚未被处理,应该由 Elmah 模块进行日志记录,但是下一个过滤器标记异常已被处理,模块会忽略它,导致异常永远没有进入 elmah。

现在,请确保您 webconfig 中的 elmah 的 appsettings 看起来像这样:

<add key="elmah.mvc.disableHandler" value="false" /> <!-- This handles elmah controller pages, if disabled elmah pages will not work -->
<add key="elmah.mvc.disableHandleErrorFilter" value="true" /> <!-- This uses the default filter for elmah, set to disabled to use our own -->
<add key="elmah.mvc.requiresAuthentication" value="false" /> <!-- Manages authentication for elmah pages -->
<add key="elmah.mvc.allowedRoles" value="*" /> <!-- Manages authentication for elmah pages -->
<add key="elmah.mvc.route" value="errortracking" /> <!-- Base route for elmah pages -->

这里重要的是"elmah.mvc.disableHandleErrorFilter",如果该值为false,它将使用elmah.mvc内部的处理程序来处理异常,而该处理程序将忽略您的customError设置。

这种设置允许您在类和视图中设置自己的ErrorHandler标签,同时仍通过ElmahMVCErrorFilter记录这些错误,通过在web.config中通过elmah模块添加自定义错误配置甚至编写自己的错误处理程序。您需要做的唯一一件事是记住不要在我们编写的elmah过滤器之前添加任何实际处理错误的过滤器。我忘了提到的是:elmah中不能有重复。


9
您可以采用上述代码,并进一步引入自定义控制器工厂,将HandleErrorWithElmah属性注入到每个控制器中。
有关MVC日志记录的更多信息,请查看我的博客系列。第一篇文章介绍了如何为MVC设置和运行Elmah。
文章末尾有可下载的代码链接。希望这能帮到您。 http://dotnetdarren.wordpress.com/

6
在我看来,将其放在基本控制器类上似乎要容易得多! - Nathan Taylor
2
Darren在日志记录和异常处理方面的系列文章非常值得一读!非常详尽! - Ryan Anderson

8

一种完全不同的解决方案是不使用MVC HandleErrorAttribute,而是依赖于ASP.Net错误处理,Elmah就是为此设计的。

您需要从App_Start\FilterConfig(或Global.asax)中删除默认的全局HandleErrorAttribute,然后在Web.config中设置错误页面:

<customErrors mode="RemoteOnly" defaultRedirect="~/error/" />

注意,这可以是一个MVC路由的URL,因此当发生错误时,上面的代码将重定向到ErrorController.Index操作。


这是迄今为止最简单的解决方案,而默认重定向可以是MVC操作 :) - Jeremy Cook
3
那会将其他类型的请求重定向,比如 JSON 等等。——这不太好。 - Andriy Volkov

7

我是ASP.NET MVC的新手。我遇到了同样的问题,以下是我的可行解决方案,在我的Erorr.vbhtml中(如果您只需要使用Elmah日志记录错误,则可以使用它)

@ModelType System.Web.Mvc.HandleErrorInfo

    @Code
        ViewData("Title") = "Error"
        Dim item As HandleErrorInfo = CType(Model, HandleErrorInfo)
        //To log error with Elmah
        Elmah.ErrorLog.GetDefault(HttpContext.Current).Log(New Elmah.Error(Model.Exception, HttpContext.Current))
    End Code

<h2>
    Sorry, an error occurred while processing your request.<br />

    @item.ActionName<br />
    @item.ControllerName<br />
    @item.Exception.Message
</h2> 

这很简单!


这绝对是最简单的解决方案。无需编写或注册自定义处理程序等。对我来说很有效。 - ThiagoAlves
3
任何 JSON / 非 HTML 响应都将被忽略。 - Craig Stuntz
6
这段话的意思是“这也是在视图中执行服务级功能,不应该放在这里。”我的翻译如下:此处进行的是视图层面的服务功能,不适合放在这里。 - Trevor de Koekkoek

6

对我来说,让电子邮件日志记录正常工作非常重要。经过一段时间的探索,我发现在Atif的示例中只需要增加两行代码即可实现这一需求。

public class HandleErrorWithElmahAttribute : HandleErrorAttribute
{
    static ElmahMVCMailModule error_mail_log = new ElmahMVCMailModule();

    public override void OnException(ExceptionContext context)
    {
        error_mail_log.Init(HttpContext.Current.ApplicationInstance);
        [...]
    }
    [...]
}

我希望这能对某些人有所帮助:)

3
这正是我为我的MVC网站配置所需要的!我对OnException方法进行了一点修改,以处理多个HandleErrorAttribute实例,就像Atif Aziz建议的那样:

请注意,如果有多个HandleErrorAttribute实例生效,则可能需要确保不会发生重复记录日志。

在调用基类之前,我只是检查context.ExceptionHandled,以了解是否有其他人在当前处理程序之前处理了异常。
对我有效,我发布代码以供其他人使用,并询问是否有人知道我是否忽略了什么。
希望它有用:
public override void OnException(ExceptionContext context)
{
    bool exceptionHandledByPreviousHandler = context.ExceptionHandled;

    base.OnException(context);

    Exception e = context.Exception;
    if (exceptionHandledByPreviousHandler
        || !context.ExceptionHandled  // if unhandled, will be logged anyhow
        || RaiseErrorSignal(e)        // prefer signaling, if possible
        || IsFiltered(context))       // filtered?
        return;

    LogException(e);
}

你似乎没有在调用base.OnException()周围加上“if”语句... 而且(exceptionHandledByPreviousHandler || !context.ExceptionHandled || ...)互相抵消,总是为真。我有什么遗漏吗? - joelvh
首先,我会检查在当前处理程序之前是否有其他处理程序处理了异常,并将结果存储在变量exceptionHandlerdByPreviousHandler中。然后,我会让当前处理程序自己处理异常:base.OnException(context)。 - ilmatte
首先,我会检查当前处理程序之前是否有其他处理程序处理了异常,并将结果存储在变量exceptionHandlerdByPreviousHandler中。然后,我会给当前处理程序自己处理异常的机会:base.OnException(context)。如果之前没有处理过该异常,则可能出现以下两种情况:1-由当前处理程序处理,则exceptionHandledByPreviousHandler = false且!context.ExceptionHandled = false;2-不是由当前处理程序处理,则exceptionHandledByPreviousHandler = false且!context.ExceptionHandled为true。只有第一种情况会记录日志。 - ilmatte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接