Обработка ошибок в ASP.NET MVC
Взялся осваивать 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
специально допущено деление на нуль. Если я его вызову, то мне вернётся следующее представление:
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks