Меню

Обработка ошибок asp net core

Обработка ошибок

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

Последнее обновление: 06.11.2019

Ошибки в приложении можно условно разделить на два типа: исключения, которые возникают в процессе выполнения кода (например, деление на 0), и
стандартные ошибки протокола HTTP (например, ошибка 404).

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

UseDeveloperExceptionPage

Если мы создаем проект ASP.NET Core, например, по типу Empty (да и в других типах проектов), то в классе Startup мы можем найти в начале метода Configure() следующие строки:

if (env.IsDevelopment())
{
	app.UseDeveloperExceptionPage();
}

Если приложение находится в состоянии разработки, то с помощью middleware app.UseDeveloperExceptionPage() приложение перехватывает исключения и
выводит информацию о них разработчику.

Например, изменим класс Startup следующим образом:

public class Startup
{
	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
	{
		if (env.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
		}
		app.Run(async (context) =>
		{
			int x = 0;
			int y = 8 / x;
			await context.Response.WriteAsync($"Result = {y}");
		});
	}
}

В middleware app.Run симулируется генерация исключения при делении ноль. И если мы запустим проект, то в браузере мы увидим
информацию об исключении:

Обработка исключений в ASP.NET Core

Этой информации достаточно, чтобы определить где именно в коде произошло исключение.

Теперь посмотрим, как все это будет выглядеть для простого пользователя. Для этого изменим метод Configure:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	env.EnvironmentName = "Production";
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	app.Run(async (context) =>
	{
		int x = 0;
		int y = 8 / x;
		await context.Response.WriteAsync($"Result = {y}");
	});
}

Выражение env.EnvironmentName = "Production"; устанавливает режим развертывания вместо режима разработки. В этом случае выражение if (env.IsDevelopment()) будет возвращать false, и мы увидим в браузере что-то наподобие «HTTP ERROR 500»

HTTP ERROR 500 в ASP.NET Core

UseExceptionHandler

Это не самая лучшая ситуация, и нередко все-таки возникает необходимость дать пользователям некоторую информацию о том, что же все-таки произошло. Либо потребуется как-то обработать данную ситуацию.
Для этих целей можно использовать еще один встроенный middleware в виде метода UseExceptionHandler(). Он перенаправляет
при возникновении исключения на некоторый адрес и позволяет обработать исключение. Например, изменим метод Configure следующим образом:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	env.EnvironmentName = "Production";
	
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/error");
	}
	
	app.Map("/error", ap => ap.Run(async context =>
	{
		await context.Response.WriteAsync("DivideByZeroException occured!");
	}));
	
	app.Run(async (context) =>
	{
		int x = 0;
		int y = 8 / x;
		await context.Response.WriteAsync($"Result = {y}");
	});
}

Метод app.UseExceptionHandler("/error"); перенаправляет при возникновении ошибки на адрес «/error».

Для обработки пути по определенному адресу здесь использовался метод app.Map(). В итоге при возникновении исключения будет срабатывать делегат
из метода app.Map.

Error Handling in ASP.NET Core

Следует учитывать, что оба middleware — app.UseDeveloperExceptionPage() и app.UseExceptionHandler()
следует помещать ближе к началу конвейера middleware.

Обработка ошибок HTTP

В отличие от исключений стандартный функционал проекта ASP.NET Core почти никак не обрабатывает ошибки HTTP, например, в случае если ресурс не найден.
При обращении к несуществующему ресурсу мы увидим в браузере пустую страницу, и только через консоль веб-браузера мы сможем увидеть статусный код.
Но с помощью компонента StatusCodePagesMiddleware можно добавить в проект отправку информации о статусном коде.
Для этого добавим в метод Configure() класса Startup вызов app.UseStatusCodePages():

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	
	// обработка ошибок HTTP
	app.UseStatusCodePages();
	
	app.Map("/hello", ap => ap.Run(async (context) =>
	{
		await context.Response.WriteAsync($"Hello ASP.NET Core");
	}));
}

Здесь мы можем обращаться только по адресу «/hello». При обращении ко всем остальным адресам браузер отобразит базовую информацию об ошибке:

UseStatusCodePages в ASP.NET Core

Данный метод позволяет настроить отправляемое пользователю сообщение. В частности, мы можем изменить вызов метода так:

app.UseStatusCodePages("text/plain", "Error. Status code : {0}");

В качестве первого параметра указывается MIME-тип ответа, а в качестве второго — собственно то сообщение, которое увидит пользователь. В сообщение мы можем
передать код ошибки через плейсхолдер «{0}».

Вместо метода app.UseStatusCodePages() мы также можем использовать еще пару других, которые также обрабатываю ошибки HTTP.

С помощью метода app.UseStatusCodePagesWithRedirects() можно выполнить переадресацию на определенный метод, который непосредственно обработает статусный код:

app.UseStatusCodePagesWithRedirects("/error?code={0}");

Здесь будет идти перенаправление по адресу «/error?code={0}». В качестве параметра через плейсхолдер «{0}» будет передаваться статусный код
ошибки.

Но теперь при обращении к несуществующему ресурсу клиент получит статусный код 302 / Found. То есть формально несуществующий ресурс будет существовать, просто статусный код 302
будет указывать, что ресурс перемещен на другое место — по пути «/error/404».

Подобное поведение может быть неудобно, особенно с точки зрения поисковой индексации, и в этом случае мы можем применить другой метод
app.UseStatusCodePagesWithReExecute():

app.UseStatusCodePagesWithReExecute("/error", "?code={0}");

Первый параметр метода указывает на путь перенаправления, а второй задает параметры строки запроса, которые будут передаваться при перенаправлении.
Вместо плейсхолдера {0} опять же будет передаваться статусный код ошибки. Формально мы получим тот же ответ, так как так же будет идти перенаправление на путь «/error?code=404». Но теперь браузер получит оригинальный статусный код 404.

Пример использования:

public void Configure(IApplicationBuilder app)
{
	// обработка ошибок HTTP
	app.UseStatusCodePagesWithReExecute("/error", "?code={0}");

	app.Map("/error", ap => ap.Run(async context =>
	{
		await context.Response.WriteAsync($"Err: {context.Request.Query["code"]}");
	}));

	app.Map("/hello", ap => ap.Run(async (context) =>
	{
		await context.Response.WriteAsync($"Hello ASP.NET Core");
	}));
}

Настройка обработки ошибок в web.config

Еще один способ обработки кодов ошибок представляет собой определение и настройка в файле конфигурации web.config элемента
httpErrors. Этот способ в принципе использовался и в других версиях ASP.NET.
В ASP.NET Core он также доступен, однако имеет очень ограниченное действие. В частности, мы его можем использовать только при развертывании на IIS, а также не можем использовать ряд настроек.

Итак, добавим в корень проекта новый элемент Web Configurarion File, который естественно назовем web.config:

Обработка ошибок в web.config

Изменим его следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <system.webServer>
	<httpErrors errorMode="Custom" existingResponse="Replace">
      <remove statusCode="404"/>
	  <remove statusCode="403"/>
      <error statusCode="404" path="404.html" responseMode="File"/>
      <error statusCode="403" path="403.html" responseMode="File"/>
	</httpErrors>
   
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".logsstdout" forwardWindowsAuthToken="false"/>
  </system.webServer>
</configuration>

Также для обработки ошибок добавим в корень проекта новый файл 404.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Ошибка 404</title>
</head>
<body>
    <h1>Ошибка 404</h1>
    <h2>Ресурс не найден!</h2>
</body>
</html>

По аналогии можно добавить файл 403.html для ошибки 403.

Итак, элемент httpErrors имеет ряд настроек. Для тестирования настроек локально, необходимо установить атрибут errorMode="Custom".
Если тестирование необязательно, и приложение уже развернуто для использования, то можно установить значение errorMode="DetailedLocalOnly".

Значение existingResponse="Replace" позволит отобразить ошибку по оригинальному запрошенному пути без переадресации.

Внутри элемента httpErrors с помощью отдельных элементов error устанавливается обработка ошибок. Атрибут statusCode
задает статусный код, атрибут path — адрес url, который будет вызываться, а атрибут responseMode указывает, как будет обрабатываться ответ вызванному url.
Атрибут responseMode имеет значение File, что позволяет рассматривать адрес url из атрибута path как статическую страницу и использовать ее в качестве ответа

Настройки элемента httpErrors могут наследоваться с других уровней, например, от файла конфигурации machine.config. И чтобы удалить
все унаследованные настройки, применяется элемент <clear />. Чтобы удалить настройки для отдельных ошибок, применяется элемент
<remove />.

Для тестирования используем следующий класс Startup:

public class Startup
{
	public void Configure(IApplicationBuilder app)
	{
		app.Map("/hello", ap => ap.Run(async (context) =>
		{
			await context.Response.WriteAsync($"Hello ASP.NET Core");
		}));
	}
}

