Mvc runtime error

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 A…

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<T>
  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<model> 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<model> 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

ASP.NET MVC gives you more options in the way that you handle exceptions. Error handling isn’t intrinsically exciting, but there are many ways of avoiding the classic yellow page of death, even getting ELMAH to manage error handling for you.

Years ago,  ASP.NET’s error handling was one of the major things that made me wonder if ASP.NET MVC could give me something that ASP.NET Web Forms couldn’t. Web Forms is based on pages; so if something goes wrong, all that you can do is to redirect the user to another page and explain what the error was or just be generically sorry. ASP.NET Web Forms allow you to map an error page for each possible HTTP status code. You control the mapping through the <customErrors> section of the web.config file.

Because of the  different architecture of the view in ASP.NET MVC,  it  is possible to save the redirect command and then programmatically switch to an error view in the context of the same request. You have this in addition to the regular page-based error handling mechanism. I wouldn’t use HTTP code redirects in ASP.NET MVC; but only because more flexible solutions are possible.

Generally speaking, error handling in ASP.NET MVC is mainly necessary to handle program and route exceptions. Program exceptions refer to catching errors in controllers and in any code you may have in Razor views. Route exceptions refer to missing links and invalid URLs.

Program Exceptions

Any stack trace you can have out of an ASP.NET MVC application originates from a method call in a controller class. The controller class, therefore, is where any exceptions in your ASP.NET MVC code can be trapped. You can do that in a number of equivalent ways. For example, you can have a try/catch block surrounding the entire method body. It works, but it’s ugly to see too. A better option is probably to override the OnException method from the base Controller class. Yet another option is using the HandleError attribute at the controller class level. Better yet, the HandleError attribute-which is ultimately an action filter-can be set globally on just every controllers and actions you can have.

At the end of the day, an effective strategy for error handling is based on the following pillars:

  • All classes down the controller level just throw exceptions when something goes wrong.
  • Some of these classes, in some of their methods, may attempt to catch some of the exceptions but mostly when a given exception is intended to be swallowed or turned into some other exception.
  • At the application level you use the HandleError global filter to catch whatever bubbles up.

Swallowing exceptions is in general a dangerous practice; but in the end it is not more dangerous than crossing the street when it’s red but there are no cars in sight. It works well as long as it doesn’t become a common practice and as long as it’s applied with a grain, or maybe two, of salt. Swallowing an exception is fine for example if your code is trying to call an external HTTP endpoint and the call times out or fails for whatever reason. In this case it might be acceptable that the routine that takes care of the call just hides the actual HTTP status code and packs the event as a special case of the regular response. Here’s an example taken from a data access repository class:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public Order FindByCustomerAndId(int id, string customerId)

{

using (var db = new MyAppEntities())

 {

try

{

var order = (from o in db.Orders

where o.OrderId == id && o.Buyer.CustomerId == customerId

select o).Single();

return order;

}

catch (InvalidOperationException)

{

return new NullOrder();

}

}

}

In LINQ, the Single method just throws an exception if the response includes any number of items different from one. The internal try/catch block swallows the exception and returns a special version of the Order type that just evaluates to NULL.

The NullOrder class is an instance of the Special Case pattern and has the merit of not killing polymorphism in code as NULL would do. The caller of the aforementioned method will have then the following skeleton:

var order = _orderRepository.FindByCustomerAndId(orderId, customerId);

if (order is NullOrder)

{

...

}

There are a few guidelines you might want to adhere to when it comes to handling exceptions. The first aspect to consider is that the catch block is quite expensive and raises a peak of CPU usage when your code gets into it. For this reason, over-catching may end up affecting the overall performance of the application. It’s probably not a big deal if your code is frontend; but for server-side code scaling up the performance of the entire system it might become problematic.

A guideline from the .NET Framework team is that you never throw an exception using the System.Exception class. You should use more specific exception types whether built-in types such as InvalidOperationException and NullReferenceException or your own application specific types. On the other hand, you should also resist the temptation of having your own exception types sprinkled everywhere and even replacing analogous .NET Framework native types.

When it comes to exceptions, you should be very specific about the exception-type that you pick up and should also create instances providing as much information as possible. For example, ArgumentNullException is way more specific than ArgumentException. If the problem consists in an unexpected NULL parameter then you should go for ArgumentNullException. Furthermore, be aware that any exceptions come with a message. Provide details within the message as the message itself is targeted to developers. Another parameter of exception types that is often neglected is the name of the parameter where the exception occurred-mention it every time. It can be a lifesaver sometimes.

The OnException Method

In ASP.NET MVC, any method of any controller runs under the aegis of a system component known as the action invoker. The invoker runs all the code within a try/catch block and simply re-throws a thread-abort exception. For all other exceptions, instead, it goes through the list of registered action filters and gives each a chance to recover from the exception. At the end of the loop, if the exception has not been marked as handled, the exception originally caught is re-thrown. What happens next depends on whether you have other mechanism of exception handling set to watch over the entire application. If none is in place, which is the default, users will experience the ASP.NET classic yellow page of death or any other error page you arranged.

