Defining and Processing SOAP Headers

Using SOAP Extensions

In the preceding section, I wrote a fair amount of code to process the SOAP Payment header. A Web service might potentially expose many Web methods that require the Payment header to be processed, so it is not ideal to have every method contain code to process the payment information. The code within the method should be responsible for the business logic, not handling tasks that can be pushed to the infrastructure. In this section, I show you how to provide extended services, such as processing the Payment SOAP header, that can be applied to any Web method.

SOAP extensions provide a way of creating encapsulated reusable functionality that you can apply declaratively to your Web service. The SOAP extensions framework allows you to intercept SOAP messages exchanged between the client and the Web service. You can inspect or modify a message at various points during the processing of the message. You can apply a SOAP extension to either the server or the client.

A SOAP extension is composed of a class derived from the SoapExtension class. It contains the implementation details that are generally used to examine or modify the contents of a SOAP message. You can then define an attribute derived from SoapExtensionAttribute that associates the SOAP extension with a particular Web method or a class.

SOAP Extension Attributes

You use a SOAP extension attribute to indicate that a particular SOAP extension should be called by the ASP.NET runtime for a particular Web method. You can also use the SOAP extension attribute to collect information that will be used by the SOAP extension.

My first SOAP extension example will automatically process the Payment and Receipt headers I created in the last section. Instead of including code within the implementation of the InstantQuote Web method, I will create an attribute called ProcessPayment that can be used to decorate Web methods that require the Payment header to be processed. Later I will create the ProcessPaymentExtension class, which will contain the actual implementation. Here is the implementation of the ProcessPayment attribute:

[AttributeUsage(AttributeTargets.Method)] public class ProcessPaymentAttribute : SoapExtensionAttribute  {     int        merchantNumber = 0;     double    fee = 0;     int        priority = 9;     public ProcessPaymentAttribute(int merchantNumber, double fee)     {         this.merchantNumber = merchantNumber;         this.fee = fee;     }     public int MerchantNumber      {         get { return merchantNumber; }         set { merchantNumber = value; }     }     public double Fee      {         get { return fee; }         set { fee = value; }     }     public override Type ExtensionType      {         get { return typeof(ProcessPaymentExtension); }     }     public override int Priority      {         get { return priority; }         set { priority = value; }     } }

The ProcessPayment attribute is responsible for gathering the information needed by the SOAP extension. The SOAP extension will require the merchant account number and the fee the client should be charged to process the payment. Thus, both the merchant number and the fee must be passed as part of the ProcessPayment attribute's constructor. I also create associated MerchantNumber and Fee properties because the only mechanism for passing information from the SOAP extension attribute to the SOAP extension is by exposing the information as a public field or a public property.

Attributes that derive from SoapExtensionAttribute must override the ExtensionType property. This property returns an instance of the Type object of the SOAP extension class. The ASP.NET runtime will access this property to locate its associated SOAP extension.

All SOAP extension attributes must override the Priority property. This property specifies the priority in which the SOAP extension will be executed with respect to other SOAP extensions. I gave the Priority property a default value of 9 so that it can be optionally set by the user of the attribute.

The priority of the SOAP extension is used by ASP.NET to determine when it should be called in relation to other SOAP extensions. The higher the priority, the closer the SOAP extension is to the actual message being sent by the client and the response sent by the server. For example, a SOAP extension that compresses the body and the header of a SOAP message should have a high priority. On the other hand, the ProcessPayment SOAP extension does not need to have a high priority because it can function properly after other SOAP extensions have processed.

SOAP Extension Class

The SOAP extension class contains the implementation of the SOAP extension. In the case of the ProcessPaymentExtension class, it will process the Payment header on behalf of the Web method. A SOAP extension derives from the SoapExtension class. The ASP.NET runtime invokes methods exposed by the class at various points during the processing of the request. These methods can be overridden by the SOAP extension to provide custom implementation. Table 6-8 describes the methods that can be overridden by a custom SOAP extension.

Table 6-8  SoapExtension Class Methods

Method

Description

ChainStream

Provides a means of accessing the memory buffer containing the SOAP request or response message.

GetInitializer

Used to perform initialization that is specific to the Web service method. This method is overloaded to provide a separate initializer for a single method or for all methods exposed by a type.

Initialize

Used to receive the data that was returned from GetInitializer.

ProcessMessage

