Tuesday, January 6, 2009

SOAP logging for webservices

If you wish to log incoming SOAP requests to your ASMX webservice, you can write a SOAP extension that does just that.

You’ll need to create a class that inherits from SoapExtension and you should override the methods GetInitializer, Initialize, ChainStream and ProcessMessage.
It’s not that hard, you can find an working example at the MSDN site: How to: Implement a SOAP Extension. It works for both the server (who accepts incoming SOAP requests) and a client (who sends SOAP requests to servers).

All you need to do is add this to your web.config (or app.config) and it will work:

<system.web>
    <webServices>
        <soapExtensionTypes>
            <add type="MySoapLogger, MyNamespace.SoapLogger" priority="1" group="High" />
        </soapExtensionTypes>
    </webServices>
</system.web>

If you wish to create a SOAP logger for a WCF webservice, that’s both easier and harder. It’s simpler to write the extension for, but now you must write different things for a server and a client. And making it configurable requires some more work then the ASMX version.

In WCF you don’t write a SOAP Extension, but rather a Message Interceptor. And you won’t need to inherit from a base class, but you’ll need to implement certain interfaces to make it work.
To be able to see incoming (server) messages you’ll need to implement IDispatchMessageInspector and for outgoing (client) messages you’ll need to implement IClientMessageInspector. It's worth mentioning that nothing prevents you from implementing these interfaces both on the same class. That's what I did.

Both interfaces require you implement two functions:

object IDispatchMessageInspector.AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState);

And:

object IClientMessageInspector.BeforeSendRequest(ref Message request, IClientChannel channel);
void IClientMessageInspector.AfterReceiveReply(ref Message reply, object correlationState);

For IDispatchMessageInspector you have AfterReceiveRequest for when the request is received, before your normal code will run and BeforeSendReply, which is directly before the response will be send to the requester, after all your normal code has run.
For IClientMessageInspector you have BeforeSendRequest which is right before the request is send to the server, after all your normal code has run and AfterReceiveReply for when the response from the server has been received, before your normal code will run.

Basically you can do anything to the request you want at those points, even alter the data. One of the examples I found was to validate incoming requests to an XSD. Another was to do some translations for backwards compatibility sake. But in this case we just want to log the data.

So what we can do in each of these functions, is the following:

try
{
    // Only try to parse the message if it is not empty.
    if(message.IsEmpty == false)
    {
        string xml = message.ToString();
        XDocument xmlDocument = XDocument.Parse(xml);

        StringBuilder sb = new StringBuilder();
        XmlWriter writer = XmlWriter.Create(sb, new XmlWriterSettings { Encoding = Encoding.ASCII, Indent = true });
        xmlDocument.WriteTo(writer);
        writer.Close();

        sb.Append("\r\n");
        this.soapLogger.Log(sb.ToString());
    }
}
catch(Exception e)
{
    if(System.Diagnostics.Debugger.IsAttached == true)
    {
        System.Diagnostics.Debugger.Break();
    }
}

In this case the "message" variable contains the SOAP message. I wrote one function to do the logging and have it called from the 4 functions mentioned above. Also the "soapLogger" class variable is the logger I use. It's not particulary important which logger this specifically is, but if you want to know, I use Log4NET.

The use of the XDocument and the XmlWriter is absolutely not necessary, but I use them to make the XML look pretty (indented) in the log. It is a lot slower then just directly logging it to the logfile, but that would result in the whole XML message to appear on 1 line.
The stuff in the try/catch clause is a neat trick I use to catch the unexpected exceptions.

If you want the most simple solution, you can also do it like this:

// Only try to parse the message if it is not empty.
if(message.IsEmpty == false)
{
    string xml = message.ToString();
    this.soapLogger.Log(xml);
}

No comments: