Introduction
This article describes a way to add custom exception details as part of ELMAH logging.
Background
I am managing the development of an ASP.NET MVC website for a client, and have included ELMAH logging to track any unmanaged exceptions that occur. For more information on ELMAH, see https://elmah.github.io/.
ELMAH works great, but only records the information based on System.Exception
. It will not record details of fields in custom exceptions that do not exist in System.Exception
. One particular example of this is System.Data.Entity.Validation.DbEntityValidationException
. If this is thrown, you get the following message in ELMAH:
- Validation failed for one or more entities. See '
EntityValidationErrors
' property for more details.
Unfortunately, since the 'EntityValidationErrors
' is specific to DbEntityValidationException
, ELMAH has no knowledge of it and doesn't record those details.
I have found articles where you can generate a custom exception with the details you need, but only at the specific location where the error occurs. I needed a way to capture this information automatically within ELMAH without having to modify every such location (whether it needs it or not).
In this example, I provide a way to capture this information in the ELMAH log so that you can determine the specific validation errors that occurred. This is managed globally within the website so that you do not need to add custom code to every location where such an error could occur.
You can also modify this code so that you can capture any custom fields for any Exceptions that you want to track.
FYI, the code is written to the following:
- .NET Framework 4.5.2
- ASP.NET MVC 5.2.3
- Elmah 1.2.2
This code can probably be adopted for other versions as appropriate.
Using the Code
Before starting, it is assumed that you have:
- An ASP.NET MVC website
- Installed ELMAH as part of your website, and tested that it is working correctly.
There are several articles that discuss how to install ELMAH, so I won't repeat this here.
You will want to find the ELMAH custom filter that has been added to your project. This should be located in the App_Start folder. In my installation, the file is called ElmahErrorAttribute.cs, and contains the class ElmahHTTPErrorAttribute
, which is derived from System.Web.Http.Filters.ExceptionFilterAttribute
.
Locate the OnException()
method, which should look like this:
public override void OnException
(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception != null)
{
Elmah.ErrorSignal.FromCurrentContext().Raise(actionExecutedContext.Exception);
}
base.OnException(actionExecutedContext);
}
In a nutshell, any unhandled exceptions are routed here. The exception is Raised in ELMAH to capture the details in the ELMAH log, then the code proceeds with its default handling.
What we want to do is determine if the Base Exception for the passed in Exception matches one of the types we want to provide additional details for.
- If the exception does not match one of specific Exception types, then process as before.
- If the exception does match one of the specific Exception types we want to track, then we want to create a new Exception, with the extended information captured in the Exception Message, and the
InnerException
property set to the original exception that was passed in.
In the example that follows, I do the following:
- Create a new custom
Exception
called ElmahExtendedException
, derived from System.Exception
. You could just use System.Exception
, but using a custom Exception
makes it easier to identify in the ELMAH log file.
public class ElmahExtendedException
: Exception
{
public ElmahExtendedException(string message, Exception e)
: base(message, e) { }
}
- Create a
private
method called GetExtendedException()
in the same class as the ELMAH filter (ElmahHTTPErrorAttribute
). This takes the original exception and determines whether it is one of the exception types we are interested in. If it isn't, we simply return it as is. Otherwise, we process the exception to create the new ElmahExtendedException
and return this.
In this case, we are processing DbEntityValidationException
instances. Here, we parse the list of ValidationError
s that are found as a set of string
s in a StringBuilder
and append these to the original Message
of the exception. We append the final message and the original Exception
and use these to create the new ElmahExtendedException
and return this.
private Exception GetExtendedException(Exception ex)
{
var baseException = ex.GetBaseException();
switch (baseException.GetType().ToString())
{
case "System.Data.Entity.Validation.DbEntityValidationException":
var dbException = baseException as
System.Data.Entity.Validation.DbEntityValidationException;
var sb = new System.Text.StringBuilder();
sb.AppendLine(baseException.Message);
foreach (var entity in dbException.EntityValidationErrors)
{
foreach (var error in entity.ValidationErrors)
{
sb.AppendLine(string.Format(" [{0}: {1}]",
error.PropertyName, error.ErrorMessage));
}
}
return new ElmahExtendedException(sb.ToString(), ex);
default:
return ex;
}
}
If you wish, you can add your own case for additional Exceptions that you wish to process. You will want to capture any additional details and add the StringBuilder
, formatting as necessary. In the above case, we are adding each ValidationError
on a separate line, so that it is easier to read each detail in the log later on.
You will note that we use the base Exception
of the original exception. We want to reference the original exception and use its error message. If you have more complex nested exceptions, you may need to modify this code to find the specific instance you are interested in.
- Finally, we modify the
OnException()
method as follows:
public override void OnException
(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception != null)
{
var exception = GetExtendedException(actionExecutedContext.Exception);
Elmah.ErrorSignal.FromCurrentContext().Raise(exception);
}
base.OnException(actionExecutedContext);
}
Here, we call our GetExtendedException()
method, which will either return the original exception or our new ElmahExtendedException
. We then raise this exception to ELMAH for logging.
If we look at this in the ELMAH logging, the logging details are now displayed as follows:
System.Data.Entity.Validation.DbEntityValidationExceptionValidation failed for one or more entities. See 'EntityValidationErrors' property for more details.
SampleWeb.ElmahExtendedException: Validation failed for one or more entities.
See 'EntityValidationErrors' property for more details.
[FieldContents: The field FieldContents must be a string or array type with a maximum length of '100'.]
---> System.Data.Entity.Validation.DbEntityValidationException: Validation failed for
one or more entities. See 'EntityValidationErrors' property for more details.
at System.Data.Entity.Internal.InternalContext.SaveChanges() ...
As you can see, we have the original exception Message
shown in the log header. However, in the body of the exception:
- We see the new
ElmahExtendedException
exception, identifying it as containing our extended exception details. - Directly underneath, we see the details we have added - in this case, the Validation Error showing the
Fieldname
followed by the specific error. If there had been multiple Validation Errors, they would have been listed one per line. - Underneath this, we have the name of the original
Exception
, followed by the full stack trace.
Summary
I hope that this proves useful. Again, it should be straightforward to adapt this to other types of exceptions as necessary.
If you have any feedback or suggestions for improvement, they are always welcome.
History
- 18th August, 2018 - Original posting