Provides a means of allowing the SOAP extension to inspect and modify the SOAP messages at each stage of processing the request and response messages.

The SOAP extension framework provides two methods of accessing the contents of the message. One way is through a stream object received by the ChainStream method that contains the raw contents of the message. The other way is through the properties and methods exposed by the instance of the SoapMessage object passed to the ProcessMessage method. For the ProcessPaymentExtension class, I will use the SoapMessage class.

The SOAP extension framework also provides a two-step initialization process through the GetInitializer and Initialize methods. The initialization process is designed to reduce the overall initialization cost associated with the extension. I discuss this in more detail later in this section.

The following diagram shows the order of the individual calls the ASP.NET runtime makes to the SOAP extension:

If multiple extensions are associated with a Web method, every extension will be called during each stage in the order of priority. For example, the GetInitializer method will be called on each SOAP extension before the ChainStream method is called. Except for the BeforeSerialize and AfterSerialize modes of the ProcessMessage method, each method will first call SOAP extensions that have a priority of 1 and then call the remaining extensions in ascending order of priority. When you invoke the ProcessMessage method during the BeforeSerialize and AfterSerialize modes, ProcessMessage will call the extensions in reverse order of priority.

Initialization

A new SOAP extension object is created each time the Web method associated with it is invoked. The SOAP extension often performs initialization that is generic across all invocations of the Web method. The SOAP extension framework provides a means of executing initialization code that should occur once.

The SOAP extension framework supports a two-phase initialization sequence. The GetInitializer method performs the initialization for a particular Web method, in this case the InstantQuote method. GetInitializer will be called only once per Web method for the life of the Web application. The Initialize method will be called each time the Web method is invoked.

To process the Payment header, I need to initialize a credit card processor object. Once it is initialized, it can be used to process any number of Payment headers. Let's assume that there is a nontrivial cost associated with initializing the object. I can initialize it once within the GetInitializer method and then use it each time the InstantQuote Web method is invoked. Here is the implementation of the GetInitializer method:

public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)  {     ProcessPaymentAttribute processPaymentAttribute = (ProcessPaymentAttribute)attribute;     // Set up connection to credit card authorization service.     creditCardProcessor = new CreditCardProcessor(processPaymentAttribute.MerchantNumber);     // Return the initialized credit card processor object and the fee.     return new object [] {creditCardProcessor, processPaymentAttribute.Fee}; }

Notice that when ASP.NET invokes the GetInitializer method, the extension's associated attribute is passed as a parameter. The attribute is used to obtain the MerchantNumber as well as the Fee properties. The method initializes the credit card processor.

This same credit card processor will then be used each time the InstantQuote Web method is invoked. However, recall that a new instance of the ProcessPaymentExtension object is created each time the Web method is invoked. So how can I use the same instance of the credit card processor object across all invocations of the Web method? The following diagram illustrates the problem.

You might have noticed that GetInitializer has a return parameter of type object. The implementation of the GetInitializer method for the ProcessPaymentExtension object returns a two-element array containing the initialized credit card processor object as well as the fee that should be charged to the customer. The ASP.NET runtime retains a reference to this array and passes it to the Initialize method each time a new object is created as a result of invoking the InstantQuote Web method.

One of the responsibilities of the Initialize method is to obtain the data returned to the ASP.NET runtime by the GetInitializer method. The Initialize method can also be used to perform any additional initialization that needs to occur for a particular Web method invocation. The following code shows the implementation of the GetInitializer and Initialize methods:

public class ProcessPaymentExtension : SoapExtension  {     CreditCardProcessor  creditCardProcessor;     double               fee = 0;     int                  referenceNumber = 0;     SoapPaymentHeader    payment = null;     SoapReceiptHeader    receipt = new SoapReceiptHeader();     public override object GetInitializer(Type type)     {         return typeof(ProcessPaymentExtension);     }     public override object GetInitializer(LogicalMethodInfo      methodInfo, SoapExtensionAttribute attribute)      {         ProcessPaymentAttribute processPaymentAttribute =          (ProcessPaymentAttribute)attribute;         // Set up connection to credit card authorization service.         creditCardProcessor =          new CreditCardProcessor(processPaymentAttribute.MerchantNumber);         // Return the initialized credit card processor object and the fee.         return new object [] {creditCardProcessor,          processPaymentAttribute.Fee};     }     public override void Initialize(object initializer)      {         // Retrieve the credit card processor and the fee          // from the initializer parameter.         creditCardProcessor = (CreditCardProcessor)((object[])initializer)[0];         fee = (double)((object[])initializer)[1];     }     // The rest of the implementation... }

The Initialize method performs any initialization that is specific to the method invocation. In the case of the ProcessPaymentExtension extension, no initialization needs to be accomplished. The only action is assigning the credit card processor object and the fee to a member variable within the class.

Processing the Message

The ProcessMessage method contains the implementation for processing the request message received from the client and the response message sent by the Web service. ProcessMessage is called by the ASP.NET runtime at four points. It is called twice during the process of deserializing the request message, once before the message is deserialized and once after. The ProcessMessage method is also called twice during the process of serializing the response message, once before serialization and once after.

Each time the ProcessMessage method is called, it is passed an instance of the SoapMessage class. During the BeforeSerialize and AfterSerialize stages, the object is initialized with the data contained within the SOAP message. Here is the implementation of the ProcessMessage method:

public override void ProcessMessage(SoapMessage message)  {     switch (message.Stage)      {         case SoapMessageStage.BeforeDeserialize:             Trace.WriteLine("ProcessMessage(BeforeDeserialize) called.");             break;         case SoapMessageStage.AfterDeserialize:             Trace.WriteLine("ProcessMessage(AfterDeserialize) called.");             // Set the return header information.             foreach(SoapUnknownHeader h in message.Headers)             {                 Trace.WriteLine(h.Element.Name);             }             if(message.Headers.Contains(payment))             {                 referenceNumber = this.creditCardProcessor.Bill(fee, payment);             }             else             {                 // Throw exception.                 throw new SoapException                 ("The required Payment header was not found.",                      SoapException.ClientFaultCode);             }             // Verify that the credit card was processed.             if(referenceNumber > 0)             {                 // Set the return header information.                 receipt.ReferenceNumber = referenceNumber;                 receipt.Amount = fee;             }             else             {                 throw new SoapException                 ("The credit card number could not be confirmed.",                      SoapException.ClientFaultCode);             }             break;         case SoapMessageStage.BeforeSerialize:             Trace.WriteLine("ProcessMessage(BeforeSerialize) called.");             message.Headers.Add(receipt);             break;         case SoapMessageStage.AfterSerialize:             Trace.WriteLine("ProcessMessage(AfterSerialize) called.");             break;         default:             throw new SoapException("An invalid stage enumeration was passed.",                 SoapException.ServerFaultCode);     } }

The SoapMessageStage property determines at which of the four stages the message is called. I use a switch case statement to identify the stage at which ProcessMessage is called.

The code to process the Payment header accesses the header information via the message parameter. The message object is populated with the data contained within the SOAP request message only after the message has been deserialized. Therefore, the code to process the payment information is placed within the SoapMessageStage.AfterDeserialize case block.

Likewise, the code to add the Receipt header to the SOAP response message does so via the message object's Header property. The message object is populated with the data contained within the SOAP request message only before the request message has been deserialized. Therefore, the code to process the payment information is placed within the SoapMessageStage.BeforeSerialize case block.

The code to process the payment information differs only slightly from the code I implemented in the SOAP Header section. One difference is that I use the preinitialized instance of the ProcessCreditCard object instead of creating a new one. The other difference is that the Payment header is obtained from the message object. The message object exposes the Headers property, which is of type SoapHeaderCollection. I obtain the Payment header by calling the Contains method on the instance of the SoapHeaderCollection object exposed by the Headers property.

The SoapMessage class contains other methods and properties that can be used within the ProcessMessage method. Table 6-9 describes some of them.

Table 6-9  Selected Properties and Methods of the SoapMessage Class

Property

Description

Action

Contains the value of the SOAPAction HTTP header

ContentType

Gets/sets the value of the Content-Type HTTP header

Exception

Gets the SoapException thrown from the method

Headers

Gets a collection of SOAP headers (SoapHeaderCollection) within the message

MethodInfo

Gets an object of type LogicalMethodInfo that can be used to reflect on the method signature

OneWay

Indicates whether the request message is accompanied by a response

Stage

Indicates the stage of processing during which the call was made to ProcessMessage

Stream

Obtains an object of type Stream containing the SOAP message

Url

Gets the base URL of the Web service

Method

Description

GetInParameterValue

Obtains a parameter at a particular index that was passed to the Web service

GetOutParameterValue

Obtains an out parameter at a particular index that was passed to the Web service

GetReturnValue

Obtains the return parameter intended for the client

ChainStream Method

Another way to access the data contained within a SOAP message is using the ChainStream method. This method is used by the extension to receive a raw stream containing the contents of the message and to pass the modified version of the stream back to the ASP.NET runtime.

The next example uses a SOAP extension that logs the messages being exchanged between the client and the server. The SoapTrace attribute can be applied to any method. Its associated SoapTrace extension accesses the stream to write the contents of the message to a file.

[AttributeUsage(AttributeTargets.Method)] public class SoapTraceAttribute : SoapExtensionAttribute  {     private string fileName = "c:\\temp\\SoapTrace.log";     private int priority;     public SoapTraceAttribute() {}     public SoapTraceAttribute(string fileName)     {         this.fileName = fileName;     }     public override Type ExtensionType      {         get { return typeof(SoapTraceExtension); }     }     public override int Priority      {         get {return priority;}         set {priority = value;}     }     public string FileName      {         get {return fileName;}         set {fileName = value;}     } }

First I declare the SoapTrace attribute. It contains an optional constructor that can be used to set the name of the trace log. If the filename is not set, it defaults to c:\temp\SoapTrace.log.

public class SoapTraceExtension : SoapExtension  {     string fileName;     Stream inboundStream;     Stream outboundStream;     bool postSerializeHandlers = false;

I declare a few private member variables. The fileName variable holds the filename of the log file obtained by the SoapTrace attribute. The inboundStream and outboundStream variables hold references to the inbound and outbound streams, respectively. Finally postSerializeHandlers indicates whether the BeforeSerialize and AfterSerialize methods have been called. This variable will be used by the ChainStream method.

public override object GetInitializer(Type type) {     return typeof(SoapTraceExtension); } public override object GetInitializer(LogicalMethodInfo methodInfo,  SoapExtensionAttribute attribute)  {     return ((SoapTraceAttribute) attribute).FileName; } public override void Initialize(object initializer)  {     fileName = (string) initializer; }

During GetInitializer, I retrieve the FileName property from the SoapExtension attribute. As with the previous extension, this value will be passed to the Initialize method.

public override Stream ChainStream( Stream stream ) {     // Set the streams based on whether we are about to call     // the deserialize or serialize handlers.     if(! postSerializeHandlers)     {         inboundStream = stream;         outboundStream = new MemoryStream();         return outboundStream;     }     else     {         outboundStream = stream;         inboundStream = new MemoryStream();         return inboundStream;     } }

Recall that ChainStream is called twice by the ASP.NET runtime: before the message received from the client is deserialized, and again before the message that will be sent from the server is serialized.

Each time ChainStream is called, two stream references are passed between the ASP.NET runtime and the SOAP extension. The ChainStream method receives a reference to an inbound stream that contains the original contents of the message. It also returns a reference to an outbound stream that will contain the contents of the new message. This effectively creates a chain of streams between the SOAP extensions associated with the method. The following diagram illustrates the chain that is created:

At least two aspects of ChainStream can easily trip you up. First, the parameters of the ChainStream method have a different meaning depending on whether the method is being called for the first time or the second time. Second, each time ChainStream is called, you need to create a new stream. The new stream is created for the outbound stream the first time it is called and for the inbound stream the second time it is called. Let's talk about each issue in detail.

ChainStream accepts a single parameter of type Stream and returns a parameter of type Stream. The first time ChainStream is called, the inbound stream is passed by the ASP.NET runtime and ChainStream is responsible for returning the outbound stream. The second time ChainStream is called, the outbound stream is passed by the ASP.NET runtime and ChainStream is responsible for returning the inbound stream.

To keep my code as straightforward as possible, I use the postSerializeHandlers member variable to signal whether ChainStream is being called for the first time or the second time. I use an if/else statement to ensure that inboundStream and outboundStream are always set appropriately.

The first time ChainStream is called, it needs to return a readable stream that contains the SOAP message that will be deserialized by the runtime. Because SOAP extensions often modify the contents of the stream, they often return a new instance of the MemoryStream class. If ChainStream creates a new stream, it becomes the outbound stream.

The second time ChainStream is called, it needs to return a read/writable stream to the ASP.NET runtime. The ASP.NET runtime will use this stream to communicate the current contents of the message to the SOAP extension. Before the BeforeSerialization stage of ProcessMessage is called, the ASP.NET runtime will populate the stream with the contents of the SOAP message that was returned by the previously called SOAP extension. When ProcessMessage is finally called, the inbound stream can then be read by the SOAP extension, the message can be modified, and finally the new message can be written to the outbound stream. Therefore, the second time ChainStream is called, it needs to create a new stream for the inbound stream.

There is one more caveat. The ChainStream method cannot modify the stream it receives from the ASP.NET runtime. The received stream can be modified only by the ProcessMessage method. If ChainStream does access any properties or methods of the stream received from ASP.NET, a runtime exception will occur.

public override void ProcessMessage(SoapMessage message)  {     switch (message.Stage)      {         case SoapMessageStage.BeforeDeserialize:             CopyStream(inboundStream, outboundStream);             break;         case SoapMessageStage.AfterDeserialize:             postSerializeHandlers = true;             break;         case SoapMessageStage.BeforeSerialize:             break;         case SoapMessageStage.AfterSerialize:             WriteTraceLogEntry("Response");             CopyStream(inboundStream, outboundStream);             break;         default:             throw new Exception("invalid stage");     } }

The ProcessMessage method is responsible for writing the contents of the inbound stream to the log file. This is accomplished by calling the WriteTraceLogEntry helper function. It must also write the contents of the SOAP message to the outbound stream. This is accomplished by calling the CopyStream helper function. Finally ProcessMessage must set the postSerializeHandlers member variable used by the ChainStream method to true before exiting the AfterDeserialize stage.

private void WriteTraceLogEntry(string messageTitle) {     // Create a file stream for the log file.     FileStream fs = new FileStream(fileName, FileMode.Append,      FileAccess.Write);     // Create a new stream writer and write the header of the trace.     StreamWriter writer = new StreamWriter(fs);     writer.WriteLine();     writer.WriteLine("{0} Message Received at {1}:", messageTitle,      DateTime.Now);     writer.Flush();     // Copy contents of the stream to the file.     AppendStream(inboundStream, fs);     fs.Close(); }

The WriteTraceLogEntry method writes an entry in the SOAP trace log. It first writes a header entry, and then it appends the log file with the contents of the inbound stream:

private void CopyStream(Stream sourceStream, Stream destinationStream)  {     long sourcePosition = 0;     long destinationPosition = 0;     // If seekable, save starting positions of the streams     // and set them both to the beginning.     if(sourceStream.CanSeek)     {         sourcePosition = sourceStream.Position;         sourceStream.Position = 0;     }     if(destinationStream.CanSeek)     {         destinationPosition = destinationStream.Position;         destinationStream.Position = 0;     }     // Copy the contents of the "to" stream into the "from" stream.     TextReader reader = new StreamReader(sourceStream);     TextWriter writer = new StreamWriter(destinationStream);     writer.WriteLine(reader.ReadToEnd());     writer.Flush();     // Set the streams back to their original position.     if(sourceStream.CanSeek) sourceStream.Position = sourcePosition;     if(destinationStream.CanSeek)          destinationStream.Position = destinationPosition; }

The CopyStream method writes the contents of the source stream to the destination stream. Because not all streams received by the ASP.NET runtime are seekable, a check is made before the position of a stream is modified:

private void AppendStream(Stream sourceStream, Stream destinationStream)  {     long sourcePosition = 0;     // If seekable, save starting positions of the streams      // and set them both to the beginning.     if(sourceStream.CanSeek)     {         sourcePosition = sourceStream.Position;         sourceStream.Position = 0;     }     if(destinationStream.CanSeek)     {         destinationStream.Position = destinationStream.Length;     }     // Copy the contents of the "to" stream into the "from" stream.     TextReader reader = new StreamReader(sourceStream);     TextWriter writer = new StreamWriter(destinationStream);     writer.WriteLine(reader.ReadToEnd());     writer.Flush();     // Set the streams back to their original positions.     if(sourceStream.CanSeek) sourceStream.Position = sourcePosition; }

The AppendStream method is used by WriteTraceLogEntry to append the contents of the inbound stream to the end of the log file stream.



Building XML Web Services for the Microsoft  .NET Platform
Building XML Web Services for the Microsoft .NET Platform
ISBN: 0735614067
EAN: 2147483647
Year: 2002
Pages: 94
Authors: Scott Short

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net