И после обращения к несуществующему ресурсу в приложении отобразится содержимое из файла 404.html.

Настройка обработки ошибок в web.config в ASP.NET Core

В преддверии старта курса «C# ASP.NET Core разработчик» подготовили традиционный перевод полезного материала.

Также рекомендуем посмотреть вебинар на тему

«Отличия структурных шаблонов проектирования на примерах». На этом открытом уроке участники вместе с преподавателем-экспертом познакомятся с тремя структурными шаблонами проектирования: Заместитель, Адаптер и Декоратор.


Введение 

Сегодня в этой статье мы обсудим концепцию обработки исключений в приложениях ASP.NET Core. Обработка исключений (exception handling) — одна из наиболее важных импортируемых функций или частей любого типа приложений, которой всегда следует уделять внимание и правильно реализовывать. Исключения — это в основном средства ориентированные на обработку рантайм ошибок, которые возникают во время выполнения приложения. Если этот тип ошибок не обрабатывать должным образом, то приложение будет остановлено в результате их появления.

В ASP.NET Core концепция обработки исключений подверглась некоторым изменениям, и теперь она, если можно так сказать, находится в гораздо лучшей форме для внедрения обработки исключений. Для любых API-проектов реализация обработки исключений для каждого действия будет отнимать довольно много времени и дополнительных усилий. Но мы можем реализовать глобальный обработчик исключений (Global Exception handler), который будет перехватывать все типы необработанных исключений. Преимущество реализации глобального обработчика исключений состоит в том, что нам нужно определить его всего лишь в одном месте. Через этот обработчик будет обрабатываться любое исключение, возникающее в нашем приложении, даже если мы объявляем новые методы или контроллеры. Итак, в этой статье мы обсудим, как реализовать глобальную обработку исключений в ASP.NET Core Web API.

Создание проекта ASP.NET Core Web API в Visual Studio 2019

Итак, прежде чем переходить к обсуждению глобального обработчика исключений, сначала нам нужно создать проект ASP.NET Web API. Для этого выполните шаги, указанные ниже.

  • Откройте Microsoft Visual Studio и нажмите «Create a New Project» (Создать новый проект).

  • В диалоговом окне «Create New Project» выберите «ASP.NET Core Web Application for C#» (Веб-приложение ASP.NET Core на C#) и нажмите кнопку «Next» (Далее).

  • В окне «Configure your new project» (Настроить новый проект) укажите имя проекта и нажмите кнопку «Create» (Создать).

  • В диалоговом окне «Create a New ASP.NET Core Web Application» (Создание нового веб-приложения ASP.NET Core) выберите «API» и нажмите кнопку «Create».

  • Убедитесь, что флажки «Enable Docker Support» (Включить поддержку Docker) и «Configure for HTTPS» (Настроить под HTTPS) сняты. Мы не будем использовать эти функции.

  • Убедитесь, что выбрано «No Authentication» (Без аутентификации), поскольку мы также не будем использовать аутентификацию.

  • Нажмите ОК.

Используем UseExceptionHandler middleware в ASP.NET Core.

Чтобы реализовать глобальный обработчик исключений, мы можем воспользоваться преимуществами встроенного Middleware ASP.NET Core. Middleware представляет из себя программный компонент, внедренный в конвейер обработки запросов, который каким-либо образом обрабатывает запросы и ответы. Мы можем использовать встроенное middleware ASP.NET Core UseExceptionHandler в качестве глобального обработчика исключений. Конвейер обработки запросов ASP.NET Core включает в себя цепочку middleware-компонентов. Эти компоненты, в свою очередь, содержат серию делегатов запросов, которые вызываются один за другим. В то время как входящие запросы проходят через каждый из middleware-компонентов в конвейере, каждый из этих компонентов может либо обработать запрос, либо передать запрос следующему компоненту в конвейере.

С помощью этого middleware мы можем получить всю детализированную информацию об объекте исключения, такую ​​как стектрейс, вложенное исключение, сообщение и т. д., а также вернуть эту информацию через API в качестве вывода. Нам нужно поместить middleware обработки исключений в configure() файла startup.cs. Если мы используем какое-либо приложение на основе MVC, мы можем использовать middleware обработки исключений, как это показано ниже. Этот фрагмент кода демонстрирует, как мы можем настроить middleware UseExceptionHandler для перенаправления пользователя на страницу с ошибкой при возникновении любого типа исключения.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    app.UseExceptionHandler("/Home/Error");  
    app.UseMvc();  
} 

Теперь нам нужно проверить сообщение об исключении. Для этого откройте файл WeatherForecastController.cs и добавьте следующий экшн-метод, чтобы пробросить исключение:

[Route("GetExceptionInfo")]  
[HttpGet]  
public IEnumerable<string> GetExceptionInfo()  
{  
     string[] arrRetValues = null;  
     if (arrRetValues.Length > 0)  
     { }  
     return arrRetValues;  
} 

Если мы хотим получить подробную информацию об объектах исключения, например, стектрейс, сообщение и т. д., мы можем использовать приведенный ниже код в качестве middleware исключения —

app.UseExceptionHandler(  
                options =>  
                {  
                    options.Run(  
                        async context =>  
                        {  
                            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;  
                            context.Response.ContentType = "text/html";  
                            var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();  
                            if (null != exceptionObject)  
                            {  
                                var errorMessage = $"<b>Exception Error: {exceptionObject.Error.Message} </b> {exceptionObject.Error.StackTrace}";  
                                await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);  
                            }  
                        });  
                }  
            );  

Для проверки вывода просто запустите эндпоинт API в любом браузере:

Определение пользовательского Middleware для обработки исключений в API ASP.NET Core

Кроме того, мы можем написать собственное middleware для обработки любых типов исключений. В этом разделе мы продемонстрируем, как создать типичный пользовательский класс middleware. Пользовательское middleware также обеспечивает гораздо большую гибкость для обработки исключений. Мы можем добавить стекатрейс, имя типа исключения, код ошибки или что-нибудь еще, что мы захотим включить как часть сообщения об ошибке. В приведенном ниже фрагменте кода показан типичный пользовательский класс middleware:

using Microsoft.AspNetCore.Http;    
using Newtonsoft.Json;    
using System;    
using System.Collections.Generic;    
using System.Linq;    
using System.Net;    
using System.Threading.Tasks;    
    
namespace API.DemoSample.Exceptions    
{    
    public class ExceptionHandlerMiddleware    
    {    
        private readonly RequestDelegate _next;    
    
        public ExceptionHandlerMiddleware(RequestDelegate next)    
        {    
            _next = next;    
        }    
    
        public async Task Invoke(HttpContext context)    
        {    
            try    
            {    
                await _next.Invoke(context);    
            }    
            catch (Exception ex)    
            {    
                    
            }    
        }    
    }    
} 

В приведенном выше классе делегат запроса передается любому middleware. Middleware либо обрабатывает его, либо передает его следующему middleware в цепочке. Если запрос не успешен, будет выброшено исключение, а затем будет выполнен метод HandleExceptionMessageAsync в блоке catch. Итак, давайте обновим код метода Invoke, как показано ниже:

public async Task Invoke(HttpContext context)  
{  
    try  
    {  
        await _next.Invoke(context);  
    }  
    catch (Exception ex)  
    {  
        await HandleExceptionMessageAsync(context, ex).ConfigureAwait(false);  
    }  
}  

 Теперь нам нужно реализовать метод HandleExceptionMessageAsync, как показано ниже:

private static Task HandleExceptionMessageAsync(HttpContext context, Exception exception)  
{  
    context.Response.ContentType = "application/json";  
    int statusCode = (int)HttpStatusCode.InternalServerError;  
    var result = JsonConvert.SerializeObject(new  
    {  
        StatusCode = statusCode,  
        ErrorMessage = exception.Message  
    });  
    context.Response.ContentType = "application/json";  
    context.Response.StatusCode = statusCode;  
    return context.Response.WriteAsync(result);  
} 

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

using Microsoft.AspNetCore.Builder;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
  
namespace API.DemoSample.Exceptions  
{  
    public static class ExceptionHandlerMiddlewareExtensions  
    {  
        public static void UseExceptionHandlerMiddleware(this IApplicationBuilder app)  
        {  
            app.UseMiddleware<ExceptionHandlerMiddleware>();  
        }  
    }  
}  

На последнем этапе, нам нужно включить наше пользовательское middleware в методе Configure класса startup, как показано ниже:

app.UseExceptionHandlerMiddleware();  

Заключение

