使用Server.TransferRequest()传递视图模型

6

我正在优化我的MVC应用程序中的错误处理。

我已经在web.config中启用了自定义错误,并添加了以下代码到Application_Error中。

Global.asax

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError() as Exception;
    if (exception != null)
    {
        Context.ClearError();
        Context.Response.TrySkipIisCustomErrors = true;

        string path = (exception is HttpException && (exception as HttpException).GetHttpCode() == 404) ?
            "~/Error/NotFound" :
            "~/Error/Index";
        Context.Server.TransferRequest(path, false);
    }
}

ErrorController.cs

[AllowAnonymous]
public ActionResult Index()
{
    Response.Clear();
    Response.StatusCode = 503;
    Response.TrySkipIisCustomErrors = true;
    return View();
}

[AllowAnonymous]
public ActionResult NotFound()
{
    Response.Clear();
    Response.StatusCode = 404;
    Response.TrySkipIisCustomErrors = true;
    return View();
}

Web.config

<system.web>
  <customErrors mode="RemoteOnly" defaultRedirect="~/Error">
    <error statusCode="404" redirect="~/Error/NotFound"/>
    <error statusCode="500" redirect="~/Error" />
  </customErrors>
</system.web>

这看起来运行得相当顺利。但是我该如何将一些错误详细信息传递给我的错误控制器?

此外,如果在控制器内发生异常,有什么提示可以将异常详细信息传递给我的错误控制器。

注意:我不想在此处使用重定向。这样做会向像Google这样的爬虫提供关于URL的错误信息。

6个回答

6
如果您想在错误控制器中获取错误详细信息,则不要在Application_Error函数内清除错误详细信息(Context.ClearError())。
一旦您进入ErrorController操作,请再次获取最后一个错误,然后将其清除。
HttpContext.Server.GetLastError()

如果您想获取异常发生时的控制器和操作名称,您可以使用以下代码获取详细信息:

Request.RequestContext.RouteData.Values["controller"]
Request.RequestContext.RouteData.Values["action"]

如果你想从Application_Error函数中运行ErrorController和Specific Action,可以按照以下步骤进行操作。

   protected void Application_Error()
   {

Exception exception = Server.GetLastError();
var httpException = exception as HttpException;
Response.Clear();
Server.ClearError();
var routeData = new RouteData();
routeData.Values["controller"] = "Errors";
routeData.Values["action"] = "Common";
routeData.Values["exception"] = exception;
Response.StatusCode = 500;
if (httpException != null)
{
  Response.StatusCode = httpException.GetHttpCode();
  switch (Response.StatusCode)
{
  case 403:
    routeData.Values["action"] = "Http403";
    break;
  case 404:
    routeData.Values["action"] = "Http404";
    break;
  case 400:
    routeData.Values["action"] = "Http400";
    break;
  }
}

Response.TrySkipIisCustomErrors = true;
IController errorsController = new ErrorsController();
var rc = new RequestContext(new HttpContextWrapper(Context), routeData);

/* This will run specific action without redirecting */
errorsController.Execute(rc);

}

如果您想将错误作为对象传递到错误控制器,则可以添加以下额外的路由数据。

routeData.Values["errorDetail"] = httpException;


3

是否添加参数有所帮助?

  • public ActionResult Index(string errorMessage)
  • public ActionResult NotFound(string errorMessage)

然后在 Application_Error 中可能会看起来像这样 -

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError() as Exception;
    if (exception != null)
    {
        Context.ClearError();
        Context.Response.TrySkipIisCustomErrors = true;

        string path = (exception is HttpException && (exception as HttpException).GetHttpCode() == 404) ?
            "~/Error/NotFound?errorMessage="+exception.Message :
            "~/Error/Index?errorMessage="+exception.Message;
        Context.Server.TransferRequest(path, false);
    }
}

您可以根据需要添加其他参数。虽然不是最佳方法。


1
这是一种方法,但绝对不是理想的解决方案。例如,有些异常包含内部异常。它们还包含额外的信息。 - Jonathan Wood

