Error view asp net core

Обработка ошибок и исключений в 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

Just recently I realized this blog had a small issue.
While there was a custom error page for 404s, it was done with a redirect, and the view was returned with a 200 OK.
This meant that in my telemetry, I would see the PageNotFound entries, but not what page they tried to access.

So I set out to fix this issue along with some other related things.
This is how I implemented custom error pages for 404s and exceptions on this blog.

Custom error pages

So what is a custom error page?
It is a nice-looking view that is meant to be used when something goes wrong in a production environment.

In development you would use:

app.UseDeveloperExceptionPage();

Now when an exception occurs, you will see a view that describes what went wrong and where.

But it should not be used in production as it could lead to security problems.
Users might also consider your site less stable, and never visit again.

So we want to show them something that says something like:

Oops! Something went wrong. We are very sorry, please try again in a moment. If the error persists…

In case of a 404 status code, by default ASP.NET Core just returns a white page.

Instead we would like to show something like:

Hey that thing you tried to access does not exist. Please check the URL is correct.

Exception handler middleware

Let’s start by handling exceptions properly.

In our application pipeline configuration, we will add the exception handler middleware:

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

So what does this middleware do?

Well, put simply it:

  1. Calls the next middleware with a try-catch block
  2. If an exception is caught, the next middleware is called again with the request path set to what you gave as an argument

This re-execution means the original URL is preserved in the browser.