Обработка исключений — это по сути сквозная функциональность для любого типа приложений. В этой статье мы обсудили процесс реализации концепции глобальной обработки исключений. Мы можем воспользоваться преимуществами глобальной обработки исключений в любом приложении ASP.NET Core, чтобы гарантировать, что каждое исключение будет перехвачено и вернет правильные сведения, связанные с этим исключением. С глобальной обработкой исключений нам достаточно в одном месте написать код, связанный с обработкой исключений, для всего нашего приложения. Любые предложения, отзывы или запросы, связанные с этой статьей, приветствуются.


Узнать подробнее о курсе «C# ASP.NET Core разработчик».

Посмотреть вебинар на тему «Отличия структурных шаблонов проектирования на примерах».

Quick and Easy Exception Handling

Simply add this middleware before ASP.NET routing into your middleware registrations.

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

Done!


Enable Dependency Injection for logging and other purposes

Step 1. In your startup, register your exception handling route:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

Step 2. Create controller that will handle all exceptions and produce error response:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

A few important notes and observations:

  • You can inject your dependencies into the Controller’s constructor.
  • [ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise, it may break your Swashbuckle swagger
  • Again, app.UseExceptionHandler("/error"); has to be one of the very top registrations in your Startup Configure(...) method. It’s probably safe to place it at the top of the method.
  • The path in app.UseExceptionHandler("/error") and in controller [Route("error")] should be the same, to allow the controller handle exceptions redirected from exception handler middleware.

Here is the link to official Microsoft documentation.


Response model ideas.

Implement your own response model and exceptions.
This example is just a good starting point. Every service would need to handle exceptions in its own way. With the described approach you have full flexibility and control over handling exceptions and returning the right response from your service.

An example of error response model (just to give you some ideas):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

For simpler services, you might want to implement http status code exception that would look like this:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

This can be thrown from anywhere this way:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

Then your handling code could be simplified to just this:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>() WAT?

ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, MVC, Swagger etc. are separated and executed sequentially in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it’s important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn’t really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be «connected» with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can «pass» exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).

Handling errors in an ASP.NET Core Web API

This post looks at the best ways to handle exceptions, validation and other invalid requests such as 404s in ASP.NET Core Web API projects and how these approaches differ from MVC error handling.

Why do we need a different approach from MVC?

In .Net Core, MVC and Web API have been combined so you now have the same controllers for both MVC actions and API actions. However, despite the similarities, when it comes to error handling, you almost certainly want to use a different approach for API errors.

MVC actions are typically executed as a result of a user action in the browser so returning an error page to the browser is the correct approach. With an API, this is not generally the case.

API calls are most often called by back-end code or javascript code and in both cases, you never want to simply display the response from the API. Instead we check the status code and parse the response to determine if our action was successful, displaying data to the user as necessary. An error page is not helpful in these situations. It bloats the response with HTML and makes client code difficult because JSON (or XML) is expected, not HTML.

While we want to return information in a different format for Web API actions, the techniques for handling errors are not so different from MVC. Much of the time, it is practically the same flow but instead of returning a View, we return JSON. Let’s look at a few examples.

The minimal approach

With MVC actions, failure to display a friendly error page is unacceptable in a professional application. With an API, while not ideal, empty response bodies are far more permissible for many invalid request types. Simply returning a 404 status code (with no response body) for an API route that does not exist may provide the client with enough information to fix their code.

With zero configuration, this is what ASP.NET Core gives us out of the box.

Depending on your requirements, this may be acceptable for many common status codes but it will rarely be sufficient for validation failures. If a client passes you invalid data, returning a 400 Bad Request is not going to be helpful enough for the client to diagnose the problem. At a minimum, we need to let them know which fields are incorrect and ideally, we would return an informative message for each failure.

With ASP.NET Web API, this is trivial. Assuming that we are using model binding, we get validation for free by using data annotations and/or IValidatableObject. Returning the validation information to the client as JSON is one easy line of code.

Here is our model:

public class GetProductRequest : IValidatableObject
{
    [Required]
    public string ProductId { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (...)
        {
            yield return new ValidationResult("ProductId is invalid", new[] { "ProductId" });
        }
    }
}

And our controller action:

[HttpGet("product")]
public IActionResult GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    ...
}

A missing ProductId results in a 400 status code plus a JSON response body similar to the following:

{
    "ProductId":["The ProductId field is required."]
}

This provides an absolute minimum for a client to consume our service but it is not difficult to improve upon this baseline and create a much better client experience. In the next few sections we will look at how simple it is to take our service to the next level.

Returning additional information for specific errors

If we decide that a status code only approach is too bare-bones, it is easy to provide additional information. This is highly recommended. There are many situations where a status code by itself is not enough to determine the cause of failure. If we take a 404 status code as an example, in isolation, this could mean:

  • We are making the request to the wrong site entirely (perhaps the ‘www’ site rather than the ‘api’ subdomain)
  • The domain is correct but the URL does not match a route
  • The URL correctly maps to a route but the resource does not exist

If we could provide information to distinguish between these cases, it could be very useful for a client. Here is our first attempt at dealing with the last of these:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    ...

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound("Product not found");
    }

    return Ok(model);
}

We are now returning a more useful message but it is far from perfect. The main problem is that by using a string in the NotFound method, the framework will return this string as a plain text response rather than JSON.

As a client, a service returning a different content type for certain errors is much harder to deal with than a consistent JSON service.

This issue can quickly be rectified by changing the code to what is shown below but in the next section, we will talk about a better alternative.

return NotFound(new { message = "Product not found" });

Customising the response structure for consistency

Constructing anonymous objects on the fly is not the approach to take if you want a consistent client experience. Ideally our API should return the same response structure in all cases, even when the request was unsuccessful.

Let’s define a base ApiResponse class:

public class ApiResponse
{
    public int StatusCode { get; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string Message { get; }

    public ApiResponse(int statusCode, string message = null)
    {
        StatusCode = statusCode;
        Message = message ?? GetDefaultMessageForStatusCode(statusCode);
    }

    private static string GetDefaultMessageForStatusCode(int statusCode)
    {
        switch (statusCode)
        {
            ...
            case 404:
                return "Resource not found";
            case 500:
                return "An unhandled error occurred";
            default:
                return null;
        }
    }
}

We’ll also need a derived ApiOkResponse class that allows us to return data:

public class ApiOkResponse : ApiResponse
{
    public object Result { get; }

    public ApiOkResponse(object result)
        :base(200)
    {
        Result = result;
    }
}

Finally, let’s declare an ApiBadRequestResponse class to handle validation errors (if we want our responses to be consistent, we will need to replace the built-in functionality used above).

public class ApiBadRequestResponse : ApiResponse
{
    public IEnumerable<string> Errors { get; }

    public ApiBadRequestResponse(ModelStateDictionary modelState)
        : base(400)
    {
        if (modelState.IsValid)
        {
            throw new ArgumentException("ModelState must be invalid", nameof(modelState));
        }

        Errors = modelState.SelectMany(x => x.Value.Errors)
            .Select(x => x.ErrorMessage).ToArray();
    }
}

These classes are very simple but can be customised to your own requirements.

If we change our action to use these ApiResponse based classes, it becomes:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(new ApiBadRequestResponse(ModelState));
    }

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound(new ApiResponse(404, $"Product not found with id {request.ProductId}"));
    }

    return Ok(new ApiOkResponse(model));
}

The code is slightly more complicated now but all three types of response from our action (success, bad request and not found) now use the same general structure.

Centralising Validation Logic

Given that validation is something that you do in practically every action, it makes to refactor this generic code into an action filter. This reduces the size of our actions, removes duplicated code and improves consistency.

public class ApiValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(new ApiBadRequestResponse(context.ModelState));
        }

        base.OnActionExecuting(context);
    }
}

Handling global errors

Responding to bad input in our controller actions is the best way to provide specific error information to our client. Sometimes however, we need to respond to more generic issues. Examples of this include:

  • A 401 Unauthorized code returned from security middleware.

  • A request URL that does not map to a controller action resulting in a 404.

  • Global exceptions. Unless you can do something about a specific exception, you should not clutter your actions with try catch blocks.

As with MVC, the easiest way to deal with global errors is by using StatusCodePagesWithReExecute and UseExceptionHandler.

We talked about StatusCodePagesWithReExecute last time but to reiterate, when a non-success status code is returned from inner middleware (such as an API action), the middleware allows you to execute another action to deal with the status code and return a custom response.

UseExceptionHandler works in a similar way, catching and logging unhandled exceptions and allowing you to execute another action to handle the error. In this example, we configure both pieces of middleware to point to the same action.

We add the middleware in startup.cs:

app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
...
//register other middleware that might return a non-success status code

Then we add our error handling action:

[Route("error/{code}")]
public IActionResult Error(int code)
{
    return new ObjectResult(new ApiResponse(code));
}

