Logging SOAP request and response on server side Logging SOAP request and response on server side asp.net asp.net

Logging SOAP request and response on server side


My solution is based on the one by mikebridge, but I had to make a few alterations. The initializers had to be included, and exceptions were thrown if you tried to access soap message information in a stage where it wasn't available.

public class SoapLoggingExtension : SoapExtension{    private Stream _originalStream;    private Stream _workingStream;    private static String _initialMethodName;    private static string _methodName;    private static String _xmlResponse;    /// <summary>    /// Side effects: saves the incoming stream to    /// _originalStream, creates a new MemoryStream    /// in _workingStream and returns it.      /// Later, _workingStream will have to be created    /// </summary>    /// <param name="stream"></param>    /// <returns></returns>    public override Stream ChainStream(Stream stream)    {        _originalStream = stream;        _workingStream = new MemoryStream();        return _workingStream;    }    /// <summary>    /// Process soap message    /// </summary>    /// <param name="message"></param>    public override void ProcessMessage(SoapMessage message)    {        switch (message.Stage)        {            case SoapMessageStage.BeforeSerialize:                break;            case SoapMessageStage.AfterSerialize:                //Get soap call as a xml string                var xmlRequest = GetSoapEnvelope(_workingStream);                //Save the inbound method name                _methodName = message.MethodInfo.Name;                CopyStream(_workingStream, _originalStream);                //Log call                LogSoapRequest(xmlRequest, _methodName, LogObject.Direction.OutPut);                break;            case SoapMessageStage.BeforeDeserialize:                CopyStream(_originalStream, _workingStream);                //Get xml string from stream before it is used                _xmlResponse = GetSoapEnvelope(_workingStream);                break;            case SoapMessageStage.AfterDeserialize:                //Method name is only available after deserialize                _methodName = message.MethodInfo.Name;                LogSoapRequest(_xmlResponse, _methodName, LogObject.Direction.InPut);                break;        }    }    /// <summary>    /// Returns the XML representation of the Soap Envelope in the supplied stream.    /// Resets the position of stream to zero.    /// </summary>    private String GetSoapEnvelope(Stream stream)    {        stream.Position = 0;        StreamReader reader = new StreamReader(stream);        String data = reader.ReadToEnd();        stream.Position = 0;        return data;    }    private void CopyStream(Stream from, Stream to)    {        TextReader reader = new StreamReader(from);        TextWriter writer = new StreamWriter(to);        writer.WriteLine(reader.ReadToEnd());        writer.Flush();    }    public override object GetInitializer(Type serviceType)    {        return serviceType.FullName;    }    //Never needed to use this initializer, but it has to be implemented    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)    {        throw new NotImplementedException();        //return ((TraceExtensionAttribute)attribute).Filename;    }    public override void Initialize(object initializer)    {        if (String.IsNullOrEmpty(_methodName))        {            _initialMethodName = _methodName;            _waitForResponse = false;        }    }    private void LogSoapRequest(String xml, String methodName, LogObject.Direction direction)    {        String connectionString = String.Empty;        String callerIpAddress = String.Empty;        String ipAddress = String.Empty;        try        {            //Only log outbound for the response to the original call            if (_waitForResponse && xml.IndexOf("<" + _initialMethodName + "Response") < 0)            {                return;            }            if (direction == LogObject.Direction.InPut) {                _waitForResponse = true;                _initialMethodName = methodName;            }            connectionString = GetSqlConnectionString();            callerIpAddress = GetClientIp();            ipAddress = GetClientIp(HttpContext.Current.Request.UserHostAddress);            //Log call here            if (!String.IsNullOrEmpty(_methodName) && xml.IndexOf("<" + _initialMethodName + "Response") > 0)            {                //Reset static values to initial                _methodName = String.Empty;                _initialMethodName = String.Empty;                _waitForResponse = false;            }        }        catch (Exception ex)        {            //Error handling here        }    }    private static string GetClientIp(string ip = null)    {        if (String.IsNullOrEmpty(ip))        {            ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];        }        if (String.IsNullOrEmpty(ip) || ip.Equals("unknown", StringComparison.OrdinalIgnoreCase))        {            ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];        }        if (ip == "::1")            ip = "127.0.0.1";        return ip;    }}

The methodName variable is used to determine for which inbound call we are waiting for a response. This is of course optional, but in my solution I make a few calls to other webservices, but I want to log just the response to the first call.

The second part is that you need to add the right lines to your web.config. Apparently it is sensitive to not including the whole class type definition (in this example only the class name is defined, which didn't work. The class was never initialized.):

<?xml version="1.0" encoding="utf-8" ?><configuration><system.web>    <webServices>        <soapExtensionTypes>            <add group="High" priority="1" type="WsNs.SoapLoggingExtension, WsNs, Version=1.0.0.0, Culture=neutral" />        </soapExtensionTypes>    </webServices></system.web></configuration>


Here's my first shot at this, inspired by this and this.

The SoapExtension has all sorts of side-effects and hidden temporal dependencies when dealing with the stream and when variables are initialized or uninitialized, so this is brittle code. I found that the key is to copy the original stream into the Memory stream and then back again at exactly the right moments.

public class SoapLoggingExtension : SoapExtension{    private Stream _originalStream;    private Stream _workingStream;    private string _methodName;    private List<KeyValuePair<string, string>> _parameters;    private XmlDocument _xmlResponse;    private string _url;    /// <summary>    /// Side effects: saves the incoming stream to    /// _originalStream, creates a new MemoryStream    /// in _workingStream and returns it.      /// Later, _workingStream will have to be created    /// </summary>    /// <param name="stream"></param>    /// <returns></returns>    public override Stream ChainStream(Stream stream)    {        _originalStream = stream;        _workingStream = new MemoryStream();        return _workingStream;    }    /// <summary>    /// AUGH, A TEMPLATE METHOD WITH A SWITCH ?!?    /// Side-effects: everywhere    /// </summary>    /// <param name="message"></param>    public override void ProcessMessage(SoapMessage message)    {        switch (message.Stage)        {            case SoapMessageStage.BeforeSerialize:                break;            case SoapMessageStage.AfterSerialize:                var xmlRequest = GetSoapEnvelope(_workingStream);                CopyStream(_workingStream, _originalStream);                LogResponse(xmlRequest, GetIpAddress(), _methodName, _parameters); // final step                break;            case SoapMessageStage.BeforeDeserialize:                CopyStream(_originalStream, _workingStream);                _xmlResponse = GetSoapEnvelope(_workingStream);                _url = message.Url;                break;            case SoapMessageStage.AfterDeserialize:                SaveCallInfo(message);                                    break;        }    }    private void SaveCallInfo(SoapMessage message)    {        _methodName = message.MethodInfo.Name;        // the parameter value is converted to a string for logging,         // but this may not be suitable for all applications.        ParameterInfo[] parminfo = message.MethodInfo.InParameters;        _parameters = parminfo.Select((t, i) => new KeyValuePair<string, String>(                t.Name, Convert.ToString(message.GetInParameterValue(i)))).ToList();    }    private void LogResponse(        XmlDocument xmlResponse,        String ipaddress,        string methodName,         IEnumerable<KeyValuePair<string, string>> parameters)    {        // SEND TO LOGGER HERE!    }    /// <summary>    /// Returns the XML representation of the Soap Envelope in the supplied stream.    /// Resets the position of stream to zero.    /// </summary>    private XmlDocument GetSoapEnvelope(Stream stream)    {        XmlDocument xml = new XmlDocument();        stream.Position = 0;        StreamReader reader = new StreamReader(stream);        xml.LoadXml(reader.ReadToEnd());        stream.Position = 0;        return xml;    }    private void CopyStream(Stream from, Stream to)    {        TextReader reader = new StreamReader(from);        TextWriter writer = new StreamWriter(to);        writer.WriteLine(reader.ReadToEnd());        writer.Flush();    }    // GLOBAL VARIABLE DEPENDENCIES HERE!!    private String GetIpAddress()    {        try        {            return HttpContext.Current.Request.UserHostAddress;        }        catch (Exception)        {            // ignore error;            return "";        }    }