带有消息的自定义错误代码页

时间:2018-08-07 18:37:01

标签: asp.net-core asp.net-core-mvc

我正在尝试创建一个自定义错误代码页,以显示在.NET Core MVC 1.1应用程序中传递给它的消息。我在Startup.cs类文件中设置了自定义错误代码页支持,然后在执行public IActionResult Example1 => NotFound("Some custom error message")的控制器中创建了一个简单视图。我希望此消息被推送到控制器,但事实并非如此。不带任何参数调用NotFound()会遇到错误控制器,但是一旦我通过消息传递,就永远不会使用该控制器,并且会显示一条简单的文本消息。

我本可以发誓过去曾经使用经典的.NET MVC做到这一点,但是已经有一段时间了。

我如何拥有显示正确错误的自定义错误代码页。对于我期望JSON响应(API动作等)的情况,我还需要控制器具有在错误期间返回标准文本或JSON响应的功能。我假设有一种方法可以使用属性来完成此操作,但是我还没有找到一种方法来执行这些任务。

2 个答案:

答案 0 :(得分:1)

您想要的东西是不可能的。当您对某条消息执行诸如return NotFound之类的操作时,仅当该消息保持不变时,该消息才会包含在响应正文中。当您执行诸如启用状态代码页之类的操作时,NotFound只会被中间件捕获,并且该请求将被简单地移交给您的错误处理操作以最终获得响应。重要的是,这意味着您的原始NotFoundResult和所有自定义消息都已被归档。

答案 1 :(得分:1)

您可以做的事情类似于StatusCodePages middleware的工作方式。该中间件允许管道重新执行模型,以允许通过常规MVC管道处理状态代码错误。因此,当您从MVC返回不成功的状态代码时,中间件会检测到该错误,然后重新执行整个管道以查找状态代码错误路由。这样,您就可以完全设计状态码错误。但是,正如克里斯·普拉特(Chris Pratt)所述,这些状态代码通常仅限于其代码。确实没有一种向其添加其他详细信息的方法。

但是我们可以做的是在该重新执行模型的基础上创建我们自己的错误处理实现。为此,我们创建了一个CustomErrorResponseMiddleware,它基本上检查CustomErrorResponseException异常,然后为我们的错误处理程序重新执行中间件管道。

// Custom exceptions that can be thrown within the middleware
public class CustomErrorResponseException : Exception
{
    public int StatusCode { get; set; }
    public CustomErrorResponseException(string message, int statusCode)
        : base(message)
    {
        StatusCode = statusCode;
    }
}

public class NotFoundResponseException : CustomErrorResponseException
{
    public NotFoundResponseException(string message)
        : base(message, 404)
    { }
}

// Custom context feature, to store information from the exception
public interface ICustomErrorResponseFeature
{
    int StatusCode { get; set; }
    string StatusMessage { get; set; }
}
public class CustomErrorResponseFeature : ICustomErrorResponseFeature
{
    public int StatusCode { get; set; }
    public string StatusMessage { get; set; }
}

// Middleware implementation
public class CustomErrorResponseMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _requestPath;

    public CustomErrorResponseMiddleware(RequestDelegate next, string requestPath)
    {
        _next = next;
        _requestPath = requestPath;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            // run the pipeline normally
            await _next(context);
        }
        catch (CustomErrorResponseException ex)
        {
            // store error information to be retrieved in the custom handler
            context.Features.Set<ICustomErrorResponseFeature>(new CustomErrorResponseFeature
            {
                StatusCode = ex.StatusCode,
                StatusMessage = ex.Message,
            });

            // backup original request data
            var originalPath = context.Request.Path;
            var originalQueryString = context.Request.QueryString;

            // set new request data for re-execution
            context.Request.Path = _requestPath;
            context.Request.QueryString = QueryString.Empty;

            try
            {
                // re-execute middleware pipeline
                await _next(context);
            }
            finally
            {
                // restore original request data
                context.Request.Path = originalPath;
                context.Request.QueryString = originalQueryString;
            }
        }
    }
}

现在,我们需要做的就是将其连接起来。因此,我们将中间件添加到Startup.Configure的开头附近:

app.UseMiddleware<CustomErrorResponseMiddleware>("/custom-error-response");