An action filter enabled to handle exceptions can be a separate class defined as an action filter (inherit from the ActionFilter class) or it can simply be a controller class that overrides the OnException method. This method is always invoked when an unhandled exception occurs in the course of the action method.

protected override void OnException(ExceptionContext filterContext)

{

...

}

It’s important to be aware that no exception that originates outside the controller will be caught by OnException. An excellent example of an exception not being caught by OnException is a ‘null reference’ exception that results in the model-binding layer. Another example is ‘route not-found’ exception.

The code in OnException has the power of controlling the entire response for the request that failed. As shown above, the method receives a parameter of type ExceptionContext which has an ActionResult property named Result. This property just refers to the next view or result past the method. If you set the Result property you can control the next screen; if you omit setting any result, then the user will see just a blank screen. Here’s a typical implementation of OnException:

protected override void OnException(ExceptionContext filterContext)

{

// Let other exceptions just go unhandled

if (filterContext.Exception is InvalidOperationException)

{

// Default view is «error«

filterContext.SwitchToErrorView();

}

}

The SwitchToErrorView method is an extension method for the ExceptionContext class with the following signature:

public static void SwitchToErrorView(this ExceptionContext context,

String view = «error«, String master = «»)

As you can see, you’re allowed to indicate the next view and even its layout.

The HandleError Attribute

If you don’t like the explicit override of OnException you can decorate the class (or just individual methods) with the HandleError attribute. 

[HandleError]

public class HomeController

{

...

}

As mentioned, HandleError is an action filter and not a plain attribute carrying just a bunch of metadata. In particular, HandleError implements the IExceptionFilter interface:

public interface IExceptionFilter

{

void OnException(ExceptionContext filterContext);

}

Internally, HandleError implements OnException using a piece of code very similar to the SwitchToErrorView method discussed earlier. A minor difference is that HandleError doesn’t trap any exceptions resulting from child actions. Properties on the attribute lets you select the exceptions to trap and views to redirect to.

[HandleError(ExceptionType=typeof(ArgumentException), View=«generic«)]

Each method can have multiple occurrences of the attribute, one for each exception you’re interested in. By default, also HandleError redirects to the same view named error we considered earlier. Note that such a view is purposely created by the ASP.NET MVC templates in Visual Studio.

When using HandleError at development time, it’s crucial to be aware that the attribute doesn’t have any effect unless you enable custom errors at the application level:

<customErrors mode=«On«>

</customErrors>

When you go live, remote users will correctly receive the selected error page regardless. To test the feature, though, you need to change the configuration file.

HandleError can be automatically applied to any method of any controller class by registering it as a global filter in global.asax:

public class MvcApplication : System.Web.HttpApplication

{

protected void Application_Start()

{

RegisterGlobalFilters(GlobalFilters.Filters);

...

}

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

{

filters.Add(new HandleErrorAttribute());

}

}

Global filters are automatically added to the list of filters before the action invoker calls out any action method.

All Other Possible Errors

An error can always find its way to the user. For this reason, we’ve been given the Application_Error method in global.asax  ince the very first version of the ASP.NET runtime. It is just there to handle any possible errors that passed through try/catch blocks. The Error event fires whenever an unhandled exception reaches the outermost shell of ASP.NET code.  It’s the final call for developer’s code before the yellow screen of death.

You could do something useful in this event handler, such as sending an email or writing to the event log. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

void Application_Error(Object sender, EventArgs e)

{

var exception = Server.GetLastError();

if (exception == null)

return;

var mail = new MailMessage { From = new MailAddress(«automated@contoso.com«) };

mail.To.Add(new MailAddress(«administrator@contoso.com«));

mail.Subject = «Site Error at « + DateTime.Now;

mail.Body = «Error Description: « + exception.Message;

var server = new SmtpClient { Host = «your.smtp.server« };

server.Send(mail);

// Clear the error

Server.ClearError();

// Redirect to a landing page

Response.Redirect(«home/landing«);

}

While you can always write the Error handler yourself, ASP.NET developers often use ELMAH. At the very end of the day, ELMAH is an HTTP module that, once configured, intercepts the Error event at the application level and logs it according to the configuration of a number of back-end repositories. The bottom line is that with ELMAH you can handle errors in many more ways and change /add actions with limited work; and without writing much code yourself.  ELMAH also offers some nice facilities, such as a web page you can use to view all recorded exceptions and drill down into each of them.  

ELMAH is an open-source project available at http://code.google.com/p/elmah. It is so popular that it counts a number of extensions, mostly in the area of repositories. To integrate it in your applications the easiest path you can take is the Nuget package you find at http://www.nuget.org/packages/elmah/1.2.2.

Summary

