8

我正在尝试为对我的 ASP.NET Web 服务的所有 SOAP 方法调用创建一个日志记录服务。我一直在查看 来自控制台应用程序的 Log SOAP 消息以及 MSDN 上 SOAP 扩展的演练(http://msdn.microsoft.com/en-us/library/s25h0swd%28v=vs.100%29.aspx)但他们似乎并没有完全覆盖它。

我不想更改 SOAP 消息,只需将其记录到数据库表中。我要做的是读取 SOAP 消息流,将其解析为 XML,记录 XML,然后让调用顺利进行。但是当我阅读流时,它被花费/处置。我尝试复制流内容以不中断流。

根据演练,该ProcessMessage方法应如下所示:

public override void ProcessMessage(SoapMessage message) 
{
   switch (message.Stage) 
   {
   case SoapMessageStage.BeforeSerialize:
       break;
   case SoapMessageStage.AfterSerialize:
       // Write the SOAP message out to a file.
       WriteOutput( message );
       break;
   case SoapMessageStage.BeforeDeserialize:
       // Write the SOAP message out to a file.
       WriteInput( message );
       break;
   case SoapMessageStage.AfterDeserialize:
       break;
   default:
       throw new Exception("invalid stage");
   }
}

我已经设法在阶段解析流而没有问题BeforeDeserialize,但随后ProcessMessage在阶段再次调用,AfterSerialize到那时流被使用并且不再包含任何数据。

根据使用 SOAP 扩展的 SOAP 消息修改 ( http://msdn.microsoft.com/en-us/library/esw638yk%28v=vs.100%29.aspx ) SOAP 调用经过以下步骤:

服务器端收到请求消息并准备响应

  1. Web 服务器上的 ASP.NET 接收 SOAP 消息。
  2. 在 Web 服务器上创建 SOAP 扩展的新实例。
  3. 在 Web 服务器上,如果这是第一次在服务器端使用此 Web 服务执行此 SOAP 扩展,则在服务器上运行的 SOAP 扩展上调用 GetInitializer 方法。
  4. Initialize 方法被调用。
  5. 调用 ChainStream 方法。
  6. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为BeforeDeserialize
  7. ASP.NET 反序列化 XML 中的参数。
  8. ProcessMessage 方法是在 SoapMessageStage 设置为AfterDeserialize时调用的。
  9. ASP.NET 创建实现 Web 服务的类的新实例并调用 Web 服务方法,传入反序列化的参数。此对象与 Web 服务器位于同一台计算机上。
  10. Web 服务方法执行其代码,最终设置返回值和任何输出参数。
  11. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为BeforeSerialize。
  12. Web 服务器上的 ASP.NET 将返回值和输出参数序列化为 XML。
  13. 调用 ProcessMessage 方法时将 SoapMessageStage 设置为AfterSerialize
  14. ASP.NET 通过网络将 SOAP 响应消息发送回 Web 服务客户端。

正确执行了第 6 步并记录了 SOAP XML。然后它不应该再做任何事情,直到服务器处理完调用,完成它需要的(步骤 10)并返回响应(步骤 13)。相反,它会立即ProcessMessageAfterSerialize舞台上再次调用,但这一次流已经用完,当我尝试记录它时会引发异常。

根据演练,我应该使用该ChainStream方法,并且应该在上面的步骤 5 中运行。当我拨打电话时,它会运行两次,一次之前BeforeDeserialize,一次之前AfterSerialize

BeforeDeserialize我尝试将消息流复制到一个单独的流并将其用于日志记录,如果已经运行,也可以设置某种状态,但问题仍然存在。

我仍然需要代码AfterSerialize来处理发送回客户端的响应。但是,如果我尝试删除我的代码AfterSerialize并仅在BeforeDeserialize' I get aHTTP 400: Bad Request` 中运行代码。

这一切都发生在实际的方法调用之前,所以我什至从未接触过方法内部的代码(第 10 步)。

4

2 回答 2

10

我的解决方案基于mikebridge的解决方案,但我不得不做一些改动。必须包含初始化程序,如果您尝试在不可用的阶段访问soap 消息信息,则会引发异常。

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;
    }
}

methodName 变量用于确定我们正在等待响应的入站呼叫。这当然是可选的,但在我的解决方案中,我对其他 Web 服务进行了几次调用,但我只想记录对第一次调用的响应。

第二部分是您需要在 web.config 中添加正确的行。显然它对不包括整个类类型定义很敏感(在这个例子中,只定义了类名,这不起作用。该类从未初始化。):

<?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>
于 2014-04-15T08:48:57.453 回答
2

这是我的第一个镜头,灵感来自thisthis

SoapExtension 在处理流以及初始化或未初始化变量时具有各种副作用和隐藏的时间依赖性,因此这是一个脆弱的代码。我发现关键是将原始流复制到内存流中,然后在正确的时刻再次返回。

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 "";
        }
    }
于 2014-02-10T19:24:41.677 回答