Showing posts with label Error Handling. Show all posts
Showing posts with label Error Handling. Show all posts

Wednesday, October 13, 2010

Azure Tables Error Messages

Azure Table Storage (ATS) is a REST-basead data access mechanism, and REST is based on HTTP. So, makes sense that ATS data access errors are returned as HTTP errors. But some translation is needed to understand those errors.

1. DataServiceRequestException and DataServiceClientException exceptions

When a data access error occurs in ATS, the exception returned usually is from DataServiceRequestException class, being the error message “An error ocurred while processing this request”. This exception encapsulates a more specific DataServiceClientException exception, that contains a XML document on the Message property describing the error:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
    <error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <code>EntityAlreadyExists</code>
    <message xml:lang="pt-BR">The specified entity already exists.</message>
</error>

The value on the <code> element needs to be interpreted considering what the application was doing when the error ocurred. For example, the code InvalidInput is generated if you are trying to save an entity that has a property of an unsupported data type; OR if you are trying to update an entity that has no concurrency control information. Almost the same, right? ;-)

2. Error codes an possible causes

Following are the HTTP error code that we’ve bumped into, and what the application was trying do do when the error ocurred.

Error Code
(value on <code> element)
Possible Causes
ConditionNotMet - Concurrency error. The app read some entity; when trying to modify or delete the entity, it had already been modified by a command from another data context.
EntityAlreadyExists - Primary key violation. The app is trying to insert an entity, but there’s an entity on the table with the same values for PartitionKey and RowKey properties on the entity being inserted.
InternalError - Use of unsupported Linq operators on ATS. (We found this one trying to recover rows that had a string property starting with some value. There’s no string support for like, >, <, and neither string.StartsWith() – the one that generated the error. You have to use string.CompareTo() – :-P)
InvalidInput - Storing an entity that has properties of types not supported by ATS.
- Updating an entity that was attached to the context (TableServiceContext.AttachTo()), but has null on it’s
ETag property.
- Linq queries on a table with no structure defined. We received this error when trying to query a table that had just been created; if you insert some entities, even if you erase all the entities on table later, it seems that the table holds the entity structure and you can use it on Linq queries.
- Linq queries using
anonymous types. The workaround was to make the query return the “whole” entity, and build a second query using anonmymous types on the data returned by ATS.
- Batch save operation (SaveChanges(SaveOptions.Batch)) with 2 or more entities having the same Partion and Row Keys.
ResourceNotFound - Linq query that returns no entitites using equality conditions on PartitionKey and RowKey (where entity.PartitionKey == “…” && entity.RowKey == “…”). (Queries with conditions using other attributes return a 0-sized list)
- Concurrency error. The app read some entity; when trying to modify or delete the entity, it had already been deleted by a command from another data context.
- Linq query on a table that does not exist on ATS.
TableAlreadyExists Found on calls to CloudTableClient.CreateTablesFromModel(). It seems that concurrent calls to this method may end up generating this exception. We placed a call to CreateTablesFromModel() on the data access context static constructor. When we raised the number of role instances to 2, one of them started ok, and the other one generated the exception.
TableBeingDeleted Same as TableAlreadyExists above.
OutOfRangeInput Generated when saving an entity with an uninitialized DateTime property. The minimum allowed value for a DateTime property on ATS is Jan 01, 1601, and an uninitialized DateTime in C# holds Jan 01, year 1. We didn’t detected it on tests against DevStorage because it changes dates before Jan 01, 1753 to Jan 01, 1753, but ATS does not.

3. How to extract the HTTP error code from DataServiceClientException objects

The code is based on an enum that has members named exactly as the error codes returned by ATS:


public enum ATSError
{
    EntityAlreadyExists,
    ConditionNotMet,
    InvalidInput,
    ResourceNotFound, 
    Unknown  // This one is the "generic" error code 
}

The following method extracts the error code from the XML returned by ATS, and converts it on the correspondent ATSError enum value:

/// <summary>
/// Translates an ATS exception to one of the ATSErrors values.
/// </summary>
/// <param name="error">Exception returned by ATS.</param>
/// <returns></returns>
public static ATSError ExtractATSError(Exception e)
{
    // Searches for a DataServiceClientException exception
    while (!(e is System.Data.Services.Client.DataServiceClientException) && (e != null)) 
        e = e.InnerException;
    if (e == null) return ATSError.Unknown;

    // At this point "e" contains a DataServiceClientException object,  
    // and the Message property contains a valid XML document describing the error
    ATSError code;
    try
    {
        XmlDocument xml = new XmlDocument();
        xml.LoadXml(e.Message);
        // The XML document uses namespace http://schemas.microsoft.com/ado/2007/08/dataservices/metadata, so 
        // we have to use a XmlNamespaceManager to represent this namespace
        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xml.NameTable);
        namespaceManager.AddNamespace("n", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
        // Retrieves the "<code>" element 
        string s = xml.SelectSingleNode("/n:error/n:code", namespaceManager).InnerText;
        // Converts the element value to the corresponding ATSError value
        code = (ATSError)Enum.Parse(typeof(ATSError), s);
    }
    catch
    { 
        code = ATSError.Unknown;
    }
    return code;
}

