ADO.NET Data Services : Efficient Error Handling across Application Tiers
While developing an application that spans multiple tiers , it is important that you be able to flow error information through the tiers without losing
any context or details in the tiers. With ADO.NET Data Services , we have an error contract which guarantees that all errors thrown from the Data Service ,
wrapped inside a DataServiceException , will be represented in a standard way on the wire when we send the error down to the client.
For example , consider the Query Interceptor shown below .
[QueryInterceptor("Customers")]
public Expression<Func<Customers, bool>> OnQueryCustomers()
{
string loggedInUser = HttpContext.Current.User.Identity.Name;
if (UserHasAccessToSet("Customers", loggedInUser))
{
//Filter Expression goes here
return entity => true;
}
else //User does not have access to '/Customers' , throw AccessViolationException
{
throw new DataServiceException(403,
"Forbidden",
String.Format("User '{0}' cannot request data from '{1}' table", loggedInUser, "Customers"),
"en-US");
}
}
When the Astoria server runtime throws the above exception , the 403 value specified above gets turned into the response status code of the request
which caused this exception.
and the error message is serialized out to be in this format :
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<error xmlns="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<code>Forbidden</code>
<message xml:lang="en-US">User 'NoPermissions' cannot request data from 'Customers' table</message>
</error>
At this point , the service has not lost any information that the service author intended to convey to a service consumer.
Now , lets consider the client library and how it handles this case.
Lets consider a query for the “Customers” set to which the currently logged-in user doesn't have permissions.
foreach (Customers customerEntity in northwindContext.CreateQuery<Customers>("Customers") ) {
//Do something with the customerEntity here
}
In this case, running the above code would result in a DataServiceQueryException being thrown.
System.Data.Services.Client.DataServiceQueryException:
An error occurred while processing this request. --->
System.Data.Services.Client.DataServiceClientException:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="https://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<code>Forbidden</code>
<message xml:lang="en-US">User 'NoPermissions' cannot request data from 'Customers' table</message>
</error>
Note that the DataServiceException thrown by the server is now set to the Message property of the InnerException of the DataServiceQueryException.
We do not de-serialize the DataServiceException into an exception type on the client side.
By default , since the exception is now a string on the client side , you don’t have direct access to the information contained inside the Exception.
The information is all still there , just not easily accessible anymore. Since the error contract is documented and follows a standard pattern ,
we can easily write a visitor that de-serializes an exception object from the “Message” property of the Inner Exception.
One such de-serializer for Error contracts is shown at the bottom of this post
It contains two methods :
TryParse : which takes in an exception caused during Querying or updating via the client library and
returns a DataServiceException that was thrown by the Server
Throw : which takes in an exception caused during Querying or updating via the client library and
re-throws the DataServiceException that was thrown by the Server
Comments
Anonymous
December 15, 2009
I had a problem with the namespaces, the innererror is in the xmlns namespace, so I changed your code as follow: [Phani : Aquilax , thanks for catching that, I'll update the Gist in Github]Anonymous
May 26, 2011
Thanks for the util class! To get at the internalexception which may even more information (if not the best info): Can add a member... public Exception InternalException; ...an additional constructor... public DataServiceException(string Message, string StackTrace, Exception innerException, Exception InternalException) : base(Message, InternalException) { this.InternalException = InternalException; stackTrace = StackTrace; } ...and modify the switch block... case "error": case "innererror": case "internalexception": // crokusek added DataServiceException innerEx = null; Exception internalEx = null; if (errorElement.Element(xnInnerError) != null) { innerEx = ParseException(errorElement.Element(xnInnerError), throwOnFailure); var e = errorElement.Element(xnInnerError).Element(xnInternalException); if (e != null) internalEx = ParseException(errorElement.Element(xnInnerError).Element(xnInternalException), throwOnFailure); } string message = errorElement.Element(xnMessage) != null ? errorElement.Element(xnMessage).Value.ToString() : String.Empty; string stackTrace = errorElement.Element(xnStackTrace) != null ? errorElement.Element(xnStackTrace).Value.ToString() : String.Empty; return new DataServiceException( message, stackTrace, innerEx, internalEx ); still kind of a pain to decode though: Exception dsex; DataServiceExceptionUtil.TryParse(ex, out dsex_); if (dsex_ != null && dsex_ is DataServiceException) { DataServiceException dsex = (DataServiceException) dsex_; // The best info is here extracted from Xml Exception iex = dsex.InternalException; if (iex != null) _sb.AppendLine('n' + iex.Message + 'n' + iex.StackTrace + "nn"); if (dsex.InnerException != null) _sb.AppendLine('n' + dsex.InnerException.Message + 'n' + dsex.InnerException.StackTrace + "nn"); _sb.AppendLine('n' + dsex.Message + 'n' + dsex.StackTrace + "nn");