The controller and action that is implemented looks like this:

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

    public ErrorController(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

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

I took a look at the source code of the exception handler middleware,
and found that it sets this IExceptionHandlerPathFeature on the context before re-executing the request.

Here we access it to get the relative URL the user tried to access and the exception that occurred.
We use Application Insights to track the exception.

The view is pretty simple:

@{
    ViewBag.Title = "Error occurred";
}

<h1>We have a problem</h1>

<p>Sorry, an error occurred while executing your request.</p>

Now when an exception is thrown:

  1. The exception is logged in Application Insights
  2. User gets a nice-looking view instead of a stack trace
  3. The original URL is preserved in the browser so the user can try to refresh
  4. The response comes back with a 500 status code so it is tracked as a failed request in Application Insights

Handling 404s

We also want to handle 404 status codes gracefully.

In this blog, it could happen because the URL did not map to a controller action.
Or it might have, but we could not find something in the database.

404s are handled with a small middleware that is placed before the MVC middleware:

app.Use(async (ctx, next) =>
{
    await next();

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

Re-executing a request is not hard as you can see.
We just call await next(); again.

Here we grab the original URL and put it in the HttpContext.Items collection.

This is the action that then handles the error in the ErrorController introduced earlier:

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

We track the event with a custom event in Application Insights, and include the original URL in the properties.

The view is quite simple again:

@{
    ViewBag.Title = "404";
}

<h1>404 - Page not found</h1>

<p>Oops, better check that URL.</p>

Go ahead, try accessing this link to see how it looks like: Test 404.

If you open the browser DevTools, you can again see we get the right status code: 404.

And the original URL is preserved in the browser.

Conclusions

Thanks for reading!

Feel free to add a comment if you have questions or improvement suggestions.

Official error handling docs can be found here: docs.asp.net.

This is the eighth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

  • Generic Host Builder in ASP .NET Core 3.1

NetLearner on GitHub :

  • Repository: https://github.com/shahedc/NetLearnerApp
  • v0.8-alpha release: https://github.com/shahedc/NetLearnerApp/releases/tag/v0.8-alpha

In this Article:

  • H is for Handling Errors
  • Exceptions with Try-Catch-Finally
  • Try-Catch-Finally in NetLearner
  • Error Handling for MVC
  • Error Handling for Razor Pages
  • Error Handling in Blazor
  • Logging Errors
  • Transient Fault Handling
  • References

H is for Handling Errors

Unless you’re perfect 100% of the time (who is?), you’ll most likely have errors in your code. If your code doesn’t build due to compilation errors, you can probably correct that by fixing the offending code. But if your application encounters runtime errors while it’s being used, you may not be able to anticipate every possible scenario.

Runtime errors may cause Exceptions, which can be caught and handled in many programming languages. Unit tests will help you write better code, minimize errors and create new features with confidence. In the meantime, there’s the good ol’ try-catch-finally block, which should be familiar to most developers.

NOTE: You may skip to the next section below if you don’t need this refresher.

try-catch-finally in C#

Exceptions with Try-Catch-Finally

The simplest form of a try-catch block looks something like this:

**try** { _ **// try something here** _} **catch** ( **Exception** ex){ _ **// catch an exception here** _}

You can chain multiple catch blocks, starting with more specific exceptions. This allows you to catch more generic exceptions toward the end of your try-catch code. In a string of _ catch _() blocks, only the caught exception (if any) will cause that block of code to run.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _}

Finally, you can add the optional _ finally _block. Whether or not an exception has occurred, the finally block will always be executed.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _} **finally** { _ **// always run this code** _}

Try-Catch-Finally in NetLearner

In the NetLearner sample app, the LearningResourcesController uses a LearningResourceService from a shared .NET Standard Library to handle database updates. In the overloaded Edit() method, it wraps a call to the Update() method from the service class to catch a possible DbUpdateConcurrencyException exception. First, it checks to see whether the ModelState is valid or not.

if (ModelState.IsValid){ **...** }

Then, it tries to update user-submitted data by passing a LearningResource object. If a DbUpdateConcurrencyException exception occurs, there is a check to verify whether the current LearningResource even exists or not, so that a 404 (NotFound) can be returned if necessary.

**try** { await _learningResourceService.Update(learningResource);} **catch** ( **DbUpdateConcurrencyException** ){ if (!LearningResourceExists(learningResource.Id)) { return NotFound(); } else { **throw** ; }}return RedirectToAction(nameof(Index));

In the above code, you can also see a throw keyword by itself, when the expected exception occurs if the Id is found to exist. In this case, the throw statement (followed immediately by a semicolon) ensures that the exception is rethrown, while preserving the stack trace.

Run the MVC app and navigate to the Learning Resources page after adding some items. If there are no errors, you should see just the items retrieved from the database.

Learning Resources UI (no errors)

If an exception occurs in the Update() method, this will cause the expected exception to be thrown. To simulate this exception, you can intentionally throw the exception inside the update method. This should cause the error to be displayed in the web UI when attempting to save a learning resource.

Exception details in Web UI

Error Handling for MVC

In ASP .NET Core MVC web apps, unhandled exceptions are typically handled in different ways, depending on whatever environment the app is running in. The default template uses the DeveloperExceptionPage middleware in a development environment but redirects to a shared Error view in non-development scenarios. This logic is implemented in the Configure () method of the Startup.cs class.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ **app.UseExceptionHandler("/Home/Error");** _ **...** _}

The DeveloperExceptionPage middleware can be further customized with DeveloperExceptionPageOptions properties, such as FileProvider and SourceCodeLineCount.

var options = new **DeveloperExceptionPageOptions** { **SourceCodeLineCount** = 2}; app.UseDeveloperExceptionPage(options); 

Using the snippet shown above, the error page will show the offending line in red, with a variable number of lines of code above it. The number of lines is determined by the value of SourceCodeLineCount , which is set to 2 in this case. In this contrived example, I’m forcing the exception by throwing a new Exception in my code.

Customized lines of code within error page

For non-dev scenarios, the shared Error view can be further customized by updating the Error.cshtml view in the Shared subfolder. The ErrorViewModel has a ShowRequestId boolean value that can be set to true to see the RequestId value.

@model **ErrorViewModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){<p><strong>Request ID:</strong> <code>@Model. **RequestId** </code></p>}<h3>header content</h3><p>text content</p> 

In the MVC project’s Home Controller, the Error () action method sets the RequestId to the current Activity.Current.Id if available or else it uses HttpContext.TraceIdentifier. These values can be useful during debugging.

[**ResponseCache** (Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]public IActionResult **Error** (){ return View(new **ErrorViewModel** { **RequestId** = Activity.Current?.Id ?? HttpContext.TraceIdentifier });} 

But wait… what about Web API in ASP .NET Core? After posting the 2019 versions of this article in a popular ASP .NET Core group on Facebook, I got some valuable feedback from the admin:

Dmitry Pavlov: “For APIs there is a nice option to handle errors globally with the custom middleware https://code-maze.com/global-error-handling-aspnetcore – helps to get rid of try/catch-es in your code. Could be used together with FluentValidation and MediatR – you can configure mapping specific exception types to appropriate status codes (400 bad response, 404 not found, and so on to make it more user friendly and avoid using 500 for everything).”

For more information on the aforementioned items, check out the following resources:

  • Global Error Handling in ASP.NET Core Web API: https://code-maze.com/global-error-handling-aspnetcore/
  • FluentValidation • ASP.NET Integration: https://fluentvalidation.net/aspnet
  • MediatR Wiki: https://github.com/jbogard/MediatR/wiki
  • Using MediatR in ASPNET Core Apps: https://ardalis.com/using-mediatr-in-aspnet-core-apps

Later on in this series, we’ll cover ASP .NET Core Web API in more detail, when we get to “W is for Web API”. Stay tuned!

Error Handling for Razor Pages

Since Razor Pages still use the MVC middleware pipeline, the exception handling is similar to the scenarios described above. For starters, here’s what the Configure () method looks like in the Startup.cs file for the Razor Pages web app sample.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ app. **UseExceptionHandler** ("/Error"); ...}

In the above code, you can see the that development environment uses the same DeveloperExceptionPage middleware. This can be customized using the same techniques outlined in the previous section for MVC pages, so we won’t go over this again.

As for the non-dev scenario, the exception handler is slightly different for Razor Pages. Instead of pointing to the Home controller’s Error () action method (as the MVC version does), it points to the to the / Error page route. This Error.cshtml Razor Page found in the root level of the Pages folder.

@ **page** @model **ErrorModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){ <p> <strong>Request ID:</strong> <code>@Model. **RequestId** </code> </p>}<h3>custom header text</h3><p>custom body text</p>