With this in place, all exceptions and non-success status codes (without a response body) will be handled by our error action where we return our standard ApiResponse.

Custom Middleware

For the ultimate in control, you can replace or complement built-in middleware with your own custom middleware. The example below handles any bodiless response and returns our simple ApiResponse object as JSON. If this is used in conjunction with code in our actions to return ApiResponse objects, we can ensure that both success and failure responses share the same common structure and all requests result in both a status code and a consistent JSON body:

public class ErrorWrappingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorWrappingMiddleware> _logger;
    
    public ErrorWrappingMiddleware(RequestDelegate next, ILogger<ErrorWrappingMiddleware> logger)
    {
        _next = next;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch(Exception ex)
        {
            _logger.LogError(EventIds.GlobalException, ex, ex.Message);

            context.Response.StatusCode = 500;
        }            

        if (!context.Response.HasStarted)
        {
            context.Response.ContentType = "application/json";

            var response = new ApiResponse(context.Response.StatusCode);

            var json = JsonConvert.SerializeObject(response);

            await context.Response.WriteAsync(json);
        }            
    }
}

Conclusion

Handling errors in ASP.NET Core APIs is similar but different from MVC error code. At the action level, we want to return custom objects (serialised as JSON) rather than custom views.

For generic errors, we can still use the StatusCodePagesWithReExecute middleware but need to modify our code to return an ObjectResult instead of a ViewResult.

For full control, it is not difficult to write your own middleware to handle errors exactly as required.

Useful or Interesting?

If you liked the article, I would really appreciate it if you could share it with your Twitter followers.

Share
on Twitter

Comments

The exception handling features help us deal with the unforeseen errors which could appear in our code.  To handle exceptions we can use the try-catch block in our code as well as finally keyword to clean up resources afterward.

Even though there is nothing wrong with the try-catch blocks in our Actions in Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions more readable and the error handling process more maintainable. If we want to make our actions even more readable and maintainable, we can implement Action Filters. We won’t talk about action filters in this article but we strongly recommend reading our post Action Filters in .NET Core.

In this article, we are going to handle errors by using a try-catch block first and then rewrite our code by using built-in middleware and our custom middleware for global error handling to demonstrate the benefits of this approach. We are going to use an ASP.NET Core Web API project to explain these features and if you want to learn more about it (which we strongly recommend), you can read our ASP.NET Core Web API Tutorial.


VIDEO: Global Error Handling in ASP.NET Core Web API video.


To download the source code for our starting project, you can visit the Global error handling start project.

For the finished project refer to Global error handling end project.

Let’s start.

Error Handling With Try-Catch Block

To start off with this example, let’s open the Values Controller from the starting project (Global-Error-Handling-Start project). In this project, we can find a single Get() method and an injected Logger service.

It is a common practice to include the log messages while handling errors, therefore we have created the LoggerManager service. It logs all the messages to the C drive, but you can change that by modifying the path in the nlog.config file. For more information about how to use Nlog in .NET Core, you can visit Logging with NLog.

Now, let’s modify our action method to return a result and log some messages:

using System;
using LoggerService;
using Microsoft.AspNetCore.Mvc;

namespace GlobalErrorHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private ILoggerManager _logger;

        public ValuesController(ILoggerManager logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IActionResult Get()
        {
            try
            {
                _logger.LogInfo("Fetching all the Students from the storage");

                var students = DataManager.GetAllStudents(); //simulation for the data base access

                _logger.LogInfo($"Returning {students.Count} students.");

                return Ok(students);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong: {ex}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

When we send a request at this endpoint, we will get this result:

Basic request - Global Error Handling

And the log messages:

log basic request - Global Error Handling

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

We see that everything is working as expected.

Now let’s modify our code, right below the GetAllStudents() method call, to force an exception:

throw new Exception("Exception while fetching all the students from the storage.");

Now, if we send a request:

try catche error - Global Error Handling

And the log messages:

log try catch error

So, this works just fine. But the downside of this approach is that we need to repeat our try-catch blocks in all the actions in which we want to catch unhandled exceptions. Well, there is a better approach to do that.

Handling Errors Globally With the Built-In Middleware

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions in our ASP.NET Core Web API application. So, let’s dive into the code to see this middleware in action.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

First, we are going to add a new class ErrorDetails in the Models folder:

using System.Text.Json;

namespace GlobalErrorHandling.Models
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }

        public override string ToString()
        {
            return JsonSerializer.Serialize(this);
        }
    }
}

We are going to use this class for the details of our error message.

To continue, let’s create a new folder Extensions and a new static class ExceptionMiddlewareExtensions.cs inside it.

Now, we need to modify it:

using GlobalErrorHandling.Models;
using LoggerService;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.Net;

namespace GlobalErrorHandling.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";

                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if(contextFeature != null)
                    { 
                        logger.LogError($"Something went wrong: {contextFeature.Error}");

                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error."
                        }.ToString());
                    }
                });
            });
        }
    }
}

In the code above, we’ve created an extension method in which we’ve registered the UseExceptionHandler middleware. Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object.

To be able to use this extension method, let’s modify the Configure method inside the Startup class for .NET 5 project:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger) 
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.ConfigureExceptionHandler(logger);

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Or if you are using .NET 6 and above:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

Finally, let’s remove the try-catch block from our code:

public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

     var students = DataManager.GetAllStudents(); //simulation for the data base access

     throw new Exception("Exception while fetching all the students from the storage.");

     _logger.LogInfo($"Returning {students.Count} students.");

     return Ok(students);
}

And there you go. Our action method is much cleaner now and what’s more important we can reuse this functionality to write more readable actions in the future.

So let’s inspect the result:

Global Handler Middleware

And the log messages:

log global handler middleware

Excellent.

Now, we are going to use custom middleware for global error handling.

Handling Errors Globally With the Custom Middleware

Let’s create a new folder named CustomExceptionMiddleware and a class ExceptionMiddleware.cs inside it.

We are going to modify that class:

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;

    public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        await context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

The first thing we need to do is to register our IloggerManager service and RequestDelegate through the dependency injection. The _next parameter of RequestDeleagate type is a function delegate that can process our HTTP requests.

After the registration process, we create the InvokeAsync() method. RequestDelegate can’t process requests without it.

If everything goes well, the _next delegate should process the request and the Get action from our controller should generate a successful response. But if a request is unsuccessful (and it is, because we are forcing an exception), our middleware will trigger the catch block and call the HandleExceptionAsync method.

In that method, we just set up the response status code and content type and return a response.

Now let’s modify our ExceptionMiddlewareExtensions class with another static method:

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<ExceptionMiddleware>();
}

In .NET 6 and above, we have to extend the WebApplication type:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

public static void ConfigureCustomExceptionMiddleware(this WebApplication app) 
{ 
    app.UseMiddleware<ExceptionMiddleware>(); 
}

Finally, let’s use this method in the Configure method in the Startup class:

//app.ConfigureExceptionHandler(logger);
app.ConfigureCustomExceptionMiddleware();

Great.

Now let’s inspect the result again:

custom handler middleware

There we go. Our custom middleware is implemented in a couple of steps.

Customizing Error Messages

If you want, you can always customize your error messages from the error handler. There are different ways of doing that, but we are going to show you the basic two ways.

First of all, we can assume that the AccessViolationException is thrown from our action:

[HttpGet]
public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

    var students = DataManager.GetAllStudents(); //simulation for the data base access

    throw new AccessViolationException("Violation Exception while accessing the resource.");

    _logger.LogInfo($"Returning {students.Count} students.");

    return Ok(students);
}

Now, what we can do is modify the InvokeAsync method inside the ExceptionMiddleware.cs class by adding a specific exception checking in the additional catch block:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

public async Task InvokeAsync(HttpContext httpContext)
{
    try
    {
        await _next(httpContext);
    }
    catch (AccessViolationException avEx)
    {
        _logger.LogError($"A new violation exception has been thrown: {avEx}");
        await HandleExceptionAsync(httpContext, avEx);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Something went wrong: {ex}");
        await HandleExceptionAsync(httpContext, ex);
    }
}

And that’s all. Now if we send another request with Postman, we are going to see in the log file that the AccessViolationException message is logged. Of course, our specific exception check must be placed before the global catch block.

With this solution, we are logging specific messages for the specific exceptions, and that can help us, as developers, a lot when we publish our application. But if we want to send a different message for a specific error, we can also modify the HandleExceptionAsync method in the same class:

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    context.Response.ContentType = "application/json";
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

    var message = exception switch
    {
        AccessViolationException =>  "Access violation error from the custom middleware",
        _ => "Internal Server Error from the custom middleware."
    };

    await context.Response.WriteAsync(new ErrorDetails()
    {
        StatusCode = context.Response.StatusCode,
        Message = message
    }.ToString());
}

