Регистрация SOAP-запроса и ответа на стороне сервера

Я пытаюсь создать службу ведения журналов для всех вызовов методов SOAP в моем веб-сервисе ASP.NET. Я просматривал сообщения журнала SOAP от консольного приложения и пошаговое руководство по расширениям SOAP на MSDN ( 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. ASP.NET на веб-сервере получает сообщение SOAP.
  2. Новый экземпляр расширения SOAP создается на веб-сервере.
  3. На веб-сервере, если это расширение SOAP выполняется впервые с этой веб-службой на стороне сервера, метод GetInitializer вызывается на расширении SOAP, работающем на сервере.
  4. Метод Initialize вызывается.
  5. Метод ChainStream вызывается.
  6. Метод ProcessMessage вызывается с SoapMessageStage, установленным в BeforeDeserialize.
  7. ASP.NET десериализует аргументы в XML.
  8. Метод ProcessMessage вызывается с SoapMessageStage, установленным в AfterDeserialize.
  9. ASP.NET создает новый экземпляр класса, реализующий веб-службу, и вызывает метод веб-службы, передавая десериализованные аргументы. Этот объект находится на том же компьютере, что и веб-сервер.
  10. Метод веб-службы выполняет свой код, в конечном итоге устанавливая возвращаемое значение и любые параметры out.
  11. Метод ProcessMessage вызывается с SoapMessageStage, установленным в BeforeSerialize.
  12. ASP.NET на веб-сервере сериализует возвращаемое значение и параметры в XML.
  13. Метод ProcessMessage вызывается с SoapMessageStage, установленным в AfterSerialize.
  14. ASP.NET отправляет ответное сообщение SOAP по сети обратно клиенту веб-службы.

Шаг 6 выполняется правильно, и SOAP XML регистрируется. Тогда он не должен делать ничего больше, пока сервер не обработает вызов, не сделает то, что ему нужно (шаг 10), и не вернет ответ (шаг 13). Вместо этого он сразу звонит ProcessMessage снова в AfterSerialize этап, но на этот раз поток уже потрачен и выдает исключение, когда я пытаюсь войти в систему.

Согласно прохождению я должен использовать ChainStream метод, и он должен быть запущен в шаге 5 выше. Когда я звоню, он запускается дважды, один раз перед BeforeDeserialize и однажды AfterSerialize,

Я попытался скопировать поток сообщений в отдельный поток и использовать его для регистрации, а также для установки какого-либо статуса, если BeforeDeserialize уже запустили, но проблема все еще сохраняется.

Мне все еще нужен код в AfterSerialize обрабатывать ответ, который отправляется обратно клиенту. Но если я попытаюсь удалить свой код в AfterSerialize и только запустить код в BeforeDeserialize' I get aHTTP 400: неверный запрос.

Все это происходит до фактического вызова метода, поэтому я даже не дохожу до кода внутри метода (шаг 10).

2 ответа

Решение

Мое решение основано на mikebridge, но мне пришлось сделать несколько изменений. Инициализаторы должны были быть включены, и возникали исключения, если вы пытались получить доступ к информации о сообщениях мыла на стадии, когда она была недоступна.

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.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>

Вот мой первый снимок, вдохновленный этим и этим.

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 "";
        }
    }
Другие вопросы по тегам