The above Error page for looks almost identical to the Error view we saw in the previous section, with some notable differences:

  • @ page directive (required for Razor Pages, no equivalent for MVC view)
  • uses ErrorModel (associated with Error page) instead of ErrorViewModel (served by Home controller’s action method)

Error Handling for Blazor

In Blazor web apps, the UI for error handling is included in the Blazor project templates. Consequently, this UI is available in the NetLearner Blazor sample as well. The _Host.cshtml file in the Pages folder holds the following < environment > elements:

<div id="blazor-error-ui"> < **environment** include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </ **environment** > < **environment** include="Development"> An unhandled exception has occurred. See browser dev tools for details. </ **environment** > <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>

The div identified by the id “blazor-error-ui” ensures that is hidden by default, but only shown when an error has occured. Server-side Blazor maintains a connection to the end-user by preserving state with the use of a so-called circuit.

Each browser/tab instance initiates a new circuit. An unhandled exception may terminate a circuit. In this case, the user will have to reload their browser/tab to establish a new circuit.

According to the official documentation, unhandled exceptions may occur in the following areas:

  1. Component instantiation : when constructor invoked
  2. Lifecycle methods : (see Blazor post for details)
  3. Upon rendering : during BuildRenderTree() in .razor component
  4. UI Events : e.g. onclick events, data binding on UI elements
  5. During disposal : while component’s .Dispose() method is called
  6. JavaScript Interop : during calls to IJSRuntime.InvokeAsync
  7. Prerendering : when using the Component tag helper

To avoid unhandled exceptions, use try-catch exception handlers within .razor components to display error messages that are visible to the user. For more details on various scenarios, check out the official documentation at:

  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Logging Errors