Here, we are using a switch expression pattern matching to check the type of our exception and assign the right message to the message variable. Then, we just use that variable in the WriteAsync method.

Now if we test this, we will get a log message with the Access violation message, and our response will have a new message as well:

{
    "StatusCode": 500,
    "Message": "Access violation error from the custom middleware"
}

One thing to mention here. We are using the 500 status code for all the responses from the exception middleware, and that is something we believe it should be done. After all, we are handling exceptions and these exceptions should be marked with a 500 status code. But this doesn’t have to be the case all the time. For example, if you have a service layer and you want to propagate responses from the service methods as custom exceptions and catch them inside the global exception handler, you may want to choose a more appropriate status code for the response. You can read more about this technique in our Onion Architecture article. It really depends on your project organization.

Conclusion

That was awesome.

We have learned, how to handle errors in a more sophisticated way and cleaner as well. The code is much more readable and our exception handling logic is now reusable for the entire project.

Thank you for reading this article. We hope you have learned new useful things.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Exception handling is required in any application. It is a very interesing issue where different apps have their own various way(s) to handle that. I plan to write a series of articles to discuss this issue

  • Exception Handling (1), in ASP.NET MVC
  • Exception Handling (2), in ASP.NET Web API
  • Exception Handling (3), in ASP.NET Core MVC — this article
  • Exception Handling (4), in ASP.NET Core Web API
  • Exception Handling (5), in ASP.NET Summary
  • Exception Handling (6), HttpStatusCode

In this article, we will be discussing various ways of handling an exception in ASP.NET Core MVC.

Introduction

In Part I of this article seriers, we discussed Exception handling for ASP.NET MVC, where we may have three ways to handle exceptions,

For ASP.NET Core MVC, we have similar situation or discussion, but, with major differences:

  1. We will not discuss the Try-Catch-Finally approach, because it is language related issue;
  2. Due to Exception Filter, the approach is just secondary importance in ASP.NET Core app, we will just make brief discussion at the end.

This will be the order inwhich we will discuss the topic today:

  • A: Exception Handling in Development Environment for ASP.NET Core MVC
    • UseDeveloperExceptionPage
  • B: Exception Handling in Production Environment for ASP.NET Core MVC
    • Approach 1: UseExceptionHandler
      • 1: Exception Handler Page
      • 2: Exception Handler Lambda
    • Approach 2: UseStatusCodePages
      • 1: UseStatusCodePages, and with format string, and with Lambda
      • 2: UseStatusCodePagesWithRedirects
      • 3: UseStatusCodePagesWithReExecute
    • Approach 3: Exception Filter
      • Local
      • Global

A: Exception Handling in Developer Environment

The ASP.NET Core starup templates generate the following code,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else 
     ......
 } 

The UseDeveloperExceptionPage extension method adds middleware into the request pipeline. The Developer Exception Page displays developer friendly detailed information about request exceptions. This helps developers in tracing errors that occur during development phase.

As this middleware displays sensitive information, it is advisable to add it only in development environment. The developer environment is a new feature in .NET Core. We will demostrate this below.

Step 1 — Create an ASP.NET Core MVC application

We use the current version of Visual Studio 2019 16.8 and .NET 5.0 SDK to build the app.

  1. Start Visual Studio and select Create a new project.
  2. In the Create a new project dialog, select ASP.NET Core Web Application > Next.
  3. In the Configure your new project dialog, enter ErrorHandlingSample for Project name.
  4. Select Create.
  5. In the Create a new ASP.NET Core web application dialog, select,
    1. .NET Core and ASP.NET Core 5.0 in the dropdowns.
    2. ASP.NET Core Web App (Model-View-Controller).
    3. Create

Step 2 — Change code in Home Controller

Replace the Index method in the HomeController with the code below:

public IActionResult Index(int? id = null)  
{  
    if (id.HasValue)  
    {  
        if (id == 1)  
        {  
            throw new FileNotFoundException("File not found exception thrown in index.chtml");  
        }  
        else if (id == 2)  
        {  
            return StatusCode(500);  
        }  
    }  
    return View();  
} 

Step 3 — Change code in Index view

Add the code in the bottom of Home/Index view, i.e., the file Index.cshtml in Views/home directory, 

<br />  
  
<div class="text-left">  
    <p>  
        <a href="/NoSuchPage">  
            Request an endpoint that doesn't exist. Trigger a 404  
        </a>.  
    </p>  
    <p><a href="/home/index/1">Trigger an exceptionn</a>.</p>  
    <p><a href="/home/index/2">Return a 500 error.</a>.</p>  
</div> 

Step 4 — Run app and Test

Run the app,

Click «Trigger an exception.» you will get,

This is the Developer Exception Page that includes the following information about the exception and the request,

  • Stack trace
  • Query string parameters if any
  • Cookies if any
  • Headers
  • Routing

For examples: Headers, and Routing

B: Exception Handling in Production Environment

ASP.NET Core configures app behavior based on the runtime environment that is determined in launchSettings.json file:

  • Development : The launchSettings.json file sets ASPNETCORE_ENVIRONMENT to Development on the local machine.
  • Staging
  • Production : The default if DOTNET_ENVIRONMENT and ASPNETCORE_ENVIRONMENT have not been set.

Note

The launchSettings.json file:

  • Is only used on the local development machine.
  • Is not deployed.
  • contains profile settings.

Now, we switch environment from Development to Production,

{  
  "iisSettings": {  
    "windowsAuthentication": false,  
    "anonymousAuthentication": true,  
    "iisExpress": {  
      "applicationUrl": "http://localhost:50957",  
      "sslPort": 44362  
    }  
  },  
  "profiles": {  
    "IIS Express": {  
      "commandName": "IISExpress",  
      "launchBrowser": true,  
      "environmentVariables": {  
        //"ASPNETCORE_ENVIRONMENT": "Development",  
        "ASPNETCORE_ENVIRONMENT": "Production"  
      }  
    },  
    "ErrorHandlingSample": {  
      "commandName": "Project",  
      "dotnetRunMessages": "true",  
      "launchBrowser": true,  
      "applicationUrl": "https://localhost:5001;http://localhost:5000",  
      "environmentVariables": {  
        //"ASPNETCORE_ENVIRONMENT": "Development",  
        "ASPNETCORE_ENVIRONMENT": "Production"  
      }  
    }  
  }  
} 

Approach 1: UseExceptionHandler

1: Exception Handler Page

For Production environment, startup file Configure method tells us: ASP.NET Core handles exception by calling UseExceptionHandler,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
    } 
    ......
} 

Run the app, and Click Trigger an exception link in the home page, we got the Exception Handler Page, and by default Home/Error.cshtml.cs generated by the ASP.NET Core templates,

This exception handling middleware,

  • Catches and logs exceptions.
  • Re-executes the request in an alternate pipeline using the path indicated. The request isn’t re-executed if the response has started. The template generated code re-executes the request using the /Home/Error path.

2: Exception Handler Lambda

An alternative to a custom exception handler page is to provide a lambda to UseExceptionHandler. Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling (startup file):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler(errorApp =>  
        {  
            errorApp.Run(async context =>  
            {  
                context.Response.StatusCode = 500;  
                context.Response.ContentType = "text/html";  
  
                await context.Response.WriteAsync("<html lang="en"><body>rn");  
                await context.Response.WriteAsync("ERROR!<br><br>rn");  
  
                var exceptionHandlerPathFeature =  
                    context.Features.Get<IExceptionHandlerPathFeature>();  
  
                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)  
                {  
                    await context.Response.WriteAsync(  
                                              "File error thrown!<br><br>rn");  
                }  
  
                await context.Response.WriteAsync(  
                                              "<a href="/">Home</a><br>rn");  
                await context.Response.WriteAsync("</body></html>rn");  
                await context.Response.WriteAsync(new string(' ', 512));   
            });  
        });  
        app.UseHsts();  
    }  
  
    app.UseHttpsRedirection();  
    app.UseStaticFiles();  
  
    app.UseRouting();  
  
    app.UseAuthorization();  
  
    app.UseEndpoints(endpoints =>  
    {  
        endpoints.MapRazorPages();  
    });  
} 

We got the result,

Note

For convenience, we keep the original startup file as startup.cs file, and make a new startup file with a class name and file name as startupLambda, the highlighted one in the graph below,

and in Program.cs, comment out Startup class, replace it by startupLambda class, like this,

public class Program  
{  
    public static void Main(string[] args)  
    {  
        CreateHostBuilder(args).Build().Run();  
    }  
  