And that’s it. If you bump into another error code, just add the corresponding member on the ATSError enum, so that the ExtractATSError() method will recognize it.

Sunday, October 11, 2009

Global error handling in ASP.NET and Windows Forms

You should not let exceptions unhandled in *any* part of your application. But that doesn't mean you have to spray your entire code with a swarm of try/catch blocks. Both Windows Forms and ASP.NET applications offer the possibility to create a global error handler, that will be activated whenever an unhandled exception (i.e., an exception that has not happened inside a try/catch block) occurs.

Windows Forms Global Exception Handler

As always, things are easier in Windows Forms than in ASP.NET. Windows Forms provides the developer with the Application object, that among other things has the ThreadException event. Basically, all you have to do is to add an even handler to the ThreadException event, and your global event handler is set.

Create a new VB.NET Windows Forms project, add a button to the Form1 form, and replace its code with the following:


Imports System.Diagnostics

Public Class Form1

Const MYAPP_SOURCE_NAME As String = "My Application"

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
' Creates the event source on Windows Application Log
' This has to be done only once, usually during app setup
If Not EventLog.SourceExists(MYAPP_SOURCE_NAME) Then
EventLog.CreateEventSource(MYAPP_SOURCE_NAME, "Application")
End If
' Designates the global error handler
AddHandler Application.ThreadException, AddressOf Me.TheGlobalErrorHandler
End Sub

Sub TheGlobalErrorHandler(ByVal sender As Object, ByVal e As Threading.ThreadExceptionEventArgs)
' This is how we get the exception data
Dim unhandledException As Exception = e.Exception
' Writes technical error information to the Application Log
EventLog.WriteEntry(MYAPP_SOURCE_NAME, _
"Unhandled error: " + vbNewLine + unhandledException.ToString(), _
EventLogEntryType.Error)
' Some nice message to the user
MsgBox("An unexpected error has occured. Technical error information " & _
"has been writen to the Windows Application Log.", _
MsgBoxStyle.Critical)
End Sub

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
' Simulates an unhandled exception
Throw New Exception("Oh oh error accessing the database")
End Sub
End Class



Click on the button to simulate an unhadled exception. A message is displayed and if you look at the Windows Application Log on EventViewer (Start > Run > eventvwr.msc), there it is the exception.

ASP.NET Global Exception Handler

ASP.NET provides you with the Application_Error global application event, that is fired whenever an unhandled exception occurs. This event handler belongs to the global application class, as Visual Studio calls it. The global application class code is located on the Global.asax file. ASP.NET applications, by default, don't have a Global.asax file. You can add one by going to the Project menu and selecting Add New Item > Global Application Class.

On the Application_Error event handler code, you have to use two special methods:
  • Server.GetLastError() gives access to the unhandled exception.
  • Server.ClearError() prevents ASP.NET from showing the default error page.
Create a new ASP.NET Web Application, add a Global.asax file to it, and replace the code in Global.asax.vb with the following:

Imports System.Web.SessionState
Imports System.Diagnostics

Public Class Global_asax
Inherits System.Web.HttpApplication

Const MYAPP_SOURCE_NAME As String = "My Application"

Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
' Creates the event source on Windows Application Log
' This has to be done only once, usually during app setup
If Not EventLog.SourceExists(MYAPP_SOURCE_NAME) Then
EventLog.CreateEventSource(MYAPP_SOURCE_NAME, "Application")
End If
End Sub

Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
' Stores the unhandled exception
Dim unhandledException As Exception = Server.GetLastError()
' Says to the Framework "Don't show default error page - we will handle it"
Server.ClearError()
' Writes techinical error information to the Application Log
EventLog.WriteEntry(MYAPP_SOURCE_NAME, _
"Unhandled error: " + vbNewLine + unhandledException.ToString(), _
EventLogEntryType.Error)
' Redirects user to some page with nice error message
Response.Redirect("/error.aspx")
End Sub

End Class


Create a page named "error.aspx"; that's the page users will be redirected to after an unhandled exception.

Finnaly, add a Button to Default.aspx, and replace its code with the following:

Partial Public Class _Default
Inherits System.Web.UI.Page

Protected Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
' Simulates an unhandled exception
Throw New Exception("Oh oh that database access error again")
End Sub
End Class


Click on the button and you will be redirected to error.aspx, and an event will be logged on the Application Log with the exception data.



UPDATE: the Application_Start code will fail on Windows Vista & Windows 7, because of tightened security. The code tries to create an event source in order to log events to it. The methods EventLog.SourceExists() and EventLog.Create() both try to scan the Windows Logs to check if the specified source exists, but access to the Security Log by ASP.NET apps is not allowed by default. There are several ways to "do it right", but a quick and dirty solution is to create the following registry key: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\eventlog\Application\YOUR_APP_NAME, changing YOUR_APP_NAME to the event log source you want to create. A possible way to "do it right" is to create a command line app containing the Application_Start code, and set it to run from your site's setup package.