To log errors in ASP .NET Core, you can use the built-in logging features or 3rd-party logging providers. In ASP .NET Core 2.x, the use of CreateDefaultBuilder () in Program.cs takes of care default Logging setup and configuration (behind the scenes).

public static IWebHostBuilder **CreateWebHostBuilder** (string[] args) => WebHost. **CreateDefaultBuilder** (args) .UseStartup<Startup>();

The Web Host Builder was replaced by the Generic Host Builder in ASP .NET Core 3.0, so it looks slightly different now. For more information on Generic Host Builder, take a look at the previous blog post in this series: Generic Host Builder in ASP .NET Core.

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

The host can be used to set up logging configuration, e.g.:

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureLogging** ((hostingContext, logging) => { logging.ClearProviders(); logging.AddConsole(options => options.IncludeScopes = true); logging.AddDebug(); }) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

To make use of error logging (in addition to other types of logging) in your web app, you may call the necessary methods in your controller’s action methods or equivalent. Here, you can log various levels of information, warnings and errors at various severity levels.

As seen in the snippet below, you have to do the following in your _ MVC Controller _ that you want to add Logging to:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyController** : Controller{ ... **private**  **readonly ILogger _logger;** public **MyController** (..., ILogger< **MyController** > logger) { ... **_logger = logger;** } public IActionResult MyAction(...) { _logger. **LogTrace** ("log trace"); _logger. **LogDebug** ("log debug"); _logger. **LogInformation** ("log info"); _logger. **LogWarning** ("log warning"); _logger. **LogError** ("log error"); _logger. **LogCritical** ("log critical"); }}

In Razor Pages, the logging code will go into the Page’s corresponding Model class. As seen in the snippet below, you have to do the following to the _ Model class that corresponds to a Razor Page _:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyPageModel** : PageModel{ ... **private readonly ILogger _logger;** public **MyPageModel** (..., **ILogger<MyPageModel> logger** ) { ... **_logger = logger;** } ... public void **MyPageMethod** () { ... _logger. **LogInformation** ("log info"); _logger. **LogError** ("log error"); ... }} 

You may have noticed that Steps 1 through 5 are pretty much identical for MVC and Razor Pages. This makes it very easy to quickly add all sorts of logging into your application, including error logging.

Transient fault handling

Although it’s beyond the scope of this article, it’s worth mentioning that you can avoid transient faults (e.g. temporary database connection losses) by using some proven patterns, practices and existing libraries. To get some history on transient faults, check out the following article from the classic “patterns & practices”. It describes the so-called “Transient Fault Handling Application Block”.

  • Classic Patterns & Practices: https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn440719(v=pandp.60)

More recently, check out the docs on Transient Fault Handling:

  • Docs: https://docs.microsoft.com/en-us/aspnet/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/transient-fault-handling

And now in .NET Core, you can add resilience and transient fault handling to your .NET Core HttpClient with Polly!

  • Adding Resilience and Transient Fault handling to your .NET Core HttpClient with Polly: https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx
  • Integrating with Polly for transient fault handling: https://www.stevejgordon.co.uk/httpclientfactory-using-polly-for-transient-fault-handling
  • Using Polly for .NET Resilience with .NET Core: https://www.telerik.com/blogs/using-polly-for-net-resilience-and-transient-fault-handling-with-net-core

You can get more information on the Polly project on the official Github page:

  • Polly on Github: https://github.com/App-vNext/Polly

References

  • try-catch-finally: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch-finally
  • Handle errors in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
  • Use multiple environments in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments
  • UseDeveloperExceptionPage: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage
  • DeveloperExceptionPageOptions: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageoptions
  • Logging: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging
  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Anyone that has used the full .net MVC framework has spent many an hour trying to rejig the web.config and custom MVC filters to get custom error pages going. Often it would lead you on a wild goose chase around Stack Overflow finding answers that went something along the lines of “just do this one super easy thing and it will work”… It never worked.

.net Core has completely re-invented how custom errors work. Partly because with no web.config, any XML configuration is out the window, and partly because the new “middleware pipeline” mostly does away with the plethora of MVC filters you had to use in the past.

Developer Exception Page

The developer exception page is more or less the error page you used to see in full .net framework if you had custom errors off. That is, you could see the stack trace of the error and other important info to help you debug the issue.

By default, new ASP.net core templates come with this turned on when creating a new project. You can check this by looking at the Configure method of your startup.cs file. It should look pretty close to the following.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}

	app.UseMvcWithDefaultRoute();
}

Note that checking if the environment is development is so important! Just like the CustomErrors tag in the full framework, you only want to leak out your stacktrace and other sensitive info if you are debugging this locally or you specifically need to see what’s going on for testing purposes. Under no circumstances should you just turn on the “UseDeveloperExceptionPage” middleware without first making sure you are working locally (Or some other specific condition).

Another important thing to note is that as always, the ordering of your middleware matters. Ensure that you are adding the Developer Exception Page middleware before you are going into MVC. Without it, MVC (Or any other middleware in your pipeline), can short circuit the process without it ever reaching the Developer Exception page code.

If your code encounters an exception now, you should see something similar to the following :

As we can see, we get the full stack as well as being able to see any query we sent, cookies and other headers. Again, not things we want to start leaking out all over the place.

Exception Handler Page

ASP.net core comes with a catch all middleware that handles all exceptions and will redirect the user to a particular error page. This is pretty similar to the default redirect in the CustomErrors attribute in web.config or the HandleError attribute in full framework MVC. An important note is that this is an “exception” handler. Other status code errors (404 for example) do not get caught and redirected using this middleware.

The pattern is usually to show the developer error page when in the development environment, otherwise to redirect to the error page. So it might look a bit like this :

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/home/error");
	}

	app.UseMvcWithDefaultRoute();
}

You would then need to create an action to handle the error. One very important thing to note is that the ExceptionHandler will be called with the same HTTP Verb as the original request. e.g. If the exception happened on a Post request, then your handler at /home/error should be able to accept Posts.

What this means in practice is that you should not decorate your action with any particular HTTP verb, just allow it to accept them all.

Statuscode Pages

ASP.net core comes with an inbuilt middleware that allows you to capture other types of HTTP status codes (Other than say 500), and be able to show them certain content based on the status code.

There are actually two different ways to get this going. The first is :

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

Using “StatusCodePagesWithRedirects” you redirect the user to the status page. The issue with this is that the client is returned a 302 and not the original status code (For example a 404). Another issue is that if the exception is somewhere in your pipeline you are essentially restarting the pipeline again (And it could throw the same issue up).

The second option is :

app.UseStatusCodePagesWithReExecute("/error/{0}");

Using ReExecute, your original response code is returned but the content of the response is from your specified handler. This means that 404’s will be treated as such by spiders and browsers, but it still allows you to display some custom content (Such as a nice 404 page for a user).

Custom Middleware

Remember that you can always create custom middleware to handle any exception/status code in your pipeline. Here is an example of a very simple middleware to handle a 404 status code.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
	app.Use(async (context, next) =>
	{
		await next.Invoke();

		//After going down the pipeline check if we 404'd. 
		if (context.Response.StatusCode == StatusCodes.Status404NotFound)
		{
			await context.Response.WriteAsync("Woops! We 404'd");
		}
	});

	app.UseMvcWithDefaultRoute();
}

On our way back out (Remember, the code after the “next” is code to be run on the way out of the pipeline), we check if the status code was 404, if it is then we return a nice little message letting people know we 404’d.

If you want more info on how to create a custom middleware to handle exceptions (Including how to write a nice class to wrap it), check out our tutorial on writing custom middleware in asp.net core.

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

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

Понравилась статья? Поделить с друзьями:
  • Error video footage
  • Error video driver crashed and was reset sea of thieves
  • Error video data loading ivi
  • Error vgacon disables amdgpu kernel modesetting
  • Error vertex file for player zombie mdl checksum 797198640 should be 1815117791