Error handling is one of the most bothersome parts of software development. One of the reasons that developers avoid it is that it doesn’t seem to require much creativity. In this regard, I think that ELMAH is emblematic. Nearly any developers knows that an HTTP module could do the trick of saving rewriting the same code over and over again to send emails and log errors on ASP.NET sites. And I guess as many developers had, in the past, a thought crossing their minds about writing a sort of simple but effective infrastructure for error handling and reporting. That’s just what ELMAH is-and that’s what ASP.NET developers need. Oh well, in addition to ad hoc try/catch blocks in the code.

Joydip Kanjilal

By ,

Columnist,

InfoWorld

|

Take advantage of global exception handling to avoid writing scads of boilerplate exception handling code in ASP.NET Core MVC.

An exclamation-mark alert in a field of abstract technology.

Alengo

Exceptions are runtime errors that might occur in your application. If exceptions are not handled properly, the program in execution is terminated. This article presents a discussion of how global error handling can be implemented in ASP.NET Core MVC, along with relevant code examples and screen images to illustrate the concepts. 

To work with the code examples in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.

Create an ASP.NET Core MVC project in Visual Studio

First off, let’s create an ASP.NET Core project in Visual Studio 2019. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new ASP.NET Core project in Visual Studio.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “ASP.NET Core Web Application” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Optionally, select the “Place solution and project in the same directory” check box.
  7. Click Create.
  8. In the “Create a New ASP.NET Core Web Application” window shown next, select .NET Core as the runtime and ASP.NET Core 2.2 (or later) from the drop-down list at the top.
  9. Select “Web Application (Model-View-Controller)” as the project template to create a new ASP.NET Core MVC application. 
  10. Ensure that the check boxes “Enable Docker Support” and “Configure for HTTPS” are unchecked as we won’t be using those features here.
  11. Ensure that Authentication is set to “No Authentication” as we won’t be using authentication either.
  12. Click Create. 

Following these steps should create a new ASP.NET Core MVC project in Visual Studio. We’ll use this project in the sections below to illustrate handling null responses in ASP.NET Core MVC.

Global exception handling options in ASP.NET Core MVC

Support for global exception handling is built into ASP.NET Core MVC. You can take advantage of global exception handling middleware to configure exception handling at a central place and hence reduce the amount of exception handling code that you would otherwise have to write in your application.

A second approach to handling exceptions globally in ASP.NET Core MVC is to use exception filters. You can read about exception filters in my previous article here. In this article, we’ll use the built-in global exception handling middleware and the UseExceptionHandler method discussed in the next section.

Use the UseExceptionHandler extension method in ASP.NET Core

UseExceptionHandler is an extension method that registers the ExceptionHandler middleware with the request processing pipeline.

You can use the IExceptionHandlerFeature interface to retrieve the exception object. The following code snippet illustrates how the UseExceptionHandler method can be used to handle exceptions globally.

app.UseExceptionHandler(
             builder =>
             {
                 builder.Run(
                 async context =>
                 {
                     context.Response.StatusCode =
                  (int)HttpStatusCode.InternalServerError;
                     context.Response.ContentType =
                     "application/json";
                     var exception =
                     context.Features.Get
                     <IExceptionHandlerFeature>();
                     if (exception != null)
                     {
                         var error = new ErrorMessage()
                         {
                             Stacktrace =
                             exception.Error.StackTrace,
                             Message = exception.Error.Message
                         };
                         var errObj =
                         JsonConvert.SerializeObject(error);
                         await context.Response.WriteAsync
                         (errObj).ConfigureAwait(false);
                     }
                 });
             }
        );

Here is the ErrorMessage class for your reference.

    public class ErrorMessage
    {
        public string Message { get; set; }
        public string Stacktrace { get; set; }
    }

Configure the global exception handling middleware in ASP.NET Core MVC

Once you have created a new ASP.NET Core MVC application, the Startup.cs file will contain a Configure method that configures global exception handling middleware as shown in the code snippet below.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template:
                    "{controller=Home}/{action=Index}/{id?}");
            });
        }

Note the statement app.UseExceptionHandler("/Error"); in the above code snippet. The UseExceptionHandler method registers the ExceptionHandler middleware and then routes the user to "/Error" whenever there is an unhandled error in the application.

You can take advantage of the UseStatusCodePagesWithReExecute extension method to add status code pages to the pipeline. You can use this method to handle the HTTP 404 errors as well. The following code snippet shows the updated Configure method.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
   {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseStatusCodePagesWithReExecute("/Error/NotFound/{0}");
            }
      //Other code
   }

Use the ErrorController class in ASP.NET Core MVC

Note that in the HomeController class you’ll find an action method to handle errors. We will not use this action method, so you can go ahead and delete it. Here is the ErrorController class that contains the action method for the "/Error" route.

