Взялся осваивать ASP.NET MVC. Лучше поздно, чем никогда.

Ну и стал смотреть, как в нём обрабатывать ошибки, точнее исключения. В ASP.NET Web Forms в глобальном классе Global.asax.cs был метод Application_Error, где можно было ловить все необработанные исключения. В ASP.NET MVC это всё тоже есть, но переход на контроллеры и представления меня несколько сбил.

Статьи по теме уже есть в достаточном количестве, но, как обычно, с ходу разобраться у меня не получилось, потому я напишу, к чему пришёл, когда постиг.

Точнее, с перехватом исключений проблем не было, тут всё то же самое - выловил, записал в журнал или в базу (отправил письмо админам):

protected void Application_Error(object sender, EventArgs e)
{
    // пишем в журнал
    write2log(string.Format("Произошла неизвестная ошибка. {0}", Server.GetLastError().Message));
}

Но я захотел возвращать также свои представления, а не стандартные “жёлтые” страницы ошибок. И вот что для этого нужно было сделать.

Во-первых, в web.config‘е я ничего не правил. В некоторых статьях пишут, что нужно добавить секции обработки кастомных ошибок, но мне это не понадобилось.

Вот какие файлы/классы потребовалось изменить (и создать):

Файлы, отвечающие за обработку ошибок

Контроллер

Идея такова: в контроллере Controllers/ErrorController.cs будет несколько методов, и каждый будет возвращать какое-то своё представление:

public class ErrorController : Controller
{
    public ViewResult Index()
    {
    	// представление по умолчанию, для исключений по моей вине
        return View();
    }

    public ViewResult AccessDenied()
    {
    	// представление для кода 403
        return View("AccessDenied");
    }

    public ViewResult NotFound()
    {
    	// представление для кода 404
        return View("NotFound");
    }

    public ViewResult HttpError()
    {
    	// представление всех остальных кодов HTTP
        return View("HttpError");
    }
}

Представления

Как видно, всего у меня 4 представления, но все я описывать не буду, хватит и одного. Возьмём представление по умолчанию для исключений, выбрасываемых по моей вине - это дефолтное Views/Error/Index.cshtml. Я изменил его следующим образом (оригинал взял из ответа на вопрос How to make custom error pages work in ASP.NET MVC 4):

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Неизвестная ошибка</h2>
<p>При выполнении операции произошла непредвиденная ошибка. Сообщите администратору приложенную ниже информацию.</p>

<img src="~/images/500.png" style="margin-top:20px; margin-bottom:20px;">

@if (Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div>
        <p>
            <b>Controller:</b> @Model.ControllerName<br />
            <b>Action:</b> @Model.ActionName<br />
            <b>Exception:</b> @Model.Exception.Message
        </p>
        <p><b>StackTrace:</b></p>
        <div style="overflow:scroll">
            <pre>@Model.Exception.StackTrace</pre>
        </div>
    </div>
}

Я знаю, что крайне не рекомендуется показывать пользователю StackTrace и прочее, но у нас ресурс внутренний, нам можно.

И да, в макете ~/Views/Shared/_Layout.cshtml у меня ещё есть разметка главной страницы (её будет видно на последнем скриншоте) - то есть возвращаемые представления будут не сами по себе страничками, а в составе общего оформления портала, и это очень круто, так как меньше стресса для пользователя :) да и вообще так приятней выглядит.

Глобальный класс приложения

В метод Application_Error класса Global.asax.cs записываю следующее (взял из статьи Exception Handling in ASP.NET MVC):

protected void Application_Error(Object sender, EventArgs e)
{
    var httpContext = ((MvcApplication)sender).Context;
    var currentController = string.Empty;
    var currentAction = string.Empty;
    var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));

    if (currentRouteData != null)
    {
        if (!string.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
        {
            currentController = currentRouteData.Values["controller"].ToString();
        }

        if (!string.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
        {
            currentAction = currentRouteData.Values["action"].ToString();
        }
    }

    // пойманное исключение
    var ex = Server.GetLastError();
    
    // тут запись в мой журнал, в этой же точке можно отправлять письма админам
    logger.Error(ex.Message);
    
    // ну а дальше подготовка к вызову подходящего метода контроллера ошибок
    var controller = new ErrorController();
    var routeData = new RouteData();
    // метод по умолчанию в контроллере
    var action = "Index";

    // если это ошибки HTTP, а не моего кода, то для них свои представления
    if (ex is HttpException)
    {
        switch (((HttpException)ex).GetHttpCode())
        {
            case 403:
                action = "AccessDenied";
                break;
            case 404:
                action = "NotFound";
                break;
            default:
                action = "HttpError";
                break;
            // можно добавить свои методы контроллера для любых кодов ошибок
        }
    }

    httpContext.ClearError();
    httpContext.Response.Clear();
    httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
    httpContext.Response.TrySkipIisCustomErrors = true;

    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = action;

    controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
    ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}

Ну и всё. Теперь если где-то я пропущу исключение, оно попадёт в этот метод, он определит подходящий метод контроллера ошибок и вызовет его, а тот вернёт нужное представление.

Например, у меня есть некий контроллер Controllers/UniversalController.cs, а в нём в методе fail специально допущено деление на нуль. Если я его вызову, то мне вернётся следующее представление:

Пример кастомного представления на возникшее исключение