简体   繁体   中英

How handle exceptions from 400 to 500 in ASP.NET Core 3

I have an ASP.NET Core MVC project(the version for Core is 3) and I must handle at least two exceptions such as 404 and 500 and have a view for 404 that should say "Sorry, The page cannot be found" and else another page for error 500 that should say "An error occurred while processing your request". These pages must have DefaultLayout. My Startup.cs is as below:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddDistributedMemoryCache();
        services.AddSession();
        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        //services.AddScoped<Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration>();
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseStatusCodePages(async context =>
        {
            context.HttpContext.Response.ContentType = "text/plain";

            await context.HttpContext.Response.WriteAsync(
                "Status code page, status code: " +
                context.HttpContext.Response.StatusCode);
        });

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error/500");

            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.Use(async (ctx, next) =>
        {
            await next();

            if (ctx.Response.StatusCode == 404 && !ctx.Response.HasStarted)
            {
                //Re-execute the request so the user gets the error page
                string originalPath = ctx.Request.Path.Value;
                ctx.Items["originalPath"] = originalPath;
                ctx.Request.Path = "/error/404";
                await next();
            }
        });

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();
        app.UseSession();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });

    }
}

and I have made an errorcontroller as below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace AgentRegister.Controllers
{
    [Route("error")]
    public class ErrorController : Controller
    {
        private readonly TelemetryClient _telemetryClient;

        public ErrorController(TelemetryClient telemetryClient)
        {
            _telemetryClient = telemetryClient;
        }
        [Route("500")]
        public IActionResult AppError()
        {
            var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            _telemetryClient.TrackException(exceptionHandlerPathFeature.Error);
            _telemetryClient.TrackEvent("Error.ServerError", new Dictionary<string, string>
            {
                ["originalPath"] = exceptionHandlerPathFeature.Path,
                ["error"] = exceptionHandlerPathFeature.Error.Message
            });
            return View();
        }


        [Route("404")]
        public IActionResult PageNotFound()
        {
            string originalPath = "unknown";
            if (HttpContext.Items.ContainsKey("originalPath"))
            {
                originalPath = HttpContext.Items["originalPath"] as string;
            }
            _telemetryClient.TrackEvent("Error.PageNotFound", new Dictionary<string, string>
            {
                ["originalPath"] = originalPath
            });
            return View();
        }

    }
}

How can I do this? Any help will be appriciated!

Consider simplifying your controller by moving the error-handling logic to a dedicated 'middleware' class.

Imagine a simple controller like this, all it does is define the target pages for routing, and includes an example exception to simulate a 500 error. It doesn't care about the specific error types. (Anon access is permitted for simplicity.)

The project is called TestError .

using System;    
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace TestError.Controllers
{
    [AllowAnonymous]
    public class HomeController : Controller
    {
        public HomeController() { }

        [HttpGet]
        public ViewResult Home() => View("Home");

        [HttpGet]
        public ViewResult Bogus() => throw new Exception("Bogus error");

        [HttpGet]
        public ViewResult Error() => View("Error");

        [HttpGet]
        public ViewResult PageNotFound() => View("PageNotFound");
    }
}

In Startup.cs , just above the route definitions, there is a reference to the error handler as app.UseMiddleware<ErrorHandler>();

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TestError.Infrastructure;

namespace TestError
{
    public class Startup
    {
        //   Updated this class for ASP.NET Core 3
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                 app.UseDeveloperExceptionPage();
            }
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseMiddleware<ErrorHandler>();
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(name: "Default", 
                                                pattern: "{controller=Home}/{action=Home}/{id?}");

                endpoints.MapControllerRoute(name: "Error",
                                                "error",
                                                new { controller = "Home", action = "Error" });

                endpoints.MapControllerRoute(name: "PageNotFound",
                                                "pagenotfound",
                                                new { controller = "Home", action = "PageNotFound" });
            });
        }
    }
}

Startup.cs also needs a reference as using TestError.Infrastructure; as the middleware class is created as Infrastructure\ErrorHandler.cs

The middleware class looks at the HttpContext pipeline context.Response.StatusCode and uses a switch statement, calling a custom method to respond to each error as required, and I have added a clause for a 404 error.

You can add more clauses for different error codes as needed, and append custom methods for dealing with specific circumstances, or build these out in separate classes if you want to ensure that the error handler does not become too complex and specific.

General code exceptions are handled separately as 500 errors, caught by the catch block.

using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace TestError.Infrastructure
{
    public class ErrorHandler
    {
        private readonly RequestDelegate _next;

        public ErrorHandler(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);

                //  Handle specific HTTP status codes
                switch (context.Response.StatusCode)
                {
                    case 404:
                    HandlePageNotFound(context);
                    break;

                    case 418:
                    //  Not implemented
                    break;

                    default:
                    break;
                }
            }
            catch (Exception e)
            {
                //  Handle uncaught global exceptions (treat as 500 error)
                HandleException(context, e);
            }
            finally
            {
            }
        }

        //  500
        private static void HandleException(HttpContext context, Exception e)
        {
            context.Response.Redirect("/Error");
        }

        //  404
        private static void HandlePageNotFound(HttpContext context)
        {
            //  Display an information page that displays the bad url using a cookie
            string pageNotFound = context.Request.Path.ToString().TrimStart('/');
            CookieOptions cookieOptions = new CookieOptions();
            cookieOptions.Expires = DateTime.Now.AddMilliseconds(10000);
            cookieOptions.IsEssential = true;
            context.Response.Cookies.Append("PageNotFound", pageNotFound, cookieOptions);
            context.Response.Redirect("/PageNotFound");
        }
    }
}

The HandleException method creates a redirect to the "/Error" page.

Error.cshtml is pretty much as you would expect

@{
    Layout = "~/Views/Shared/_Layout_Main.cshtml";
    ViewData["Title"] = "Error";
}
<div id="dataSection">
    <div class="TextLine Label">An error occurred while processing your request</div>
</div>

HandlePageNotFound creates a short-lived cookie to store the address of the requested page, and redirects to "/PageNotFound".

PageNotFound.cshtml references the cookie to display a meaningful error

@{
    Layout = "_Layout_Main";
    ViewData["Title"] = "Page Not Found";
    string notFoundMessage = "Sorry, The page cannot be found";
    string pageNotFound = Context.Request.Cookies["PageNotFound"];
    if (pageNotFound != null){
        notFoundMessage += " : ";
    }
}
<div id="dataSection">
    <div class="TextLine Label">@notFoundMessage<b>@pageNotFound</b></div>
</div>

Example usage, there is a homepage as follows (with a layout page)

@{
    ViewData["Title"] = "Home";
    Layout = "~/Views/Shared/_Layout_Main.cshtml";
}

<h1>Hello World</h1>

Example 1, /Home/Home : finds Home.cshtml 在此处输入图像描述 Example 2, /Nopage : redirects to PageNotFound.cshtml 在此处输入图像描述 Example 3, /Home/Bogus : throws an exception and redirects to Error.cshtml 在此处输入图像描述

Hope this helps for error-handling. There are lots of variations on this, and as mentioned, you can add more switch clauses, and even include a specific case for 500.

A more detailed example would also include some logging, which I have excluded for simplicity.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM