Schema Versioning, WSE 2.0, and the 2004/08 Addressing Namespace
There is an interesting problem if you are using WSE 2.0 and trying to interop with a service that is WS-Addressing aware. WSE 2.0 implements the https://schemas.xmlsoap.org/ws/2004/03/addressing namepace. WSE 3.0 implements the https://schemas.xmlsoap.org/ws/2004/08/addressing namespace. If you look closely, those differ by a single digit, and WSE 2.0 will comfortably ignore messages that contain the 2004/08 namespace... which is likely not a desirable scenario.
Easy enough, just change the addressing namespace in the message, right? It's more complicated than that.
Consider Keith's Surreptitious Forwarding post. Keith's post highlights the scenario why you would want to sign the WS-Addressing header. If you use an output filter in the WSE pipeline that transforms the WS-Addressing namespace from the 2004/03 namespace to the 2004/08 namespace, that breaks the signature. In fact, you have the same problem no matter how you try to accomodate WSE here: using a SoapExtension, an HttpModule, whatever... you can't inject processing because the first thing that WSE does is process the addressing headers, then it processes security.
Using my favorite tool for .NET development, you can peek into the Pipeline.ProcessInputMessage method and see that the addressing headers are processed before the filters in the SoapInputFilterCollection, storing correlation data so that requests and responses can be paired together properly - a great feature for WSE. Unfortunately, this means that you have no means of using security and intercepting the message processing to make modifications. Which is a good thing... if you are using XML signatures in the message, you don't want to provide the ability to change the message.
Sanin posted on the issue of addressing namespace incompatibilities, with a different twist. It just didn't fully hit home until today when I realized that there is no way to have WSE 2.0 accomodate the 2004/08 namespace while using digital signatures. It's not a matter of extensibility, Sanin... the stack has to make some assumptions about its support for certain elements. Just like you wouldn't expect ASMX to understand a new namespace for SOAP envelope (imagine https://schemas.xmlsoap.org/soap/envelope/2005/09), you can't expect WSE to start understanding a new addressing namespace.
All of this is moot in WSE 3.0, which supports the 2004/08 namespace and provides ASMX hosting via TCP. The following is for those using WSE 2.0 that could face this type of problem.
Now that I provided due-dilligence on why it is generally not a good idea to modify the SOAP when you are using signatures, there are plenty of scenarios when you are using WSE for messaging and not using WS-Security.
WSE2 SoapOutputFilter
One option might seem to be using an OutputFilter for WSE. This actually works very well for the outbound message.
using System;
using System.Xml;
using Microsoft.Web.Services2;
using Msdn.Web.Services.Addressing.Extensions;
namespace Msdn.Web.Services.Addressing
{
public class AddressingOutputFilter : Microsoft.Web.Services2.SoapOutputFilter
{
public override void ProcessMessage(SoapEnvelope envelope)
{
System.Xml.XmlNodeReader reader = new XmlNodeReader(envelope);
System.IO.MemoryStream mem = new System.IO.MemoryStream();
//Opportunity here to use Chris Lovett's XmlNodeWriter and chain the writers
XmlAddressingWriter writer = new XmlAddressingWriter(mem);
writer.WriteNode(reader,true);
writer.Flush();
mem.Position = 0;
envelope.Load(mem);
}
}
}
Given this output filter, we need something to actually process the message and do the conversion. We could use lots of DOM methods for this (SoapEnvelope derives from XmlDocument), but yuck.. I just don't like writing DOM code. We could serialize the whole SoapEnvelope to a string, do a string replace, and then parse the string back into the SoapEnvelope... but again, yuck. Instead, an elegant means is to push the contents from an XmlReader to an XmlWriter using the XmlWriter::WriteNode method. We just have to extend the XmlWriter. This might look like a lot of code, but it really isn't. There are only a few methods that you have to override: the ctor, WriteStartAttribute, WriteStartElement, WriteString, and WriteQualifiedName. I love using this pattern with XmlWriter versus lots of XSLT or XPath statements, and generally this performs faster.
using System;
using System.Xml;
namespace Msdn.Web.Services.Addressing.Extensions
{
///
/// Transforms an XML stream containing elements within the 03/2004 WS-Addressing
/// namespace to the 08/2004 WS-Addressing namespace
///
public class XmlAddressingWriter : System.Xml.XmlTextWriter
{
public class WSA2004v03
{
public const string AddressingNS = "https://schemas.xmlsoap.org/ws/2004/03/addressing";
public const string AnonymousRole = "https://schemas.xmlsoap.org/ws/2004/03/addressing/role/anonymous";
public const string UnspecifiedMessageUri = "https://schemas.xmlsoap.org/ws/2004/03/addressing/id/unspecified";
public const string FaultUri = "https://schemas.xmlsoap.org/ws/2004/03/addressing/fault";
}
public class WSA2004v08
{
public const string AddressingNS = "https://schemas.xmlsoap.org/ws/2004/08/addressing";
public const string AnonymousRole = "https://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous";
public const string UnspecifiedMessageUri = "https://schemas.xmlsoap.org/ws/2004/08/addressing/id/unspecified";
public const string FaultUri = "https://schemas.xmlsoap.org/ws/2004/08/addressing/fault";
}
//Indicates if we are currently working with a namespace declaration (xmlns).
private bool _handleNamespace = false;
private bool _handleAttribute = false;
public XmlAddressingWriter(System.IO.Stream stream) : base(stream, System.Text.Encoding.UTF8 )
{
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
if(ns.CompareTo(WSA2004v03.AddressingNS) == 0)
{
base.WriteStartElement(prefix, localName,WSA2004v08.AddressingNS);
}
else
{
base.WriteStartElement(prefix,localName,ns);
}
}
public override void WriteStartAttribute(string prefix, string localName, string ns)
{
_handleAttribute = true;
if(prefix.CompareTo("xmlns") == 0)
{
_handleNamespace = true;
if(ns.CompareTo(WSA2004v03.AddressingNS) == 0)
{
base.WriteStartAttribute(prefix,localName,WSA2004v08.AddressingNS);
}
else
{
base.WriteStartAttribute(prefix,localName,ns);
}
}
else
{
if(ns.CompareTo(WSA2004v03.AddressingNS) == 0)
{
base.WriteStartAttribute(prefix, localName,WSA2004v08.AddressingNS);
}
else
{
base.WriteStartAttribute(prefix,localName,ns);
}
}
}
public override void WriteString(string text)
{
//If we are not handling an attribute, then write the content
if(! _handleAttribute)
{
base.WriteString(text);
}
else
{
//We are handling an attribute... might be a namespace
if(_handleNamespace && text.CompareTo(WSA2004v03.AddressingNS) ==0)
{
base.WriteString(WSA2004v08.AddressingNS);
}
else
{
if(text.CompareTo(WSA2004v03.AnonymousRole) == 0)
base.WriteString(WSA2004v08.AnonymousRole);
else if(text.CompareTo(WSA2004v03.FaultUri) == 0)
base.WriteString(WSA2004v08.FaultUri);
else if(text.CompareTo(WSA2004v03.UnspecifiedMessageUri) == 0)
base.WriteString(WSA2004v08.UnspecifiedMessageUri);
else base.WriteString(text);
}
}
//No longer processing namespace declarations
_handleNamespace = false;
//No longer possibly handling attribute values
_handleAttribute = false;
}
public override void WriteQualifiedName(string localName, string ns)
{
if(ns.CompareTo(WSA2004v03.AddressingNS) == 0)
{
base.WriteQualifiedName(localName, WSA2004v08.AddressingNS);
}
else
{
base.WriteQualifiedName(localName,ns);
}
}
}
}
Another option is to use SoapExtensions for ASMX services. The downside is that this won't work for a SoapService registered as an IHttpHandler (because it isn't using the ASMX pipeline, it only relies on the ASP.NET pipeline). You could also use an IHttpModule to pre-process an HTTP request, which might be your only option when using a SoapService directly.
WSE2 SoapInputFilter?
That was cool, it saves us from serializing and de-serializing the SoapEnvelope. But what about the input case? That is, our service sends requests as outputs, and receives responses as inputs. Since WSE first processes the addressing headers for correlation, you need to pre-process the message before WSE ever gets it. The choices here are more limited. If you are writing your own transport, then this might be as simple as handling it at the transport layer. But if you are using the http or tcp transports that come with WSE 2.0, then you have to rely on some pre-processor to access the message and perform transformations before WSE ever tries to process the input message.