k10n
Jim Klopfenstein's Radio Weblog

 



Subscribe to "k10n" in Radio UserLand.

Click to see the XML version of this web page.

Click here to send an email to the editor of this weblog.
 
 

Rpc and document SOAP from one .NET web service

An aside in a post on a UserLand discussion forum got me thinking (and coding) late last week.  After a series of posts in which a couple of us were trying to help a programmer connect Radio with .NET Web Services using SOAP, he asked "Am I correct in saying that if you want your service to be more useable you should publish it in both doc & rpc formats?"

Nobody responded to his question, which was almost rhetorical anyway, but it got me thinking.  Could I write a SoapExtension for .NET that would allow the same web service to handle both formats?  It was late in the week, and I was in the mode of code-now, think-later, so I jumped in.  Before the afternoon was over, I had something working.  In the meantime, I've had a little time to think about the limitations of what I did and its possible usefulness.

But first, a few words about what the issue is.

SOAP messages are supposed to be strongly typed.  A SOAP endpoint can specify that messages that it will accept must follow a certain pattern, and messages that are sent to it must indicate that that is the pattern they are following.

Basic SOAP RPC requests specify the datatypes of their parameters one-by-one using XML Schema basic types.  So an RPC request might specify that one parameter is a xsd:string and that another is an xsd:int.  This is part of a protocol that is called SOAP-encoding.

A document-style SOAP request generally applies an XML Schema complex type to its body as a whole.  The assumption is that this complex type is specified somewhere, or at least can be generated automatically on demand.  The place where it is generally specified is in a WSDL file.  This is the "literal" alternative to SOAP-encoding.

When a programmer tags a Microsoft .NET web-service method as a WebMethod, the .NET class framework makes it use document-style, literal messaging.  This is the case in spite of the fact that WebMethods implement remote procedure calls.  This behavior by the .NET framework is easy to change; all the programmer has to do is tag the method with a second attribute, SoapRpcMethod, and the web service will accept only RPC-stype messages for that method.  But document/literal is the default.