    public static IHostBuilder CreateHostBuilder(string[] args) =>  
        Host.CreateDefaultBuilder(args)  
            .ConfigureWebHostDefaults(webBuilder =>  
            {  
                //webBuilder.UseStartup<Startup>();  
                webBuilder.UseStartup<StartupLambda>();  
                //webBuilder.UseStartup<StartupUseStatusCodePages>();  
                //webBuilder.UseStartup<StartupStatusLambda>();  
                //webBuilder.UseStartup<StartupFormat>();  
                //webBuilder.UseStartup<StartupSCredirect>();  
                //webBuilder.UseStartup<StartupSCreX>();  
            });  
} 

With similarity, we create several new startup classes as in the above graph, we will define them and use them in later discussions.

Approach 2: UseStatusCodePages

The two techniques discussed so far deal with the unhandled exceptions arising from code. However, that’s not the only source of errors. Many times errors are generated due to internal server errors, non existent pages, web server authorization issues and so on. These errors are reflected by the HTTP status codes such as 500, 404 and 401.

By default, an ASP.NET Core app doesn’t provide a status code page for HTTP error status codes, such as 404 — Not Found. When the app encounters an HTTP 400-599 error status code that doesn’t have a body, it returns the status code and an empty response body.

Click the link: Request an endpoint that doesn’t exist. Trigger a 404 below,

We will get,

Whie clicking Run a 500 error,

To deal with such errors we can use UseStatusCodePages() method (status code pages middleware) to provide status code pages.

1: Default UseStatusCodePages, or with format string, or with Lambda

To enable default text-only handlers for common error status codes, call UseStatusCodePages in the Startup.Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePages();  
  
    ......
}  

We make this file (class) name as startupUseStatusCodePages, and  Remove the comments from webBuilder.UseStartup<StartupUseStatusCodePages>(); in Program.cs.

Run the app, the resullt will be:

for 404 error, and below for 500 error:

 

Again, we make startup file named as StartupFormat

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else  
     {  
         app.UseExceptionHandler("/Home/Error");  
         app.UseHsts();  
     }  
  
     app.UseStatusCodePages(  
         "text/plain", "Status code page, status code: {0}");  
     ...... 
 } 

Remove the comments from webBuilder.UseStartup<StartupFormat>(); in Program.cs. Run the app, the resullt will be shown:


and

The same, to make startup file named as StartupStatusLambda,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else  
     {  
         app.UseExceptionHandler("/Home/Error");  
         app.UseHsts();  
     }  
  
     app.UseStatusCodePages(async context =>  
     {  
         context.HttpContext.Response.ContentType = "text/plain";  
  
         await context.HttpContext.Response.WriteAsync(  
             "Status code lambda, status code: " +  
             context.HttpContext.Response.StatusCode);  
     });  
     ...... 
 } 

Remove the comments from webBuilder.UseStartup<StartupStatusLambda>(); in Program.cs. Run the app, the resullt will be shown:


and

Note

UseStatusCodePages isn’t typically used in production because it returns a message that isn’t useful to users.

2: UseStatusCodePagesWithRedirects

The UseStatusCodePagesWithRedirects extension method:

  • Sends a status code to the client.
  • Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200. 

Implementation

Step 1: Set up Startup file

Make startup file named as StartupSCredirect,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePagesWithRedirects("/Home/MyStatusCode?code={0}");  
    ...... 
} 

Remove the comments from webBuilder.UseStartup<StartupSCredirect>(); in Program.cs.

Step 2:  Add an Action method in HomeController,

public IActionResult MyStatusCode(int code)  
{  
    if (code == 404)  
    {  
        ViewBag.ErrorMessage = "The requested page not found.";  
    }  
    else if (code == 500)  
    {  
        ViewBag.ErrorMessage = "My custom 500 error message.";  
    }  
    else  
    {  
        ViewBag.ErrorMessage = "An error occurred while processing your request.";  
    }  
  
    ViewBag.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;  
    ViewBag.ShowRequestId = !string.IsNullOrEmpty(ViewBag.RequestId);  
    ViewBag.ErrorStatusCode = code;  
  
    return View();  
} 

Step 3

Create a view for the Action: View/Home/MyStatusCode.cshtml

@{   
    Layout = null;  // clean up F12 tool network tab   
}  
  
@{ ViewData["Title"] = "Status Code @ViewBag.ErrorStatusCode"; }  
<head>  
    <!-- prevent favicon.ico from being requested. -->  
    <link rel="icon" href="data:,">  
</head>  
  
<h1>MyStatusCode page</h1>  
<h2 class="text-danger">Status Code: @ViewBag.ErrorStatusCode</h2>  
<h2 class="text-danger"> @ViewBag.ErrorMessage</h2>  
  
@if (ViewBag.ShowRequestId)  
{   
<h3>Request ID</h3>  
                <p>  
                    <code>@ViewBag.RequestId</code>  
                </p>  
} 

Run the app, click either 400 or 500 errors, we got (for Error Code 400):

Note

The link is redirected to a new link that is endpoint provided.

3: UseStatusCodePagesWithReExecute

The UseStatusCodePagesWithReExecute extension method:

  • Returns the original status code to the client.
  • Generates the response body by re-executing the request pipeline using an alternate path.

Implementation

Step 1: Set up Startup file

Make startup file named as StartupSCreX:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePagesWithReExecute("/Home/MyStatusCode2", "?code={0}");
    ...... 
} 

Remove the comments from webBuilder.UseStartup<StartupSCreX>(); in Program.cs.

Step 2:  Add an Action method in HomeController, 

public IActionResult MyStatusCode2(int code)  
{  
  
    var statusCodeReExecuteFeature = HttpContext.Features.Get<  
                                           IStatusCodeReExecuteFeature>();  
    if (statusCodeReExecuteFeature != null)  
    {  
        ViewBag.OriginalURL =  
            statusCodeReExecuteFeature.OriginalPathBase  
            + statusCodeReExecuteFeature.OriginalPath  
            + statusCodeReExecuteFeature.OriginalQueryString;  
    }  
  
    ViewBag.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;  
    ViewBag.ShowRequestId = !string.IsNullOrEmpty(ViewBag.RequestId);  
    ViewBag.ShowOriginalURL = !string.IsNullOrEmpty(ViewBag.OriginalURL);  
    ViewBag.ErrorStatusCode = code;  
  
    return View();  
} 

Step 3

Create a view for the Action: View/Home/MyStatusCode2.cshtml 

@{  
    Layout = null;  // clean up F12 tool network tab  
}  
  
@{ ViewData["Title"] = "Status Code @ViewBag.ErrorStatusCode"; }  
  
<head>  
    <!-- prevent favicon.ico from being requested. -->  
    <link rel="icon" href="data:,">  
</head>  
  
<h1 class="text-danger">Status Code: @ViewBag.ErrorStatusCode</h1>  
<h2 class="text-danger">An error occurred while processing your request.</h2>  
  
@if (ViewBag.ShowRequestId)  
{  
<h3>Request ID</h3>  
                <p>  
                    <code>@ViewBag.RequestId</code>  
                </p>}  
  
@if (ViewBag.ShowOriginalURL)  
{  
<h3>Original URL</h3>  
                <p>  
                    <code>@ViewBag.OriginalURL</code>  
                </p>} 

Run the app, click either 400 or 500 errors, we got (for Error Code 400):

Note

The link is kept the same with original one.

Approach 3: Exception Filter

General Discussion

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. 

Exception filters are useful for trapping exceptions that occur within MVC actions, but they’re not as flexible as the built-in exception handling middleware, UseExceptionHandler. Microsoft recommend using UseExceptionHandler, unless you need to perform error handling differently based on which MVC action is chosen. 

Difference from ASP.NET MVC

  1. In ASP.NET MVC, Exception Filter is the major approach for exception handling, while for ASP.NET Core MVC, as Microsoft suggested, the built-in exception hadling middleware, UseExceptionHandler, is more flexible and suitable.
  2. IExceptionFilter Interface for ASP.NET is derived by System.Web.Mvc.HandleErrorAttribute and System.Web.Mvc.Controller, therefore, we can either overriding OnException method from a class derived from HandleErrorAttribute class, or directly overriding OnException method from a controller. However, IExceptionFilter Interface for ASP.NET Core is only derived by Microsoft.AspNetCore.Mvc.Filters.ExceptionFilterAttribute, not by Controller any more. So, we have to implemente IExceptionFilter interface directly or from ExceptionFilterAttribute class, but not from Controller directly any more.

 ASP.NET

ASP.NET Core

Exception filters

  • Implement IExceptionFilter or IAsyncExceptionFilter.
  • Can be used to implement common error handling policies.

The following sample exception filter uses a custom error view to display details about exceptions that occur when the app is in development:

Implementation

Step 1

Create an Custom Exception Filter: CustomExceptionFilter

using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.Filters;  
using Microsoft.AspNetCore.Mvc.ModelBinding;  
using Microsoft.AspNetCore.Mvc.ViewFeatures;  
  