public class ErrorController : Controller
    {
        [HttpGet("/Error")]
        public IActionResult Index()
        {
            IExceptionHandlerPathFeature
            iExceptionHandlerFeature =
            HttpContext.Features.Get
            <IExceptionHandlerPathFeature>();
            if (iExceptionHandlerFeature != null)
            {
                string path = iExceptionHandlerFeature.Path;
                Exception exception =
                iExceptionHandlerFeature.Error;
                //Write code here to log the exception details
                return View("Error",
                iExceptionHandlerFeature);
            }                
            return View();
        }
        [HttpGet("/Error/NotFound/{statusCode}")]
        public IActionResult NotFound(int statusCode)
        {
            var iStatusCodeReExecuteFeature =   
            HttpContext.Features.Get
            <IStatusCodeReExecuteFeature>();
            return View("NotFound",
            iStatusCodeReExecuteFeature.OriginalPath);
        }
    }

You can take advantage of IExceptionHandlerPathFeature to retrieve exception metadata. You can also take advantage of IStatusCodeReExecuteFeature to retreive the path where a HTTP 404 exception occurred. To leverage IExceptionHandlerPathFeature and IStatusCodeReExecuteFeature, ensure that the Microsoft.AspNetCore.Diagnostics NuGet package is installed in your project. The following statement illustrates how you can retrieve the route in which the exception has occurred.

string route = iExceptionHandlerFeature.Path;

To retrieve the exception details, you can write the following code.

var exception = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

Once you have retrieved the route and exception details, you can write your own code to log the details as well.

Create the views to display error messages in ASP.NET Core MVC

You’ll need a View to display the error messages. Here is the code for the Error view page.

@model Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature
@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row">
    <div class="text-danger">
        <h3>Error: @Model.Error.Message</h3>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <p>@Model.Error.StackTrace</p>
        <p>@Model.Error.InnerException</p>
    </div>
</div>

Here is the code for the NotFound view page.

@model string
@{
    ViewData["Title"] = "NotFound";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
    <h1 class="text-danger">
    Error: The requested URL @Model was not found!</h1>
<hr />

Now, when you execute the application, you should see the following output for a global exception and an HTTP 404 error, respectively.

asp net mvc global exception IDG

Figure 1: An exception handled globally in ASP.NET Core MVC. 

And, here’s how HTTP 404 error would be handled if you try to browse an URL that is not existent.

asp net mvc http 404 error IDG

Figure 2: An HTTP 404 error handled globally in ASP.NET Core MVC.

The support for global exception handling is built into ASP.NET Core. You can leverage global exception handling in ASP.NET Core to handle all exceptions in a central place in your application, and to handle exceptions differently based on the environment —development, production, etc. — the application is running in.

Joydip Kanjilal is a Microsoft MVP in ASP.Net, as well as a speaker and author of several books and articles. He has more than 20 years of experience in IT including more than 16 years in Microsoft .Net and related technologies.

Copyright © 2019 IDG Communications, Inc.

Introduction

No matter how proficiently you developed your application there are chances that your code may not work as expected and will generate an error at runtime. Users may enter some invalid data, mathematical calculations can go wrong, some network level fault may cause errors and more. That is why it is always a good idea to implement a robust error handling mechanism in your web application. To that end ASP.NET MVC offers several techniques that help you build such an error handling mechanism. This article discusses them with examples.

Exception Handling Techniques for ASP.NET MVC

Before we get into the actual error handling techniques offered by ASP.NET MVC, let’s quickly enumerate them here:

  • try…catch
  • Overriding OnException method
  • Using the [HandleError] attribute on actions and controllers
  • Setting a global exception handling filter
  • Handling Application_Error event

The first technique listed above is not specific to ASP.NET MVC; it is applicable to any piece of C# code. However, we will still glance over it for the sake of understanding. If you have ever developed ASP.NET Web Forms applications, you might be aware of the Page_Error event available at the page level. Since ASP.NET MVC doesn’t follow the page life cycle events as such, obviously this event is not available to your application. Something analogous is, however, available through the OnException() method. More on that later. The [HandleError] attribute is possibly the most simple way to deal with errors in an ASP.NET MVC application. This technique doesn’t involve any special controller code other than this attribute. All you need is a custom error page in the form of a View. The last couple of techniques are global level techniques that are applicable to the whole ASP.NET MVC application and not to a particular action or controller. 

Now that you know the error handling techniques available to your application, let’s discuss each of them with a code sample.

To begin with, create a new ASP.NET MVC application. Add an ADO.NET Entity Data Model for the Customers table of Northwind database to the Models folder. The following figure shows the Customer entity:

The Customer Entity
The Customer Entity

Then add the Home controller in the Controllers folder.

Using the Try…Catch Statement

To illustrate the try..catch technique, you will deliberately cause some database related exception. Add the following code in the Index() action method of the HomeController class.

public ActionResult Index()
{
  try
  {
    NorthwindEntities db = new NorthwindEntities();
    Customer obj = new Customer();
    obj.CustomerID = "ABCDEFGHIJK";
    obj.CompanyName = "Company Name 1";
    obj.ContactName = "Contact Name 1";
    obj.Country = "USA";
    db.Customers.Add(obj);
    db.SaveChanges();
  }
  catch(Exception ex)
  {
    return View("Error");
  }
  return View();
}

The above piece of code attempts to insert a Customer whose CustomerID is more than five characters long – a limit defined for the column at the database level. Obviously, at SaveChanges() an exception is thrown. The exception is handled by the catch block. The above code doesn’t handle different exceptions using different catch blocks (which you are likely to do in a real world application), rather it just handles all the possible exceptions using the generic catch block. The catch block simply returns the Error view to the browser. The Error view is intended to display a generic friendly error message to the end user. To add the Error view, create a subfolder named Shared under the Views folder and then add a View (Error.cshtml) inside the Shared folder. This way you can use the same error view for all the controllers of the application. Of course, you could have also placed it in individual view folders if you wanted. The Error view in this case contains the following markup:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <h3>Unexpected error! Please contact the Administrator.</h3>
</body>
</html>

If you run the application you will see the Error view rendered in the browser like this:

 Error View
Error View

Overriding OnException Method

The try…catch technique discussed in the previous section allows you to trap errors at code level. However, this also means that you should identify all the places in your code that can potentially throw an exception. This may not be always possible and you may want to trap errors at the whole controller level. That’s what the OnException() method allows you to do. In this technique you override the OnException() method of the Controller base class and then write the exception handling code. This method is called whenever there is an unhandled error in the controller. The following code snippet shows how OnException() can be overridden in the HomeController class.

protected override void OnException(ExceptionContext filterContext)
{
  if (!filterContext.ExceptionHandled)
  {
    string controller = filterContext.RouteData.Values["controller"].ToString();
    string action = filterContext.RouteData.Values["action"].ToString();
    Exception ex = filterContext.Exception;
    //do something with these details here
    RedirectToAction("Error", "Home");
  }
}

The OnException() method receives the filterContext parameter that gives more information about the exception. Inside, you check the ExceptionHandled property to see whether the exception has been handled already by some other part of the controller or not. If this property returns false you go ahead and grab the controller and action name that caused the exception. Notice how RouteData.Values is used to retrieve the controller name and the action name. To get the actual Exception that was thrown you use the Exception property. Although not shown in the above code, you can use these pieces of information for logging or deciding a further course of action. In this example you simply redirect the control to the Error action method so that the Error view can be sent to the browser. The Error action method looks like this:

public ActionResult Error()
{
    return View();
}

Using HandleError Attribute

The [HandleError] attribute is possibly the simplest error handling technique. It requires that you decorate either the action methods or the controller with the [HandleError] attribute and create an Error view. If you place [HandleError] on top of action methods then any unhandled exceptions raised from that action cause the Error view to be sent to the browser. By default [HandleError] assumes that you have a view named Error either in the specific Views > <controller_name> folder or inside the Shared folder. You can also customize this view name using one of the properties of the [HandleError].

The following code shows how [HandleError] can be used with action methods as well as controllers:

[HandleError]
public ActionResult Index()
{
  ...
  return View();
}
[HandleError]
public class HomeController : Controller
{
  ...
}

If you add [HandleError] to the whole controller, unhandled exceptions arising in any of its action methods are handled by it and the Error view is displayed. Obviously, if you place [HandleError] at the controller level you don’t need to place it on top of each and every action method.

One tricky thing to remember is that [HandleError] requires custom errors enabled in the web.config. So, ensure that you have the following markup inside web.config:

<customErrors mode="On"></customErrors>

Before you run the application make sure to comment out the try…catch block as well as the OnException() method you wrote earlier and then test the working of the [HandleError] attribute.

The ErrorHandlerAttribute class has ExceptionType and View properties that can be used to customize the behavior of [HandleError]. The ExceptionType property can be used to specify a specific exception type that you wish to handle rather than generic exceptions. The View property can be used to specify a view acting as an error view.

Setting HandleError Attribute as a Global Filter

In the previous example you used the [HandleError] attribute at the action or controller level. You can register the same attribute class (HandleErrorAttribute) as a global error handling filter. To do so, open Global.asax and add this code in the Application_Start event handler:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    GlobalFilters.Filters.Add(new HandleErrorAttribute());
}

Here, you add HandleErrorAttribute to the GlobalFilters.Filters collection so as to set it as a global error handler. Any unhandled exception that takes place within the boundary of the MVC application will now be handled by this global error handler.

To test this global handler, comment out the [HandleError] attribute from the action or the controller and then run the application.

Handling Application_Error Event

The last exception handling technique discussed here is the Application_Error event. If you ever worked with ASP.NET Web Forms chances are you already know about this event. The Application_Error event is raised whenever  there is any unhandled exception in the application. That means an exception is not handled by any of the other techniques discussed earlier, it eventually gets bubbled up to the Application_Error event. Inside this event handler you can do tasks such as error logging and take some alternate path of execution. The following code shows how Application_Error can be added to Global.asax:

protected void Application_Error()
{
    Server.ClearError();
    Response.Redirect("/home/error");
}

The Application_Error event handler calls Server.ClearError() so as to convey to ASP.NET that the exception has been handled and that there is no longer an exception in the application. This way if you have set a custom error page in the web.config, it won’t be displayed. Then the code redirects the user to /home/error so that the Error view is displayed in the browser.

Summary

Error handling is an important consideration in any web application. ASP.NET MVC offers several error handling techniques in addition to try…catch that you can use. They include – overriding OnException() method, [HandleError] attribute, HandleErrorAttribute as a global filter and Application_Error event. Which of these techniques to use depends on the granularity of exception handling you need in an application.

Сайтостроение |

создано:
6/3/2012
|
опубликовано:
6/3/2012
|
обновлено:
11/16/2022
|
просмотров:
21282

| всего комментариев:
17

ASP.NET MVC Framework умеет многое, и более того может прекрасно расширяться и дополняться. В этой статье поговорим об обработке ошибок. Будут показаны несколько способов.

Содержание

ASP.NET MVC: История одного проекта «Готовимся к старту» (часть 1)
ASP.NET MVC: История одного проекта «Всё ради данных» (часть 2)
ASP.NET MVC: История одного проекта «Шаблоны и внешний вид» (часть 3)
ASP.NET MVC: История одного проекта «Еще немного классов» (часть 4)
ASP.NET MVC: История одного проекта «UI — всё для пользователя» (часть 5)
ASP.NET MVC: История одного проекта «UI — Добавление экспоната» (часть 6)
ASP.NET MVC: История одного проекта «UI — Редактирование экспоната» (часть 7)
ASP.NET MVC: История одного проекта «Обработка ошибок» (часть 8)
ASP.NET MVC: История одного проекта «Фильтрация» (часть 9)
ASP.NET MVC: История одного проекта «Поиск» (часть 10)
ASP.NET MVC: История одного проекта «Облако тегов» (часть 11)
ASP.NET MVC: История одного проекта «Главная страница» (часть 12)

Палки в колеса

ASP.NET MVC Framework умеет многое. Если вам что-то не нравится, или вы просто хотите реализовать что-либо по-другому, MVC Framework не станет «вставлять палки в колеса», а наоборот предоставит дружелюбный интерфейс. Сегодня поговорим про обработку ошибок. Будут описаны четыре с половиной способа реализации, от простого до продвинутого. Выбирать вам предстоит вам, основываясь на конкретном проекте, или на личных предпочтениях.

Вариант первый «Как два пальца об асфальт»

Самый наверное простой способ использовать HandleErrorAttribute, который любезно предоставили разработчики фреймворка. Но для начала я хочу показать, какая разница и для его нужна обработка ошибок. Никто не застрахован от ошибок, и даже программисты. :) Для этого давайте подготовим проект к выдачи ошибки при открытие главной страницы. Специально вызовим ошибку:

public ActionResult Index() {
  throw new ArgumentException("Специальная ошибка для теста");
  //return View();
}

Запустим сайт и увидим ошибку («некрасивая» форма отображения):

Не очень приятная картина, не правда ли? Но всё меняется когда приходят мы поставим вышеуказанный атрибут (можно над методом, а можно охватить все методы поставив его на контроллером). Атрибут имеет параметр ViewName, который, как вы понимаете принимает название представления для перенаправления при возникновении ошибки. Если вы поставите атрибут над контролером, то перенаправление будет всегда на одну страницу. А если над методом, то у каждого из них может быть своя страница с ошибкой. Я поставил над контролером, а значит по умолчанию перенаправление будет на представление Error:

[HandleError() ]
public class SiteController : Controller {
  /// много букв
}

Теперь запустим… Опаньки! не сработал! Ах, чёрт побери, совсем забыл включить обработку ошибок в файле конфигурации (web.config). Итак, чтобы заработал атрибут, надо включить в секцию System.Web строку CustomErrors:

<system.web>
  <!-- много букв-->

  <customErrors mode="On" />

  <!-- много букв-->
</system.web>

Запускаем еще раз! Ура! Вот что я увидел:

Пожалуй немного поясню. По умолчанию в проекте MVCApplication в папке Views/Shared создается представление Error.cshtml. Это как раз то представление (View), куда делает перенаправление при ошибке атрибут HandleError. Я это представление немного изменил:

@model System.Web.Mvc.HandleErrorInfo
@{
  ViewBag.Title = "Error";
}
<h2>
  Ошибка
