Kirk Evans Blog

.NET From a Markup Perspective

Programmatically Insert SoapHeader into SOAP Request with ASMX SoapExtensions

Programmatically Insert SoapHeader into SOAP Request with ASMX SoapExtensions

Rate This
  • Comments 3

A customer asked about adding arbitrary headers to an outbound SOAP message from Compact Framework. I would have loved to show how to do this with WCF, but alas customers have to wait for .NET Compact Framework 3.5 for WCF support on the mobile device. For projects constrained to using currently shipping technologies, you will need to use ASMX technologies, which are supported on .NET Compact Framework 2.0.

We could just add a SoapHeader to our web service so that our generated proxy picks up the SoapHeader definition and adds it to the WSDL, but the customer needs the ability to dynamically insert headers into the message. The extensibility model for ASMX is the SoapExtension Framework. Using a SoapExtension and some .NET 2.0 goodness, we can create a SoapExtension to insert the header programmatically.

To be clear, everything I am about to show below happens ON THE CLIENT, and uses .NET 2.0 technologies.  There is absolutely NO change to the service at all, and we are not using any .NET 3.x bits here... it's all that "legacy" .NET 2.0 stuff :)  The code that you introduce below is all deployed locally with the client application, because we are affecting the behavior of just the client to insert SOAP headers on outbound messages.

The first step we need to take is to define the SOAP header. Our custom header is derived from the System.Web.Services.Protocols.SoapHeader type.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Services.Protocols;
using System.Xml.Serialization;

namespace DPE.Samples.Web.Services
{
    [XmlRoot("Keystone", Namespace = "urn:com-sample-dpe:soapextension")]
    public class CustomSoapHeader : SoapHeader
    {
        private string _castMemberID;

        public string CastMemberID
        {
            get { return _castMemberID; }
            set { _castMemberID = value; }
        }    
    }
}

The next step is to define the SoapExtension, the mechanism that will insert the above SoapHeader into an outgoing SOAP request.  I am not going into much detail here, other than to state that I started by ripping off the SoapExtension example in the SDK documentation.  I admit, the SoapExtension model itself is a little bizarre and hard to get started with.  The best way I can suggest to get started is to play with a few, maybe even work with Clemens' C# Wizard for SoapExtensions.  If you intend on working with SoapExtensions in the near future (you should really work with WCF's amazing extensibility model instead), make sure to read Steve Maine's rant on ChainStream.  Find a working example, then gut it to make it do what you want it to do.

using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;
using System.Net;
using System.Diagnostics;

namespace DPE.Samples.Web.Services
{
    public class CustomHeaderExtension : SoapExtension
    {
        Stream oldStream;
        Stream newStream;
        string filename;

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }

        public override object GetInitializer(Type WebServiceType)
        {            
            return "C:\\" + WebServiceType.FullName + ".log";
        }

        public override void Initialize(object initializer)
        {
            filename = (string)initializer;
        }

        // Save the Stream representing the SOAP request or SOAP response into
        // a local memory buffer.
        public override Stream ChainStream(Stream stream)
        {
            oldStream = stream;
            newStream = new MemoryStream();
            return newStream;
        }
        
        public override void ProcessMessage(SoapMessage message)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                    //Add the CustomSoapHeader to outgoing client requests
                    if (message is SoapClientMessage)
                    {
                        AddHeader(message);
                    }
                    break;
                case SoapMessageStage.AfterSerialize:
                    newStream.Position = 0;
                    WriteOutput(message);
                    newStream.Position = 0;
                    Copy(newStream, oldStream);
                    break;
                case SoapMessageStage.BeforeDeserialize:
                    Copy(oldStream, newStream);
                    WriteInput(message);
                    newStream.Position = 0;
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
            }
        }

        private void AddHeader(SoapMessage message)
        {
            CustomSoapHeader header = new CustomSoapHeader();
            header.CastMemberID = "gsupike@hotmail.com";
            header.MustUnderstand = false;
            message.Headers.Add(header);
        }

        [Conditional("DEBUG")]
        private void WriteOutput(SoapMessage message)
        {            
            FileStream fs = new FileStream(filename, FileMode.Append,
                FileAccess.Write);
            StreamWriter w = new StreamWriter(fs);

            string soapString = (message is SoapServerMessage) ? "SoapResponse" : "SoapRequest";
            w.WriteLine("-----" + soapString + " at " + DateTime.Now);
            w.Flush();
            Copy(newStream, fs);
            w.Close();
        }

        [Conditional("DEBUG")]
        private void WriteInput(SoapMessage message)
        {            
            FileStream fs = new FileStream(filename, FileMode.Append,
                FileAccess.Write);
            StreamWriter w = new StreamWriter(fs);

            string soapString = (message is SoapServerMessage) ?
                "SoapRequest" : "SoapResponse";
            w.WriteLine("-----" + soapString +
                " at " + DateTime.Now);
            w.Flush();
            newStream.Position = 0;
            Copy(newStream, fs);
            w.Close();            
        }

        private void Copy(Stream from, Stream to)
        {
            TextReader reader = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }
    }
}