namespace ErrorHandlingSample.Filters  
{  
    public class CustomExceptionFilter : IExceptionFilter  
    {  
        private readonly IModelMetadataProvider _modelMetadataProvider;  
  
        public CustomExceptionFilter(IModelMetadataProvider modelMetadataProvider)  
        {  
            _modelMetadataProvider = modelMetadataProvider;  
        }  
  
        public void OnException(ExceptionContext context)  
        {  
            var result = new ViewResult { ViewName = "CustomError" };  
            result.ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState);  
            result.ViewData.Add("Exception", context.Exception);  
  
            // Here we can pass additional detailed data via ViewData  
            context.ExceptionHandled = true; // mark exception as handled  
            context.Result = result;  
        }  
    }  
} 

Step 2

Create a CustomError view: View/Shared/CustomError.cshtml

@{  
    ViewData["Title"] = "CustomError";  
    var exception = ViewData["Exception"] as Exception;  
}  
  
<h1>An Error has Occurred</h1>  
  
<p>@exception.Message</p>  

Step 3

Register in either locally in Controller level or Action level, e.g.

[TypeFilter(typeof(CustomAsyncExceptionFilter))]  
public IActionResult Failing()  
{  
    throw new Exception("Testing custom exception filter.");  
} 

or global level in startup.ConfigureService,

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddControllersWithViews();  
  
    services.AddControllersWithViews(config => config.Filters.Add(typeof(CustomExceptionFilter)));  
} 

Run the app, and Test it: Click Trigger an exception (you must either register the Exception filter locally in Action or Controller or Globally):

C: the Discussion here is suitable for ASP.NET Core Web App.

Finally, I would like to make a point that our discussions in this article, Exception Handling for ASP.NET Core MVC, are suitable for ASP.NET Core Web app, because the structure of ASP.NET Core MVC app and ASP.NET Core Web app are quite similar. If we compare the startup file for both Core MVC app and Core Web app, we can see that:

There are only three differences in the startup codes, they are all not structure difference, they are all for views. The first difference indicates AddControllers() for web app, and AddControllersWithViews() for MVC: 

 The second one just directs the error handling page to diffrent places:

And the third one is related to endpoints, the routing:

app. UseEndpoints(endpoints app. UseEndpoint5(endpoint5 "default" , n;

Therefore, in exception handling, Web App and MVC App are the same, we can apply our discussion for MVC to Web App.

Summary

In this article, we had a comprehensive discussion about Exception handling for ASP.NET Core MVC (Also for .NET Core Web App), this is the summary:

  • A: Exception Handling in Development Environment for ASP.NET Core MVC
    • UseDeveloperExceptionPage
  • B: Exception Handling in Production Environment for ASP.NET Core MVC
    • Approach 1: UseExceptionHandler
      • 1: Exception Handler Page
      • 2: Exception Handler Lambda
    • Approach 2: UseStatusCodePages
      • 1: UseStatusCodePages, and with format string, and with Lambda
      • 2: UseStatusCodePagesWithRedirects
      • 3: UseStatusCodePagesWithReExecute
    • Approach 3: Exception Filter
      • Local
      • Global

References

  • Handle errors in ASP.NET Core — MS
  • Use multiple environments in ASP.NET Core — MS
  • ASP.NET Core — Exception Handling — Tutorialsteacher.com
  • Five Methods to Deal with Errors in ASP.NET Core —  binaryintellect.net

In this article, we will learn about Global Exception Handling in ASP.NET Core applications. Exceptions are something inevitable in any application however well the codebase is. This can usually occur due to external factors as well, like network issues and so on. If these exceptions are not handled well within the application, it may even lead the entire application to terminations and data loss.

The source code for the implementation can be found here.

Getting started with Exception Handling in ASP.NET Core

For this demonstration, We will be working on a new ASP.NET Core Web API Project. I will be using Visual Studio 2019 as my default IDE.

The try-catch block is our go-to approach when it comes to quick exception handling. Let’s see a code snippet that demonstrates the same.

[HttpGet]
public IActionResult Get()
{
    try
    {
        var data = GetData(); //Assume you get some data here which is also likely to throw an exception in certain cases.
        return Ok(data);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex.Message);
        return StatusCode(500);
    }      
}

Here is a basic implementation that we are all used to, yeah? Assume, the method GetData() is a service call that is also prone to exceptions due to certain external factors. The thrown exception is caught by the catch block whose responsibility is to log the error to the console and returns a status code of 500 Internal Server Error in this scenario.

To learn more about logging in ASP.NET Core Applications, I recommend you to go through the following articles that demonstrate Logging with probably the best 2 Logging Frameworks for ASP.NET Core – Serilog & NLog

Let’s say that there was an exception during the execution of the Get() method. The below code is the exception that gets triggered.

throw new Exception("An error occurred...");

Here is what you would be seeing on Swagger.

image Global Exception Handling in ASP.NET Core - Ultimate Guide

The Console may get you a bit more details on the exception, like the line number and other trace logs.

image 1 Global Exception Handling in ASP.NET Core - Ultimate Guide

Although this is a simple way for handling exceptions, this can also increase the lines of code of our application. Yes, you could have this approach for very simple and small applications. Imagine having to write the try-catch block in each and every controller’s action and other service methods. Pretty repetitive and not feasible, yeah?

It would be ideal if there was a way to handle all the exceptions centrally in one location, right? In the next sections, we will see 2 such approaches that can drastically improve our exception handling mechanism by isolating all the handling logics to a single area. This not only gives a better codebase but a more controlled application with even lesser exception handling concerns.

Default Exception Handling Middleware in ASP.NET Core

To make things easier, UseExceptionHandler Middleware comes out of the box with ASP.NET Core applications. This when configured in the Configure method of the startup class adds a middleware to the pipeline of the application that will catch any exceptions in and out of the application. That’s how Middlewares and pipelines work, yeah?

Let’s see how UseExceptionHandler is implemented. Open up the Configure method in the Startup class of your ASP.NET Core application and configure the following.

app.UseExceptionHandler(
    options =>
    {
        options.Run(async context =>
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                context.Response.ContentType = "text/html";
                var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();
                if (null != exceptionObject)
                {
                    var errorMessage = $"{exceptionObject.Error.Message}";
                    await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);
            }});
    }
);

This is a very basic setup & usage of UseExceptionHandler Middleware. So, whenever there is an exception that is detected within the Pipeline of the application, the control falls back to this middleware, which in return will send a custom response to the request sender.

In this case, a status code of 400 Bad Request is sent along with the Message content of the original exception which in our scenario is ‘An error occurred…’. Pretty straight-forward, yeah? Here is how the exception is displayed on Swagger.

image 2 Global Exception Handling in ASP.NET Core - Ultimate Guide

Now, whenever there is an exception thrown in any part of the application, this middleware catches it and throws the required exception back to the consumer. Much cleaned-up code, yeah? But there are still more ways to make this better, by miles.

Custom Middleware – Global Exception Handling In ASP.NET Core

In this section let’s create a Custom Global Exception Handling Middleware that gives even more control to the developer and makes the entire process much better.

Custom Global Exception Handling Middleware – Firstly, what is it? It’s a piece of code that can be configured as a middleware in the ASP.NET Core pipeline which contains our custom error handling logics. There are a variety of exceptions that can be caught by this pipeline.

We will also be creating Custom Exception classes that can essentially make your application throw more sensible exceptions that can be easily understood.

But before that, let’s build a Response class that I recommend to be a part of every project you build, at least the concept. So, the idea is to make your ASP.NET Core API send uniform responses no matter what kind of requests it gets hit with. This make the work easier for whoever is consuming your API. Additionally it gives a much experience while developing.

Create a new class ApiResponse and copy down the following.

public class ApiResponse<T>
{
    public T Data { get; set; }
    public bool Succeeded { get; set; }
    public string Message { get; set; }
    public static ApiResponse<T> Fail(string errorMessage)
    {
        return new ApiResponse<T> { Succeeded = false, Message = errorMessage };
    }
    public static ApiResponse<T> Success(T data)
    {
        return new ApiResponse<T> { Succeeded = true, Data = data };
    }
}

The ApiResponse class is of a generic type, meaning any kind of data can be passed along with it. Data property will hold the actual data returned from the server. Message contains any Exceptions or Info message in string type. And finally there is a boolean that denotes if the request is a success. You can add multiple other properties as well depending on your requirement.

We also have Fail and Success method that is built specifically for our Exception handling scenario. You can find how this is being used in the upcoming sections.

As mentioned earlier, let’s also create a custom exception. Create a new class and name it SomeException.cs or anything. Make sure that you inherit Exception as the base class. Here is how the custom exception looks like.

public class SomeException : Exception
{
    public SomeException() : base()
    {
    }
    public SomeException(string message) : base(message)
    {
    }
    public SomeException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args))
    {
    }
}

Here is how you would be using this Custom Exception class that we created now.

throw new SomeException("An error occurred...");

Get the idea, right? In this way you can actually differentiate between exceptions. To get even more clarity related to this scenario, let’s say we have other custom exceptions like ProductNotFoundException , StockExpiredException, CustomerInvalidException and so on. Just give some meaningful names so that you can easily identify. Now you can use these exception classes wherever the specific exception arises. This sends the related exception to the middleware, which has logics to handle it.

Now, let’s create the Global Exception Handling Middleware. Create a new class and name it ErrorHandlerMiddleware.cs

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception error)
        {
            var response = context.Response;
            response.ContentType = "application/json";
            var responseModel = ApiResponse<string>.Fail(error.Message);
            switch (error)
            {
                case SomeException e:
                    // custom application error
                    response.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;
                case KeyNotFoundException e:
                    // not found error
                    response.StatusCode = (int)HttpStatusCode.NotFound;
                    break;
                default:
                    // unhandled error
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }
            var result = JsonSerializer.Serialize(responseModel);
            await response.WriteAsync(result);
        }
    }
}

Line 3 – RequestDelegate denotes a HTTP Request completion.
Line 10 – A simple try-catch block over the request delegate. It means that whenever there is an exception of any type in the pipeline for the current request, control goes to the catch block. In this middleware, Catch block has all the goodness.

Line 14 – Catches all the Exceptions. Remember, all our custom exceptions are derived from the Exception base class.
Line 18 – Creates an APIReponse Model out of the error message using the Fail method that we created earlier.
Line 21 – In case the caught exception is of type SomeException, the status code is set to BadRequest. You get the idea, yeah? The other exceptions are also handled in a similar fashion.
Line 34 – Finally, the created api-response model is serialized and send as a response.

Before running this implementation, make sure that you don’t miss adding this middleware to the application pipeline. Open up the Startup.cs / Configure method and add in the following line.

app.UseMiddleware<ErrorHandlerMiddleware>();

Make sure that you comment out or delete the UseExceptionHandler default middleware as it may cause unwanted clashes. It doesn’t make sense to have multiple middlewares doing the same thing, yeah?

I also assume that you have done the necessary changes that will throw the SomeException Exception in the Get method of the default controller you are working with.

With that done, let’s run the application and see how the error get’s displayed on Swagger.

image 3 Global Exception Handling in ASP.NET Core - Ultimate Guide

There you go! You can see how well built the response is and how easy it is to read what the API has to say to the client. Now, we have a completely custom-built error handling mechanism, all in one place. And yes, of course as mentioned earlier, you are always free to add more properties to the API Reponses class that suits your application’s needs.

I have been using this approach for literally all of my open source projects, and it’s With that, let’s wrap up the article for now 😉

Consider supporting me by buying me a coffee.

Thank you for visiting. You can buy me a coffee by clicking the button below. Cheers!

Buy Me A Coffee

Summary

In this article, we have looked through various ways to implement Exception handling in our ASP.NET Core applications. The favorite approach should definitely be the one where we implemented Global Exception Handling in ASP.NET Core using Custom Middlewares. You can also find the complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! 😀

Единственная настоящая ошибка — не исправлять своих прошлых ошибок. Конфуций.

Ошибки в приложении неизбежны, но есть те, которые мы «поджидаем», а есть зайцы, которые проскакивают сквозь дыру в заборе. Как сделать так, чтоб незамеченным не остался ни один заяц, и чтоб пользователь был доволен всегда будет рассказано в этой статье.

Для обработки ошибки выделим три ключевых шага, которые необходимо выполнить:

  1. Перехватить ошибку.
  2. Обработать ошибку (добавить информацию в логи или установить сообщение об ошибке пользователю, выполнить обработку ошибочной операции).
  3. Отобразить приемлемый результат пользователю.

Ключевой особенностью ASP.NET Core MVC приложений, по сравнению с ASP.NET MVC, является то, что в основу архитекторы заложили принцип объединения в цепочку отдельных функциональных частей (middleware).

Middleware цепочка

Соответственно MVC является одним из элементов данной цепочки, и запрос проходит по данной цепочке до обработчика и обратно. Каждый элемент вызывает последующий, если тот существует. И Каждый элемент возвращает результат вызова следующего. А это дает следующий факт — обработать ошибку можно, используя следующие точки влияния:

  1. Try catch в своем коде.
  2. Фильтры исключений MVC.
  3. “Try catch” на уровне middleware.

Обработка на уровне своего кода предельно ясна и ее рассматривать мы не будем. Для MVC мы можем использовать фильтр исключения. Для этого нам нужно создать фильтр исключения и зарегистрировать его для всех действий контроллеров. Код фильтра:

//Класс фильтра ошибок
public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

    public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
    {
        _logger = logger;
    }
    public void OnException(ExceptionContext context)
    {
        _logger.LogError(0, context.Exception, context.Exception.Message);

        context.ExceptionHandled = true;
        context.Result = new ViewResult {ViewName = "Error-500"};
    }
}

В фильтре мы логируем нашу ошибку и возвращаем страницу ошибки сервера «Error-500». Что бы добавить фильтр, мы должны зарегистрировать его в глобальные фильтры MVC, то есть фильтры, которые выполняются для всех действий всех контроллеров.

//Добавляем глобальный фильтр, который будет получать информацию об ошибке
services.AddMvc(options =>
{
    options.Filters.Add(typeof(GlobalExceptionFilter));
});

В данном случае мы обработаем все наши Exception и наследуемые от него, но как же отобразить пользователю красивое окно 404, к примеру, когда он пытался посмотреть ресурс, которого нет. Для этого мы добавим обработку HTTP кодов. В компоненте StatusCodePagesMiddleware есть так же методы UseStatusCodePages, UseStatusCodePagesWithRedirects и UseStatusCodePagesWithReExecute. Объясню разницу между 3мя методами.

UseStatusCodePages – использует текстовое представление HTTP кода ошибки, и все что вы можете изменить, это формат и текст сообщения, который увидете в браузере.

Пример страницы с ошибкой

UseStatusCodePagesWithRedirects – может перенаправить вас на страницу ошибки, но ключевое слово перенаправит, то есть браузер, пытаясь получить не существующий ресурс, получит HTTP код 302 (ресурс перемещен) и будет выполнено перенаправление на страницу ошибки. Но по факту браузер будет думать, что ресурс существует, просто перемещен на страницу ошибки.

UseStatusCodePagesWithReExecute – самый оптимальный вариант, так как может вернуть и HTTP код и страницу ошибки в одном флаконе. Для использования мы зарегистрируем компонент:

//Добавляем разденеие обработки ошибок
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseBrowserLink();
}
else
{
    app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");
}

В строчном параметре {0} будет передано числовое отображение HTTP кода ошибки, с которым мы можем оперировать. Код контроллера с перенаправлением на страницы ошибок:

//Контроллер и действие обработки ошибки
public class StatusCodeController : Controller
{
    private Int32[] _availableCodes = new Int32[] { 404, 500 };

    [Route("/StatusCode/{statusCode}")]
    public IActionResult Index(Int32 statusCode)
    {
        if (!_availableCodes.Contains(statusCode))
            statusCode = 500;
        return View($"Error-{statusCode}");
    }
}

И последняя точка для обработки исключений – обработчик Middleware. Для чего это может быть нужно? Если у нас не используется MVC, а мы написали свой middleware для обработки запроса, то функциональность фильтров из MVC нам не будет доступна, поэтому и используем отдельный middleware для обработки ошибок:

//Middleware для обработки ошибок
public sealed class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlerMiddleware> _logger;
    public ExceptionHandlerMiddleware(RequestDelegate next, ILogger<ExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            try
            {
                //Обработка ошибки, к примеру, просто можем логировать сообщение ошибки
            }
            catch (Exception innerException)
            {
                _logger.LogError(0, innerException, "Ошибка обработки исключения");
            }
            // Если в коде обработки ошибки мы снова получили ошибку, то пробрасываем ее выше по цепочке Middleware
            throw;
        }
    }
}

Регистрируем наш middleware:

app.UseMiddleware<ExceptionHandlerMiddleware>();

Ключевой момент – обработчик должен быть зарегистрирован раньше всех остальных элементов цепочки, потому как, если произойдет исключение в цепочке до обработчика, то он не будет выполнятся и информацию об ошибке не получит. В ASP.NET Core важен порядок регистрации элементов middleware. Ну и самый простой вариант, это, не создавая свой middleware, обернуть все регистрации в try catch и обработать ошибку. Принцип работы тот же, только менее красиво.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Обработка ошибки отсутствия страницы
  • Обработка ошибки 500 python