</h2>
@Html.ShowAlert(@"В результате выполнения запроса
возникла непредвиденная ситуация. Информация о
случившемся уже направлена администраторам системы.
Проблема будет устранена в кратчайшие сроки.
Приносим вам свои извинения за возможно доставленные неудобства.")
<p>
  С уважением, Администрация</p>

Именно это вы и увидели на предыдущей картинке. Уже не плохо, но дальше — лучше.

Вариант второй «Немного летмотивов»

Поехали дальше… Я убрал атрибут и настройку в файле конфигурации web.config для чистоты эксперимента. Контролеры в MVC — это лейтмотив фреймворка. По умолчанию контролеры, которые вы создаете или будете создавать в своем проекте наследуются от базого абстрактного класса Controller:

public abstract class Controller :
  ControllerBase, IActionFilter,
  IAuthorizationFilter, IDisposable,
  IExceptionFilter, IResultFilter {
///...много букв
}

Нам интересен тот факт, что у этого базового контролера есть виртуальный метод OnException:

//
// Summary:
//     Called when an unhandled exception occurs in the action.
//
// Parameters:
//   filterContext:
//     Information about the current request and action.
protected virtual void OnException(ExceptionContext filterContext);

Из описания понятно, что предназначен отслеживать исключения в контролере. А раз он виртуальный, то его можно переопределить в своём контролере, например, следующим образом:

protected override void OnException(ExceptionContext filterContext) {
  var controllerName = filterContext.RouteData.Values["controller"] as String;
  var actionName = filterContext.RouteData.Values["action"] as String;
  var model = new HandleErrorInfo(
    filterContext.Exception,
    controllerName,
    actionName);
  var result = new ViewResult {
    <strong>ViewName = "error",</strong>
    MasterName = string.Empty,
    ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
    TempData = filterContext.Controller.TempData
  };
  filterContext.Result = result;

  // сконфигурируем отправляемый ответ
  filterContext.ExceptionHandled = true;
  filterContext.HttpContext.Response.Clear();
  filterContext.HttpContext.Response.StatusCode = 500;
  filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}

Результатом поделанных махинаций действий будет представление, обратите внимание на жирную строку… Правильно! Всё то же представление (Error.cshtml) об ошибке (назовем его » красивое»). Уже совсем другой компот, потому что появилась какая-то «управляемость» процессом.

Вариант третий «Место встречи изменить нельзя»

Чтобы не писать в каждом контролере один и тот же код на обработку исключений можно сделать немного интереснее. В файле Global.asax в методе Application_Error написать код один раз, например так:

private void Application_Error(Object sender, EventArgs e) {
  var exception = Server.GetLastError();
  if (exception == null)
    return;

  //  ваша реализация обработки ошибки
  // вплоть до отправки ее на электронную почту
  // администратора системы

  // очищаем ошибку
  Server.ClearError();

  // перенаправляем пользователя на другую страницу
  // созданную специльно для этого.
  <strong>Response.Redirect("site/feedback");</strong>
}

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

Примечание. Если реализуете третий вариант и второй, то приоритет на срабатывание первым будет иметь OnException и только потом уже Application_Error.

Вариант четверный «Любимый»

Я всегда использую этот вариант. Он немного посложнее, но целиком оправдывает свою сложность. Итак, первым делом я создаю новый контролер ErrorController. Он небольшой я приведу его весь:

public class ErrorController : Controller
{
  public ActionResult General(Exception exception) {
    return View("Exception", exception);
  }

  public ActionResult Http404() {
    return View("404");
  }

  public ActionResult Http403() {
    return View("403");
  }

  public ActionResult ExhibitNotFound() {
    return View();
  }
}

Также создаю все указанные представления (View) для методов. ExhibitNotFound.cshtml, 404.cshtml и 403.cshtml содержат просто текст с информацией, что «экспонат не найден»,»страница не найдена» или «доступ ограничен» соответственно, а General ключевое представление, поэтому покажу его полностью:

@model Exception
@{
  ViewBag.Title = "Exception";
  Layout = "~/Views/Shared/_LayoutExtended.cshtml";
}
<h2>
  Ошибка сайта</h2>
<p style="font-size: 1.2em; color: Red; font-weight: bold;">
  Сообщение об ошибке уже отправлено разработчикам. Надеемся на ваше понимание.</p>
<p style="font-weight: bold;">@Model.Message</p>
<p>
  Вы можете:</p>
<ul>
  <li>Перейти на главную @Html.ActionLink("страницу", "index", "site"). </li>
  <li>Сообщить о том, что Вы искали или при каких условиях появилась этот ошибка. Напишите
    разработчикам @Html.ActionLink("сообщение", "feedback", "site")</li></ul>
<fieldset style="font-size: .85em;">
  <legend>Информация для разработчиков</legend>
  @Html.Raw(@Model.StackTrace.ToString())
</fieldset>

После этого я в Global.asax пишу метод Application_Error примерно так:

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
    case 403:
      routeData.Values["action"] = "Http403";
      break;
    case 404:
      routeData.Values["action"] = "Http404";
      break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif

}

Если запустить мой проект сейчас, то я увижу такою картинку:

Мне кажется, это более интересная реализация обработки ошибок. Причем стек можно отображать только при условии, если пользователь имеет права администратора. А самое главное, что теперь можно немного усовершенствовать полученный результат и записывать ошибки в базу данных, создать, своего рода, журнал изменений (Logs), в который можно писать не только ошибки (Errors), но и предупреждения (Warnings), и просто информацию (Information) о действиях пользователя или статусов системы.