2
一种简单的方法是像这样传递异常或ViewModel:
在你的application_error中:
HttpContext.Current.Items["Exception"] = exception;

在您的错误控制器中:
var exception = HttpContext.Current.Items["Exception"] as Exception;

注意:我不喜欢使用HttpContext。


为什么不喜欢使用 HttpContext - Jonathan Wood
System.Web是一个有问题的庞大API,正在被替换,但更重要的是HttpContext是一个单例/全局状态,非常难以测试。 - Chad Grant

1
这是我的设置。它不会重定向并且可以处理应用程序和一些已配置的IIS错误。您还可以将任何信息传递给您的错误控制器。
在Web.config中:
<system.web>
  <customErrors mode="Off" />
  ...
</system.web>

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Auto">
    <remove statusCode="403" />
    <remove statusCode="404" />
    <remove statusCode="500" />
    <error statusCode="403" responseMode="ExecuteURL" path="/Error/Display/403" />
    <error statusCode="404" responseMode="ExecuteURL" path="/Error/Display/404" />
    <error statusCode="500" responseMode="ExecuteURL" path="/Error/Display/500" />
  </httpErrors>
...
</system.webServer>

ErrorController 中(仅显示方法签名以简洁为要):
// This one gets called from Application_Error
// You can add additional parameters to this action if needed
public ActionResult Index(Exception exception)
{
   ...
}

// This one gets called by IIS (see Web.config)
public ActionResult Display([Bind(Prefix = "id")] HttpStatusCode statusCode)
{
    ...
}

此外,我有一个ErrorViewModel和一个Index视图。

Application_Error中:

protected void Application_Error(object sender, EventArgs e)
{
    var exception = Server.GetLastError();

    var httpContext = new HttpContextWrapper(Context);

    httpContext.ClearError();

    var routeData = new RouteData();
    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = "Index";
    routeData.Values["exception"] = exception;
    // Here you can add additional route values as necessary.
    // Make sure you add them as parameters to the action you're executing

    IController errorController = DependencyResolver.Current.GetService<ErrorController>();
    var context = new RequestContext(httpContext, routeData);
    errorController.Execute(context);
}

到目前为止,这是我所拥有的基本设置。它不会执行重定向(错误控制器操作从Application_Error中执行),并且处理控制器异常以及例如IIS 404(例如yourwebsite.com/blah.html)。
从这一点开始,您的ErrorController内发生的任何事情都将基于您的需求。
作为一个例子,我将添加一些关于我的实现的额外细节。就像我所说的那样,我有一个ErrorViewModel
我的ErrorViewModel:
public class ErrorViewModel
{
    public string Title { get; set; }

    public string Text { get; set; }

    // This is only relevant to my business needs
    public string ContentResourceKey { get; set; }

    // I am including the actual exception in here so that in the view,
    // when the request is local, I am displaying the exception for
    // debugging purposes.
    public Exception Exception { get; set; }
}

我的 ErrorController(相关部分):

public ActionResult Index(Exception exception)
{
    ErrorViewModel model;

    var statusCode = HttpStatusCode.InternalServerError;

    if (exception is HttpException)
    {
        statusCode = (HttpStatusCode)(exception as HttpException).GetHttpCode();

        // More details on this below
        if (exception is DisplayableException)
        {
            model = CreateErrorModel(exception as DisplayableException);
        }
        else
        {
            model = CreateErrorModel(statusCode);
            model.Exception = exception;
        }
    }
    else
    {
        model = new ErrorViewModel { Exception = exception };
    }

    return ErrorResult(model, statusCode);
}

public ActionResult Display([Bind(Prefix = "id")] HttpStatusCode statusCode)
{
    var model = CreateErrorModel(statusCode);

    return ErrorResult(model, statusCode);
}

private ErrorViewModel CreateErrorModel(HttpStatusCode statusCode)
{
    var model = new ErrorViewModel();

    switch (statusCode)
    {
        case HttpStatusCode.NotFound:
            // Again, this is only relevant to my business logic.
            // You can do whatever you want here
            model.ContentResourceKey = "error-page-404";
            break;
        case HttpStatusCode.Forbidden:
            model.Title = "Unauthorised.";
            model.Text = "Your are not authorised to access this resource.";
            break;

        // etc...
    }

    return model;
}


