Working with XML Directly


You’ve learned how to build a basic handler, how it works, and how requests come to it through the ASP.NET pipeline. Now we need to deal with SOAP requests and building SOAP responses in kind, and that means incorporating some XML handling into our service handlers.

If you’ll recall, one of the first reasons you saw for working with the handler API rather than a serializer was so you could work in XML directly. There are several other good reasons:

  • If you’re working only with text, there’s no point in using the serializer.

  • You might want to work with a new standard (for example, XML 1.1 or SOAP 1.2) that the serializer doesn’t support yet.

  • You might want to append some processing to your SOAP message that’s specific to your service and that the generic serializer can’t handle. For example, you can make sure the message was validated against the message schema before you touch it, or you can handle encoded images inside or attached to your message.

We’ll look specifically at how to use the two main .NET XML APIs to process incoming SOAP messages and generate SOAP responses—that is, how to approximate what the serializer does in the default Web service handler. Each of these XML APIs— the XML streaming API (not to be confused with the Simple API for XML) and the XML Document Object Model (DOM) API—has its pros and cons, which we’ll discuss as we go along.

Using the XML Streaming API

Let’s take an example called XmlHandler1, which you’ll find included with this book’s sample code. The client sends our handler the name of an album and the artist who recorded it for storage in a public database. Rather than deserializing the two strings into, for example, a RecordDetails object containing two string fields, we’ll use our own handler (XmlHandler1) to pull the data straight out of the request message and add it to the database accordingly using .NET’s XML streaming API.

Let’s assume we’ve already published a schema (you’ll find it in the sample code as AlbumEntry.xsd) for this service that specifies the following format for SOAP request messages:

<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"      xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body>     <AddRecord xmlns="http://www.notashop.com/wscr">         <album>Name_of_album_here</album>         <artist>Name_of_artist_here</artist>     </AddRecord> </soap:Body> </soap:Envelope>

And this format for response messages:

<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"                 xmlns:xsd="http://www.w3.org/2001/XMLSchema">  <soap:Body>     <AddRecordResponse xmlns="http://www.notashop.com/wscr">         <AddRecordResult>Response_message</AddRecordResult>     </AddRecordResponse> </soap:Body> </soap:Envelope>

The code for ProcessRequest in XmlHandler1.ashx has exactly the same shape as in SimpleHandler2.ashx (listed earlier, in the section “Accounting for Request Types”), with the execution of the AddRecord SOAP action reflecting the two steps the server needs to take to process the request and send a response. (Remember, there’s no serialization.)

if (context.Request.Headers["SOAPAction"] == "AddRecord") {     string ResponseMessage = "";     ResponseMessage = AddRecord(context.Request.InputStream);     SendAddRecordResponse(context.Response.OutputStream,          ResponseMessage); }

The SOAP request message can always be found in the InputStream property of the current HttpRequest object, and it’s this stream we send to our method to retrieve and process the message. The AddRecord method returns a string as an acknowledgement message, which SendAddRecordResponse places inside a SOAP response message, pushing it down the stream to the server’s response back to the client:

