Asp.net core 带有消息的自定义错误代码页

Asp.net core 带有消息的自定义错误代码页,asp.net-core,asp.net-core-mvc,Asp.net Core,Asp.net Core Mvc,我正在尝试创建一个自定义错误代码页,该页显示我在.NET Core MVC 1.1应用程序中传递给它的消息。我在Startup.cs类文件中设置了自定义错误代码页支持,然后在控制器中创建了一个简单的视图,该视图执行public IActionResult Example1=>NotFound(“一些自定义错误消息”)。我希望此消息被推送到控制器,但事实并非如此。在没有任何参数的情况下调用NotFound()会命中错误控制器,但只要我传递一条消息,控制器就不会被使用,并且会显示一条简单的文本消息

我正在尝试创建一个自定义错误代码页,该页显示我在.NET Core MVC 1.1应用程序中传递给它的消息。我在
Startup.cs
类文件中设置了自定义错误代码页支持,然后在控制器中创建了一个简单的视图,该视图执行
public IActionResult Example1=>NotFound(“一些自定义错误消息”)
。我希望此消息被推送到控制器,但事实并非如此。在没有任何参数的情况下调用
NotFound()
会命中错误控制器,但只要我传递一条消息,控制器就不会被使用,并且会显示一条简单的文本消息

我可以发誓我曾经用经典的.NETMVC做过这件事,但已经有一段时间了


如何让自定义错误代码页显示正确的错误。我还需要控制器能够在错误期间返回标准文本或JSON响应,以防出现JSON响应(API操作等)。我假设有一种方法可以通过属性来完成这项任务,但我还没有找到一种方法来完成这两项任务。

您想要的是不可能的。当您对一条消息执行类似return
NotFound
的操作时,该消息只有在未受干扰的情况下才会包含在响应正文中。当您执行诸如启用状态代码页之类的操作时,
NotFound
将被中间件捕获,请求将被传递给错误处理操作,以最终获得响应。重要的是,这意味着您的原始
NotFoundResult
以及任何自定义消息都已被循环归档。

您可以做的事情与该方法的工作原理类似。该中间件允许管道重新执行模型,允许通过普通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;
            }
        }
    }
}
/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;
}
当然,我们也可以进一步扩展,但这应该是基本思想


如果您已经在使用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);
}
所以通常,该字符串是纯文本响应。但是在执行结果和生成响应之前,我们可以运行一个结果过滤器来修改它并返回一个视图结果

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(statuscodereultfilter))]
属性来注册筛选器,也可以注册。

那么,向用户显示某种有意义的错误消息的最佳方法是什么?这实际上是非常不典型的。例如,对于404之类的东西,状态消息和URL就是您所需要的全部上下文。这个资源不存在。实际的404错误页面可能会有一些额外的帮助,如主页、站点地图、搜索框等的链接,以帮助用户恢复,但这些都是通用的。如果您遇到验证错误或用户需要采取某种操作的其他问题,则应通过返回视图而不是错误来处理。首先,感谢您提供详细的答复。我确实喜欢这种方法,并且考虑过做一个中间件解决方案。很遗憾,您无法在中间件类中截获NotFound、Unauthorized等响应。对于我的应用程序,我必须创建一个控制器来处理自定义错误,并且只需重定向到另一个接受消息并返回URL的页面。这将比在现有视图中进行模型验证更容易,因为我已经有了自定义视图模型验证。将它们重定向到另一个页面以获取未经授权的内容是有意义的。您可以在MVC过滤器中截获
NotFound
等响应,因此这实际上可能是一个更好的解决方案!我把这作为第三个选项添加到我的答案中。也许这对你来说是一个更好的解决办法:)啊,太好了。我以后得试试这个。如果它像我认为的那样工作,我将把它标记为可接受的答案。
// generate a 404
throw new NotFoundResponseException("This item could not be found");

// or completely custom
throw new CustomErrorResponseException("This did not work", 400);
// 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();
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();
    }
}