/custom-error-response是当请求自定义响应时我们正在重新执行的路由。这可以是正常的MVC控制器操作:

[Route("/custom-error-response")]
public IActionResult CustomErrorResponse()
{
    var customErrorResponseFeature = HttpContext.Features.Get<ICustomErrorResponseFeature>();

    var view = View(customErrorResponseFeature);
    view.StatusCode = customErrorResponseFeature.StatusCode;
    return view;
}

由于它使用MVC,因此还需要一个视图:

@model ICustomErrorResponseFeature
@{
    ViewData["Title"] = "Error";
}

<p>There was an error with your request:</p>
<p>@Model.StatusMessage</p>

基本上就是这些。现在,我们可以从MVC操作中抛出自定义错误响应异常来触发此操作:

// generate a 404
throw new NotFoundResponseException("This item could not be found");

// or completely custom
throw new CustomErrorResponseException("This did not work", 400);

当然,我们也可以进一步扩展,但这应该是基本思想。


如果您已经在使用StatusCodePages中间件,当您已经在StatusCodePages中间件中完全使用了这种自定义重新执行功能时,您可能会认为它是否真的必要。嗯,事实并非如此。我们也可以直接对此进行扩展。

为此,我们将只添加上下文功能,我们可以在正常执行过程中的任何时候设置上下文功能。然后,我们只返回一个状态代码,并让StatusCodePages中间件运行。然后,在其处理程序内部,我们可以查找我们的功能并使用那里的信息来扩展状态代码错误页面:

// Custom context feature
public interface IStatusCodePagesInfoFeature
{
    string StatusMessage { get; set; }
}
public class StatusCodePagesInfoFeature : IStatusCodePagesInfoFeature
{
    public string StatusMessage { get; set; }
}

// registration of the StatusCodePages middleware inside Startup.Configure
app.UseStatusCodePagesWithReExecute("/error/{0}");

// and the MVC action for that URL
[Route("/error/{code}")]
public IActionResult StatusCode(int code)
{
    var statusCodePagesInfoFeature = HttpContext.Features.Get<IStatusCodePagesInfoFeature>();

    return View(model: statusCodePagesInfoFeature?.StatusMessage);
}

除了正常的控制器操作外,我们还可以在返回状态代码之前设置该功能:

HttpContext.Features.Set<IStatusCodePagesInfoFeature>(new StatusCodePagesInfoFeature
{
    StatusMessage = "This item could not be found"
});
return NotFound();

  

太糟糕了,您无法在中间件类中拦截NotFound,Unauthorized等响应。

好的,选择三!您可以完全拦截这些响应,而不仅仅是在中间件内部,因为它们是MVC结果,不会离开MVC管道。因此,您必须在MVC筛选器管道中拦截它们。但是我们绝对可以运行一个过滤器,例如result filter,它可以修改结果。

问题在于我们仍然需要一种传递信息的方法。我们可以再次使用上下文功能,但是也可以使用MVC对象结果。因此,我们可以在MVC操作中执行以下操作:

return NotFound("The item was not found");

因此通常,该字符串将是纯文本响应。但是,在执行结果和生成响应之前,我们可以运行结果过滤器进行修改,然后返回视图结果。

public class StatusCodeResultFilter : IAsyncResultFilter
{
    public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
    {
        // retrieve a typed controller, so we can reuse its data
        if (context.Controller is Controller controller)
        {
            // intercept the NotFoundObjectResult
            if (context.Result is NotFoundObjectResult notFoundResult)
            {
                // set the model, or other view data
                controller.ViewData.Model = notFoundResult.Value;

                // replace the result by a view result
                context.Result = new ViewResult()
                {
                    StatusCode = 404,
                    ViewName = "Views/Errors/NotFound.cshtml",
                    ViewData = controller.ViewData,
                    TempData = controller.TempData,
                };
            }
            // intercept other results here…
        }

        await next();
    }
}

您所需要的只是现在Views/Errors/NotFound.cshtml上的视图,一旦注册了过滤器,一切都会神奇地起作用。

您可以通过向控制器添加[TypeFilter(typeof(StatusCodeResultFilter))]属性或单个操作来注册过滤器,也可以register it globally