Notice in the SoapExtension above that I used the ConditionalAttribute.  If the DEBUG constant is defined, calls to this method are compiled in.  If it is not defined, then the calls are not compiled in, and the method will never be JIT compiled (because it is never called).  When you deploy a Release build, the DEBUG constant is not defined and you won't have logging turned on.

The next thing that we need to do is to create an attribute that can be applied to the client proxy.  This attribute is how the SoapExtension framework knows to invoke the SoapExtension for the outbound message.

using System;
using System.Web.Services.Protocols;


namespace DPE.Samples.Web.Services
{
    // Create a SoapExtensionAttribute for the SOAP Extension that can be
    // applied to an XML Web service method.
    [AttributeUsage(AttributeTargets.Method)]
    public class CustomHeaderExtensionAttribute : SoapExtensionAttribute
    {
        public override Type ExtensionType
        {
            get { return typeof(CustomHeaderExtension); }
        }


        public override int Priority
        {
            get
            {
                return 100;
            }
            set
            {
                
            }
        }
    } 
}

Now that we created our SoapExtension and the attribute that is applied to the client proxy type, we can apply it to the generated proxy.  This is one of my favorite aspects of .NET 2.0 and Visual Studio 2005... the ability to affect client proxies without ever touching the generated source.  We can use a partial type to create a method that includes our custom SOAP header without ever touching the generated Reference.cs file.

using System;
using System.Web.Services.Protocols;
using DPE.Samples.Web.Services;
using System.Web.Services.Description;

namespace ConsoleApplication1.localhost 
{
    //Note the use of the partial class and the CustomHeaderExtensionAttribute.
    //This allows us to add the CustomHeaderExtension
    //without changing the existing proxy code, which allows us to 
    //apply the attribute to just a single method on the proxy.
    public partial class MyService : SoapHttpClientProtocol
    {
        [SoapDocumentMethod("http://tempuri.org/HelloWorld", RequestNamespace = "http://tempuri.org/", ResponseNamespace = "http://tempuri.org/", Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
        [CustomHeaderExtension]
        public string CustomHelloWorld()
        {
            object[] results = this.Invoke("HelloWorld", new object[0]);
            return ((string)(results[0]));
        }
    }
}

If you are wondering what type of voodoo this is, just pop open the Reference.cs file that is generated in Visual Studio when you choode "Add Web Reference."  Inside there, you will see a method called "HelloWorld" that has the same method body.  Here, we are just introducing a new method with a new name that does the same thing... the difference is that we apply our custom attribute to this new method (which must be in the same namespace and declared in a partial class of the same name as the one found in Reference.cs).

The last little bit that we need to take care of is letting the SoapExtension Framework know that we have a SoapExtension registered.  This is done through the client's app.config.  Don't let the section name "system.web" fool you... this is all happening on the client with no changes to the server stuff at all.

  <system.web>

    <webServices>
      <soapExtensionTypes >
        <add type="DPE.Samples.Web.Services.CustomHeaderExtension, DPE.Samples.Web.Services" />
      </soapExtensionTypes>
    </webServices>
  </system.web>

As usual, this looks like a bit of code... admittedly, it is.  The good news is that the extensibility model is so much nicer in WCF that most of this (including the ChainStream silliness) goes away with WCF.  But in case you are constrained where you can't use WCF just yet (such as on a mobile device in a currently shipping app), the steps to injecting a custom SOAP header via a SoapExtension are:

  1. Create the SOAP header definition
  2. Create the SOAP extension (copy and paste from the SDK docs)
  3. Create a custom attribute that will apply your SOAP extension
  4. Create a partial class that extends your generated client proxy
  5. Register the SOAP extension in the client's app.config

Here is the resulting output to the log file.

-----SoapRequest at 8/6/2007 11:23:12 AM

<?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:Header><Keystone xmlns="urn:com-sample-dpe:soapextension"><CastMemberID>gsupike@hotmail.com</CastMemberID></Keystone></soap:Header><soap:Body><HelloWorld xmlns="http://tempuri.org/" /></soap:Body></soap:Envelope>

-----SoapResponse at 8/6/2007 11:23:13 AM

<?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><HelloWorldResponse xmlns="http://tempuri.org/"><HelloWorldResult>hello</HelloWorldResult></HelloWorldResponse></soap:Body></soap:Envelope>

Update: After getting several email requests already on this, I added the solution as an attachment.  I used Visual Studio 2008 Beta 2 for this, so be forewarned that the .sln project will not open in Visual Studio 2005.  To resolve this, just create a new .sln and add each .csproj to it manually, then resolve the references (add a project reference from ConsoleApplication1 to DPE.Samples.Web.Services).

Attachment: DPE.Samples.Web.Services.zip
  • Plenty of resources talk about extensibility in WCF, and mention IClientMessageInspector and IDispatchMessageInspector

  • Plenty of resources talk about extensibility in WCF, and mention IClientMessageInspector and IDispatchMessageInspector

Page 1 of 1 (3 items)
Leave a Comment
  • Please add 3 and 5 and type the answer here:
  • Post
Translate This Page
Search
Archive
Archives