An SOA odyssey

Friday, April 07, 2006

Services and .NET 2.0

At long last I've started to look into porting our various frameworks to version 2.0 of the .NET Framework.

As a first step I was able to recompile the entire Enterprise Development Application Framework (EDAF) which included our customizations to the web service interface adapter and transaction handler. While I encounter numerous warnings (mostly related to changes in the System.Configuration namespace and how the assembly key file is specified in the project) there were no compilation errors. Next, I compiled our custom handlers (logging, rules, monitoring). All of that worked well so I was able to deploy EDAF to a new server hosting ASP.NET 2.0.

From there I was able to re-generate all the .NET code from our schemas and then reubild our common classes (such as the DataFactory) and base classes and saw virtually no warning and no errors. Finally, I built the Constituent Management Service that we use to handle sponsor information and deployed it to the server which once again went smoothly.

From my machine I then used our existing v1.1 Service Agent for the Constituent Service to make a call to the SelectConstituent operation. Moving forward this is the way our main clients including BizTalk orchestrations and the Ultimus BPM Studio will be consuming the services.

It worked!

From here it's just a matter of checking functionality for the various operations but it looks very promising thus far.

Exception Handling and Suppression

One of the interesting architectural issues we had to grapple with when developing our service oriented infrastructure was how to handle exceptions from our services.

Obviously one simple approach would simply have been to throw .NET exceptions from our service code in EDAF and then allow ASP.NET to translate that into a SOAP fault. However, this approach does not allow us to differentiate between business (the child you chose to sponsor is no longer available) and system (the database was down) exceptions, does not allow us to add additional information to the exception, does not allow for the return of relevant information in the main body of the response along with the exception, and does not allow overriding of certain exceptions.

To handle these issues we designed an Exception schema that looks as follows.

<xs:element name="Exceptions">
<xs:element minOccurs="1" maxOccurs="unbounded"
<xs:element minOccurs="1" maxOccurs="1"
name="DateTimestamp" type="xs:dateTime"/>
<xs:element minOccurs="0" maxOccurs="1"
name="Source" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1"
name="Code" type="xs:string" />
<xs:element minOccurs="1" maxOccurs="1"
name="Message" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1"
name="StackTrace" type="xs:string" />
<xs:attribute name="Type" type="xs:string"
use="required" />
<xs:attribute name="Level" type="xs:int"
use="required" />
<xs:any minOccurs="0" maxOccurs="unbounded"
namespace="##local" processContents="lax" />
<xs:attribute name="SchemaVersion"
use="optional" />
<xs:anyAttribute namespace="#any"
processContents="lax" />

As you can see this schema looks similar to what you'd find in a .NET exception inherited from System.Exception with the addition of Type and Level as well as the ability to add additional data with the inclusion of the any element and attribute per our schema standards.

You'll also note that this schema includes the notion of multiple exceptions through the root Exceptions element that includes one or more Exception elements.

This schemas is then added to the response envelope schemas for each service operations like so:

<xs:element minOccurs="0" maxOccurs="1"
ref="ex:Exceptions" />

In this way service actions can populate these exception arrays as they see fit, for example in response to a business violation.

ExceptionsException exception =
new csc.ExceptionsException();
exception.Source = System.Reflection.Assembly.
exception.StackTrace = Environment.StackTrace;
exception.Message = string.Format(
exception.DateTimestamp = DateTime.Now;

The typical service then uses a resource file to retrieve the actual error message based on the culture found in the SOAP header. We also have a base class method that can build one of these exceptions when given a .NET exception.

You'll notice that the base class also allows for the collection of the exceptions to be tracked using an array list so that the service can simply add the exception to the collection and continue processing.

The precense of exceptions is what then triggers whether or not the service attempts to rollback any transactions that have started and what the response code will be. This the typical code block we use near the end of our services.

if (this.Exceptions.Count > 0)
//append exceptions
response.Exceptions = this.GetExceptions();
response.ResponseCode =
//invalidate trx.

Here the exceptions that have been collected get placed into the response object to be serialized back to the caller, the response code is set to Failure (which allows clients to detemrine whether or not they should inspect the exceptions collection since we're not throwing a SOAP fault), and the base class method is called to mark the transaction is being invalid (behind the scenes this method uses an EDAF context property that is read by the transaction handler configured in the pipeline for the business action).

The second interesting aspect of how this works is the Level attribute. We've added this attribute so that we can tag our exceptions being thrown in the business actions as follows:

  • Business Critical and System = 0

  • Business Overridable = 10

  • Business Warning = 20

  • Business Informational = 30

  • The idea here is that if a consumer of a service receives a failure response and inspects the exceptions, they have the option of submitting the request again but this time setting an ExceptionSuppressionLevel configured on the envelope of the request message like so:

    <xs:attribute name="ExceptionSuppressionLevel"
    type="xs:int" use="required" />

    This attribute will default to 0 as will the level in any exceptions that are created in business actions.

    The consumer will then populate the ExceptionSuppressionLevel on the call and the service will suppress any exceptions at that level or above.

    For example, in the case where one of our UI's wishes to create a constituent and override the non-critical exceptions the call would set the ExceptionSuppressionLevel to 10 and the call might return with a Success response and avoid throwing the exception.

    The interesting aspect of this architecture is that it allows us in the future to have services that return a mix of information including informational messages. Exception Levels 0-20 will implicitly roll back the work of the business action while level 30 never does. Using an integer rather than an enumeration allows us to be flexible as other levels of exceptions arise.