private string AddRecord(Stream SOAPRequest) {     XmlTextReader RequestReader;     string Response = "";     string album = "";     string artist = "";     //Create new XmlTextReader object from InputStream of HTTP Request     RequestReader = new XmlTextReader(SOAPRequest);

Inside AddRecord, we use the XML streaming API to pull out the album and artist. Specifically, we use an XmlTextReader object to work through the SOAP request sent by the client:

    try     {         //Iterate through document nodes         while(RequestReader.Read())         {             // Filter out nodetypes and concatenate text types accordingly.             if (RequestReader.LocalName == "album")             {                 album = RequestReader.ReadString();             }             if (RequestReader.LocalName == "artist")             {                 artist = RequestReader.ReadString();             }         }         Response = album + " by " + artist +                     " has been added to the record database.";     }     catch(Exception err)     {         Response = "Error occurred while reading request message: "              + err.ToString();     }     return Response; }

The actual work we do here is simple—iterating through the elements in the SOAP request until we find those we want and saving their text content into the appropriate variable. We don’t need to worry about character encoding either because the reader discovers and deals with it automatically.

We could create the response by writing XML tags directly into a string. In this case, checking well-formedness and escaping reserved characters would be our responsibility. We’ll use an XmlTextWriter object instead. Note that in the constructor of the XmlTextWriter object we must specify an encoding if we need something other than the default of UTF-8.

private void SendAddRecordResponse     (Stream responseStream, string message) {     //Create XmlTextWriter to build SOAP request     XmlTextWriter soapWriter =          new XmlTextWriter(responseStream, Encoding.ASCII);

The rest of this method writes out the SOAP response almost verbatim, using the XmlTextWriter object’s write methods to create the elements and set up their namespaces:

    //Write Xml Declaration  soapWriter.WriteStartDocument();     //Write Envelope Element     soapWriter.WriteStartElement("env", "Envelope",                      "http://schemas.xmlsoap.org/soap/envelope/");     soapWriter.WriteAttributeString("xmlns","xsi",null,                      "http://www.w3.org/2001/XMLSchema-instance");     soapWriter.WriteAttributeString("xmlns","xsd",null,                     "http://www.w3.org/2001/XmlSchema");     //Write Body Element     soapWriter.WriteStartElement("Body",                     "http://schemas.xmlsoap.org/soap/envelope/");     //Write AddRecordResponse Element     soapWriter.WriteStartElement(null, "AddRecordResponse",                     "http://www.notashop.com/wscr");     //Write AddRecordResult elements     soapWriter.WriteElementString("AddRecordResult",                     "http://www.notashop.com/wscr", message);     //Close All Elements     soapWriter.WriteEndElement();     soapWriter.WriteEndElement();     soapWriter.WriteEndElement();     soapWriter.WriteEndDocument();

Finally, we close the Response stream so the response is sent:

    //Write to file and close     soapWriter.Close(); }

Which XML API?

In more complex situations, you need to address the question of which .NET XML API to use in your handler services. Even in our trivial example, you could argue that it would be better to use the XML DOM API rather than the XML streaming API. In AddRecord, for example, we could use the DOM API to move directly to the elements we need rather than iterate through the entire SOAP request. As ever, it’s a case of weighing the pros and cons and using the solution that seems best. With that in mind, here are some of the arguments for either API.

The advantages of the DOM API are as follows:

  • It’s a random access API rather than forward-only, which means you can access particular document fragments—such as Web method parameters— in any order.

  • It’s a better choice for complex queries. The DOM API uses XPath statements to identify a group of XML nodes for assessment. You can be very specific if you use XPath correctly.

  • It’s easier to create a SOAP response that’s based on the contents of the SOAP request.

  • You can use the XmlNodeChangedEventArgs class to enforce business rules while the request is being processed by the server.

And, on the flip side, the advantages of the streaming API are

  • It can start processing the SOAP request immediately. The DOM API must first parse the request into memory as a tree before you can start to use it.

  • The larger the SOAP request, the more resources the DOM API uses to deal with it compared to the streaming API—for the reason noted above.

  • It’s better suited to reading through data that has been serialized from a data source (that is, a table or set of tables converted into XML).

  • It’s better suited to working with XML asynchronously than the DOM API. That is, it can start reading through a SOAP request before it has all been retrieved and write a response in which the data to be included is also retrieved asynchronously.

One “feature” the APIs share, however, is the inability to interpret default namespaces correctly. Take, for example, our sample SOAP request:

<soap:Body>     <AddRecord xmlns="http://www.notashop.com/wscr">         <album>Accelerated Evolution</album>         <artist>Devin Townsend</artist>     </AddRecord> </soap:Body>

Both APIs correctly identify that the <Body> element belongs to the namespace identified by the prefix soap. Unfortunately, both return the information that <AddRecord>, <album>, and <artist> have no namespace at all, not even the default one, as is actually the case. The solution is to qualify every element in your request with a prefix rather than use a default namespace.

Using the XML DOM API

In this next example—called SchemaHandler1.ashx in the sample code—we’ll combine the strengths of both APIs and rewrite our previous example so it does the following:

  • Processes and returns messages that do not use a default namespace. Instead, the (previously default) namespace is associated with the prefix a.

    <soap:Body>     <a:AddRecord xmlns:a="http://www.notashop.com/wscr">         <a:album>Accelerated Evolution</a:album>         <a:artist>Devin Townsend</a:artist>     </a:AddRecord> </soap:Body>
  • Validates the request message against our schema, AlbumEntry.xsd, and the SOAP 1.1 schema using an XmlValidatingReader object.

  • Uses the XML DOM API to handle the processing of the request and the construction of a response.

  • Sends a correctly formatted SOAP fault message back to the client if something goes wrong.

With a validation process in our handler against a schema we’ve created, the handler is now stricter than the default ASP.NET Web service handler. However, if you’re validating incoming requests, they should be doc/literal requests rather than RPC/ encoded requests. Doc/literal requests are literally schema-defined and therefore leave no ambiguities in their contents. In contrast, RPC/encoded requests include machine- generated information from client to server that must be accommodated in your schema using wildcards, thus weakening the validation process (the more wildcards your schema contains, the less effective it is) and hindering your ability to control what’s actually sent to your Web service.

Adding Error Handling

Although the general structure of the handler hasn’t actually changed, we’ve added a new try/catch statement to keep track of whether an error occurs while the request is being processed:

case "text/xml":     if (context.Request.Headers["SOAPAction"] == "AddRecord")     {         string ResponseMessage = "";         try         {             ResponseMessage = AddRecord(context.Request.InputStream);             SendAddRecordResponse(ResponseMessage,                  context.Response.OutputStream);         }         catch(Exception e)         {             ResponseMessage = "Error occurred in AddRecord: "                  + e.Message;             GenerateSoapError(ResponseMessage,                  context.Response.OutputStream);         }     }     else     {         GenerateSoapError("You made a HTTP-SOAP request "             + "for a method that doesn't exist",             context.Response.OutputStream);     }     break;

The GenerateSoapError method uses the DOM API to create a new SOAP message containing details of the problem we’ve encountered and then an XmlTextWriter object to channel it back into the appropriate Response stream:

private void GenerateSoapError(string message, Stream responseStream) {     XmlDocument doc = new XmlDocument();

Taking our own advice, neither success nor failure responses use a default namespace, making sure that every element has a valid prefix:

    //Write Envelope Element     XmlElement envelope = doc.CreateElement("env", "Envelope",          "http://schemas.xmlsoap.org/soap/envelope/");     doc.AppendChild(envelope);     //Write Body Element     XmlElement soapBody = doc.CreateElement("env:Body",          "http://schemas.xmlsoap.org/soap/envelope/");     envelope.AppendChild(soapBody);     //Write Fault Element     XmlElement soapFault = doc.CreateElement("env:Fault",          "http://schemas.xmlsoap.org/soap/envelope/");     soapBody.AppendChild(soapFault);

For brevity’s sake, this example includes only the <faultString> element in the SOAP fault message. We encourage you to extend it to include the remaining <faultcode>, <faultactor>, and <detail> elements.

    //Write FaultString Element     XmlElement faultString = doc.CreateElement("env:FaultString",          "http://schemas.xmlsoap.org/soap/envelope/");     faultString.InnerText = message;     soapFault.AppendChild(faultString);        

The cooperation between the streaming API and the DOM API here is in the WriteTo method. Once the fault message has been constructed as an XmlDocument object (a tree structure), the WriteTo method serializes this into the literal text that the writer can deal with:

    XmlTextWriter soapWriter =          new XmlTextWriter(responseStream, Encoding.ASCII);     soapWriter.Formatting = Formatting.Indented;     doc.WriteTo(soapWriter);     soapWriter.Close(); }

The same strategy is used to create a successful SOAP response and send it on its way in SendAddRecordResponse, so we’ll move on and cover the processing of the request message in AddRecord.

Adding Validation

The actual code to check that a SOAP request message (or indeed any XML document) is valid against a schema is straightforward. Here are the steps:

  1. Create a new XmlValidatingReader object against the message.

  2. Set its ValidationType property to Schema and tell it where to find the appropriate schema.

  3. Provide some mechanism to catch any validation errors that might occur as the message is read by the reader.

And that’s it. An XmlValidatingReader object includes a hook to a single event handler that’s called if a validation error occurs. If it isn’t implemented and a validation error occurs, a simple Exception occurs instead. We use this latter approach to catch any errors:

private string AddRecord(Stream SOAPRequest){     string album, artist, Response = "";         XmlTextReader tr = new XmlTextReader(SOAPRequest);     XmlValidatingReader vr = new XmlValidatingReader(tr);     vr.ValidationType = ValidationType.Schema;

Don’t worry about your SOAP message containing elements across several namespaces. The XmlValidatingReader class allows you to declare as many schema documents as are required using its Schemas property. The XmlValidatingReader class also validates (fragments of) XML documents against document type definitions (DTDs) and XML-Data Reduced (XDR) schemas if required, but not all at once; you can specify only one ValidationType at a time. The only tricky part might be actually locating the schema in the first place. Chapter 6 gives more information on locating schemas that are not your own, but for quick reference, the SOAP 1.1 schema is one of the few that can be found at the address given as its namespace URI: http://schemas.xmlsoap.org/soap/envelope. We retrieved a local copy and saved it in the application directory as SoapSchema.xsd.

    vr.Schemas.Add("http://www.notashop.com/wscr",              "http://localhost/wscr/05/schemahandler/AlbumEntry.xsd");     vr.Schemas.Add("http://schemas.xmlsoap.org/soap/envelope/",                "http://localhost/wscr/05/schemahandler/SOAPSchema.xsd");

To use the DOM API, we need to link the Reader object we’ve set up with a new XmlDocument object. Fortunately, one of the overloaded versions of the XmlDocument.Load method does just that:

    XmlDocument doc = new XmlDocument();     doc.Load(vr);

The DOM API provides several document context objects that make life easier when you’re working with an XmlDocument object. One such context object is XmlNamespaceManager, which eliminates the need to specify the namespace URI associated with every element in our document. Instead, we can associate the URI with a prefix and then use that prefix rather than the URI in the XPath expressions we use to extract the artist and album name to be stored in our database:

    XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);     ns.AddNamespace("a", "http://www.notashop.com/wscr");     ns.AddNamespace("env","http://schemas.xmlsoap.org/soap/envelope/");          album = doc.SelectSingleNode        ("/env:Envelope/env:Body/a:AddRecord/a:album/text()", ns).Value;     artist = doc.SelectSingleNode       ("/env:Envelope/env:Body/a:AddRecord/a:artist/text()", ns).Value;     Response = album + " by " + artist +          " has been added to the record database.";     return Response;  }




Programming Microsoft. NET XML Web Services
Programming MicrosoftВ® .NET XML Web Services (Pro-Developer)
ISBN: 0735619123
EAN: 2147483647
Year: 2005
Pages: 172

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