private ErrorViewModel CreateErrorModel(DisplayableException exception)
{
    if (exception == null)
    {
        return new ErrorViewModel();
    }

    return new ErrorViewModel
    {
        Title = exception.DisplayTitle,
        Text = exception.DisplayDescription,
        Exception = exception.InnerException
    };
}

private ActionResult ErrorResult(ErrorViewModel model, HttpStatusCode statusCode)
{
    HttpContext.Response.Clear();
    HttpContext.Response.StatusCode = (int)statusCode;
    HttpContext.Response.TrySkipIisCustomErrors = true;

    return View("Index", model);
}

在某些情况下,当出现错误时,我需要显示自定义消息。为此,我有一个自定义异常:
[Serializable]
public class DisplayableException : HttpException
{
    public string DisplayTitle { get; set; }

    public string DisplayDescription { get; set; }

    public DisplayableException(string title, string description)
        : this(title, description, HttpStatusCode.InternalServerError, null, null)
    {
    }

    public DisplayableException(string title, string description, Exception exception)
        : this(title, description, HttpStatusCode.InternalServerError, null, exception)
    {
    }

    public DisplayableException(string title, string description, string message, Exception exception)
        : this(title, description, HttpStatusCode.InternalServerError, message, exception)
    {
    }

    public DisplayableException(string title, string description, HttpStatusCode statusCode, string message, Exception inner)
        : base((int)statusCode, message, inner)
    {
        DisplayTitle = title;
        DisplayDescription = description;
    }
}

然后我像这样使用它:
catch(SomeException ex)
{
    throw new DisplayableException("My Title", "My  custom display message", "An error occurred and I must display something", ex)
}

在我的ErrorController中,我单独处理此异常,从这个DisplayableException设置ErrorViewModelTitleText属性。

似乎你的代码需要更多的错误检查,例如在一些地方检查null。你不想在错误处理代码中引入新的异常。 - Jonathan Wood
我完全同意我们不希望在异常处理代码中出现异常。然而,回顾我的代码,我认为可能会出问题的只有两个地方:ExecuteErrorAction 方法(在 HttpModule 中)或 ErrorController 中的 CreateErrorModel(DisplayableException) 重载方法。在第一种情况下,如果无法创建 ErrorController 的新实例(我认为这不太可能,并且在开发过程中会立即显现),则可能会出现问题。第二种情况是由于一个不太可能的 null 参数导致失败。你觉得呢? - Andrei Olariu
我知道“不太可能”通常不是不做某事的好理由,但对于ExecuteErrorAction的情况,我认为可以安全地假设将创建一个实例。对于第二种情况(CreateErrorModel(DisplayableException)),好吧,它更有可能发生。我之所以说“不太可能”,是因为在这种特定情况下调用方法的方式。然而,我知道这不能成为一个好的论点,因为一个方法不应该知道它被如何调用。这也适用于ExecuteErrorAction。我将更新代码以反映这一点。 - Andrei Olariu
我移除了 ExecuteErrorAction 并将代码放到了 Application_Error 中(这个方法本来就有点无意义)。我还在 CreateErrorModel 中添加了一个异常的空值检查。如果它是空的,我会返回一个新的空模型。 - Andrei Olariu

0

0

试试这样做;

        protected void Application_Error(Object sender, EventArgs e)
    {
        var exception = Server.GetLastError();
        var statusCode = exception.GetType() == typeof (HttpException) ? ((HttpException) exception).GetHttpCode() : 500;
        var routeData = new RouteData
        {
            Values =
            {
                {"controller", "Error"},
                {"action", "Index"},
                {"statusCode", statusCode},
                {"exception", exception}
            }
        };

        Server.ClearError();
        Response.TrySkipIisCustomErrors = true;

        IController errorController = new ErrorController();
        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

        Response.End();
    }
}

然后在ErrorController的Index方法中编写一些业务代码。(如果StatusCode == 400 ... else ...)


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