Вариант четвертный с половиной или новый класс (Log)

Создаю просто класс, который будет использоваться при работе с логами:

public class Log {
  public Log() { }
  /// <summary>
  /// Создает экземпляр записи в журнале документов
  /// </summary>
  /// <param name="message">текст сообщения</param>
  public Log(string message) {
    this.Message = message;
    this.CreatedAt = DateTime.Now;
  }

  [Key]
  [Display(Name = "Идентификатор")]
  public int Id { get; set; }

  /// <summary>
  /// Текст сообщения
  /// </summary>
  [Required]
  [Display(Name = "Текст сообщения о событии")]
  [StringLength(500)]
  public string Message { get; private set; }

  /// <summary>
  /// Дата выполнения операции
  /// </summary>
  [Required]
  [Display(Name = "Выполнено")]
  public DateTime CreatedAt { get; private set; }

  /// <summary>
  /// Имя пользователя
  /// </summary>
  [Required]
  [ Display(Name = "Автор")]
  [ StringLength(50)]
  public string UserName { get; set; }
}

А теперь при помощи MvcScaffolding создаю репозиторий:

PM> Scaffold Repository Log -DbContextType MuseumContext
Added 'Logs' to database context 'Calabonga.Mvc.Humor.Engine.MuseumContext'
Added repository 'ModelsLogRepository.cs'
PM>

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

public interface ILogRepository {
  IQueryable<Log> All { get; }
  Log Find(int id);
  void Log(string message, bool sendNotify);
  void Log(string messageformat, string param0);
  void Log(string messageformat, string param0, bool sendNotify);
  void Log(string messageformat, string param0, string param1);
  void Log(string messageformat, string param0, string param1, bool sendNotify);
  void Delete(int id);
  void Clear();
}

Вы можете придумать свою реализацию этого интерфейса, да и, собственно говоря, и сам интерефейс тоже можете себе придумать сами. А мне остается только добавить запись в журнал в методе Application_Error изпользуя новый LogRepository (жирным шрифтом):

protected void Application_Error() {
#if !DEBUG
  var exception = Server.GetLastError();
  var httpException = exception as HttpException;
  Response.Clear();
  Server.ClearError();
  var routeData = new RouteData();
  routeData.Values["controller"] = "Error";
  routeData.Values["action"] = "General";
  routeData.Values["exception"] = exception;
  Response.StatusCode = 500;
  if (httpException != null) {
    Response.StatusCode = httpException.GetHttpCode();
    switch (Response.StatusCode) {
      case 403:
        routeData.Values["action"] = "Http403";
        break;
      case 404:
        routeData.Values["action"] = "Http404";
        break;
    }
  }
  Response.TrySkipIisCustomErrors = true;
  IController errorsController = new ErrorController();
  <strong>LogRepository logger = new LogRepository();
  logger.Log(string.Concat("ОШИБКА: ", exception.Message));
</strong>  HttpContextWrapper wrapper = new HttpContextWrapper(Context);
  var rc = new RequestContext(wrapper, routeData);
  errorsController.Execute(rc);
#endif
}

У вас наверное возникнет вопрос, почему именно так? Потому что если возникает ошибка или «выстреливает» какое-нибудь исключение, то получить ILogRepository через Dependency Injection уже не получится, поэтому я создаю экземпляр класса и использую его напрямую. Но в контролерах я буду получать именно ILogRepository через Dependency Injection в конструкторе, как и положено.

И напоследок

Я обычно в методах, которые должны каким-то образом реагировать на ошибки и исключения писал TODO, например как этом методе:

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  // TODO: пока при отсутвии обработки ошибок
  // буду перекидывать на страницу списка
  return RedirectToAction("index");
}

После того как заработала система обработки ошибок, я могу все методы поправить включив обработку в методы. Например для предыдущего метода сделаю так:

public ActionResult Show(int id) {
  Exhibit exh = exhibitRepository.Find(id);
  if (exh != null) {
    return View(new ShowExhibitViewModel(exh));
  }
  return RedirectToAction("http404", "error");
}

Таким образом, пользователь получит правильное уведомление о том что такой записи в базе нет.

Заключение

Я сделал на странице администрирования сайта ссылку на просмотр журнала изменений, которая будет отобржать логи постранично. Надеюсь, это у вас не вызовет затруднений, поэтому я не буду описывать эти действия, а покажу как это выглядит у меня:

И, как я обещал в комментариях к предыдущей статье, вот [ссылка удалена] на скачивание проекта в текущей сборке. Спасибо за внимание — пишите комментарии.

Понравилась статья? Поделить с друзьями:
  • Mutex error total war warhammer 2
  • Mutex error total war three kingdoms
  • Mutex error total war rome 2 как исправить
  • Mutex error total war napoleon
  • Mutex error total war empire