[There's another aspect to SOAP-encoding that I haven't mentioned, and that my current code ignores--what's become know as the problem of "multi-ref accessors."  The original SOAP authors wanted a way to specify that multiple parameters (i.e. references) refer to the same entity, so they invented a syntax for specifying entities distinct from their use as parameters.  This means that you can reference the same entity multiple times in a parameter list and that fact will be encoded in the SOAP message.  This method has also become known as Section 5 encoding, as it was described in Section 5 of the SOAP 1.0 spec.]

Certain SOAP requester implementations, including the one in UserScript, the language of Radio Userland and Manila, and the one embodied in the soap: moniker in Microsoft XP, only do rpc/encoded SOAP.  This means that they won't interop with default .NET web services.  For now this is not a big problem, but it will be.  Unless web services authors know to tag their methods with the SoapRpcMethod attribute, they won't be able to interoperate with these common environments.

It is definitely possible to express things in a document/literal SOAP message that can't be expressed in an rpc/encoded message.  [The obverse is also true, but it only really applies to the multi-ref case.]  But except for the multi-ref accessor issue, Microsoft WebMethods do not go beyond what is possible in rpc/encoded messages.  So it seemed to me that I could write some code to transform rpc/encoded messages into document/literal messages on the fly.  This would allow the same WebMethod to accept both kinds of traffic.

There is a hook in the .NET framework SOAP infrastructure that can be used to modify an incoming SOAP packet before most of the infrastructure sees it, the SoapExtension.  This was designed in so that SOAP messages could be encrypted and decrypted, and so new headers could be defined to meet the needs of advanced web services.  These SoapExtensions can be configured at compile time, through the use of attributes, or they can be configured through config files after the service they apply to has been built.  This configuration can be done on a server-wide as well as on a service-by-service basis.

Here's how my extension works.  Whenever it sees an incoming soap packet, it looks for an attribute called encodingStyle with the value "http://schemas.xmlsoap.org/soap/encoding."  This is required in rpc-style messages, so for our purposes it can be used to separate the messages we want to modify from those we want to leave alone.

When my code discovers this attribute, it does four things:

  1. it removes the encodingStyle attribute from the Soap envelope or Soap body element;
  2. it removes the target namespace declaration from the envelope, if it is there (it gets the target namespace value from the HTTP SOAPAction header);
  3. it adds this target namespace as the default namespace on the method call element; and
  4. it removes all attributes from method parameters.

This in effect transforms an rpc/encoded message into a doc/literal one.

To build this SoapExtension, put the following code in a file called RpcToDocument.cs.

using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;
using System.Xml;
namespace SoapRpcToDocumentConversion
{
  public class RpcToDocument : SoapExtension
  {
    Stream inwardStream;
    Stream outwardStream;

private void logMessage(string header,string message) { FileStream fs = new FileStream("c:\\logs\\log.txt", FileMode.Append,FileAccess.Write); StreamWriter w = new StreamWriter(fs); w.WriteLine("--- " + header); w.WriteLine(message); w.Flush(); w.Close(); }

public override System.IO.Stream ChainStream(System.IO.Stream stream) { outwardStream = stream; inwardStream = new MemoryStream(); return inwardStream; }

public override object GetInitializer(System.Type serviceType) { return null; }

public override object GetInitializer( System.Web.Services.Protocols.LogicalMethodInfo methodInfo, System.Web.Services.Protocols.SoapExtensionAttribute attribute) { return null; }

public override void Initialize(object initializer) { }

public override void ProcessMessage(System.Web.Services.Protocols.SoapMessage message) { StreamReader r; StreamWriter w; string whatami; string soapText;

switch (message.Stage) { case SoapMessageStage.BeforeDeserialize: whatami = (message is SoapClientMessage) ? "Response to Client" : "Request to Server"; r = new StreamReader(outwardStream); w = new StreamWriter(inwardStream); soapText = r.ReadToEnd(); logMessage("Unmodified " + whatami,soapText); if (-1 != soapText.IndexOf( "encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"")) { soapText = modifySoapText(message.Action,soapText); logMessage("Modified " + whatami,soapText); } w.Write(soapText);; w.Flush(); inwardStream.Position = 0; break; case SoapMessageStage.AfterSerialize: whatami = (message is SoapClientMessage) ? "Request from Client" : "Response from Server"; inwardStream.Position = 0; r = new StreamReader(inwardStream); w = new StreamWriter(outwardStream); soapText = r.ReadToEnd(); logMessage(whatami,soapText); w.Write(soapText); w.Flush(); break; } }

private string modifySoapText(string action, string soapText) { int nLastSlash; nLastSlash = action.LastIndexOf("/") + 1; string targetNS = action.Substring(0,nLastSlash); string procName = action.Substring(nLastSlash); XmlDocument doc = new XmlDocument(); doc.LoadXml(soapText); XmlNode node = doc.DocumentElement; foreach (XmlAttribute attrib in node.Attributes) { if (attrib.Name.StartsWith("xmlns:") && (attrib.Value == targetNS)) { node.Attributes.Remove(attrib); break; } } foreach (XmlAttribute attrib in node.Attributes) { if (attrib.LocalName == "encodingStyle" && attrib.NamespaceURI == "http://schemas.xmlsoap.org/soap/envelope/") { node.Attributes.Remove(attrib); break; } } XmlNode body = null; foreach (XmlElement elem in node.ChildNodes) { if (elem.LocalName == "Body" && elem.NamespaceURI == "http://schemas.xmlsoap.org/soap/envelope/") { body = elem; break; } } foreach (XmlAttribute attrib in body.Attributes) { if (attrib.LocalName == "encodingStyle" && attrib.NamespaceURI == "http://schemas.xmlsoap.org/soap/envelope/") { body.Attributes.Remove(attrib); break; } } XmlNode method = body.FirstChild; body.RemoveAll(); body.AppendChild(doc.CreateElement(null,method.LocalName,targetNS)); XmlNode newBody = body.FirstChild; cloneWithNewNamespaceNoAttribs(doc,ref newBody,method,targetNS); return doc.OuterXml; }

private static void cloneWithNewNamespaceNoAttribs( XmlDocument doc,ref XmlNode newNode,XmlNode oldNode,string tns) { foreach (XmlNode nd in oldNode.ChildNodes) { if (nd.NodeType == XmlNodeType.Text) newNode.InnerText = nd.InnerText; else { XmlNode newChild = doc.CreateElement(null,nd.LocalName,tns); newNode.AppendChild(newChild); cloneWithNewNamespaceNoAttribs(doc,ref newChild,nd,tns); } } } }

[AttributeUsage(AttributeTargets.Method)] public class RpcToDocumentConversionAttribute : SoapExtensionAttribute { private int priority; public override System.Type ExtensionType { get { return typeof(RpcToDocument); } }

public override int Priority { get { return priority; } set { priority = value; } } } }

Next, build a dll with the command line:

csc /t:dll /r:System.Web.Services.dll RpcToDocument.cs

To apply it to a C# web service, add the attribute [SoapRpcToDocumentConversion.RpcToDocument] to a web service and link in the dll when you build the web service.  (I have included message logging code in the file. Comment it out if you don't want logging.).

A web service with this extension in effect will accept SOAP RPC calls from Radio Userland.  The only place the target namespace needs to be indicated is in the SOAPAction header, so the soap.rpc.client call can be fairly simple.


Click here to visit the Radio UserLand website. © Copyright 2003 Jim Klopfenstein.
Last update: 2/10/2003; 8:43:52 AM.