У .NET есть несколько вариантов создания SOAP клиента, одним из них является его генерация с помощью wsdl.exe. На выходе получаем файл (поскольку пишу я на C#, то генерировал cs, соответственно), основой которого является класс, унаследованный от SoapHttpClientProtocol. Подробнее тут.

С моей точки зрения, это достаточно удобный способ, к тому же сам клиент можно подразогнать с помощью sgen.exe (очень хороший пример). Тем не менее есть у него один очень серьезный недостаток — это отсутствие штатной возможности получения текста запроса/ответа. А это было бы крайне удобно при первичной отладке сервисов, разборе ошибок и, самое главное, при возможных разбирательствах со стороны, эти самые сервисы предоставляющей.

Впрочем, если очень хочется, то нужно сделать.

Основная идея


Вполне хорошим выходом будет каким-либо образом переопределить SoapHttpClientProtocol и добавить эту самую возможность логирования. Вариант для получения запросов клиента, представленный тут, был взят за основу, после чего к нему была «прикручена» возможность логирования и ответов сервера.

Переопределям SoapHttpClientProtocol


Релизуем свой класс, пусть будет SoapHttpClientProtocolSpy, унаследованный от SoapHttpClientProtocol соответственно. Для перехвата запросов клиента переопределяем метод GetWriterForMessage, а для перехвата ответов сервера — GetReaderForMessage. Первый возвращает XmlWriter, второй — XmlReader; вместо них и вернем собственные реализации, которые будут позволять получить XMl, через них проходящий.

Получаем следующий класс:

SoapHttpClientProtocolSpy
   public class SoapHttpClientProtocolSpy: SoapHttpClientProtocol
    {
        private XmlWriterSpy writer;
        private XmlReaderSpy reader;
        public SoapHttpClientProtocolSpy() : base(){}

        protected override XmlWriter GetWriterForMessage(SoapClientMessage message, int bufferSize)
        {
            writer = new XmlWriterSpy(base.GetWriterForMessage(message, bufferSize));
            return writer;
        }

        protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
        {
            reader = new XmlReaderSpy(base.GetReaderForMessage(message, bufferSize));
            return reader;
        }

        public string XmlRequest => reader?.Xml;
        public string XmlResponce => writer?.Xml;
    }


XmlWriterSpy и XmlReaderSpy — это декораторы для XmlWriter и XmlReader.

XmlWriterSpy


В сущности, сама реализация заключается в добавлении StringWriter, в который и будем складировать текст запроса по ходу обработки. То есть необходимо переопределить все методы записи так, чтобы они вызывали штатный обработчик и передавали результат в StringWriter.

Реализация
    public class XmlWriterSpy : XmlWriter
    {
       //это же декоратор, поэтому просто пользуемся той реализацией, которая придет
        private XmlWriter _me;
        private XmlTextWriter _bu;
        private StringWriter _sw;

        public XmlWriterSpy(XmlWriter implementation)
        {
            _me = implementation;
            _sw = new StringWriter();
            _bu = new XmlTextWriter(_sw);
            _bu.Formatting = Formatting.Indented;
        }

        public override void Flush()
        {
            _me.Flush();
            _bu.Flush();
            _sw.Flush();
        }
         public string Xml => _sw?.ToString();

        public override void Close() { _me.Close(); _bu.Close(); }
        public override string LookupPrefix(string ns) { return _me.LookupPrefix(ns); }
        public override void WriteBase64(byte[] buffer, int index, int count) { _me.WriteBase64(buffer, index, count); _bu.WriteBase64(buffer, index, count); }
        public override void WriteCData(string text) { _me.WriteCData(text); _bu.WriteCData(text); }
   //И так далее, в том же духе

    }


Еще раз спасибо данной статье за это.

XmlWriterSpy


Здесь идея абсолютно та же. Но только с реализацией придется немного повозиться. С одной стороны, достаточно влезть только в 1 метод(Read), с другой стороны, там чтение из потока, и не так просто его не сломать. Идею взял отсюда.

Реализация
    public class XmlReaderSpy : XmlReader
    {
       //это же декоратор, поэтому просто пользуемся той реализацией, которая придет
        private XmlReader _baseXmlReader;
        StringWriter _sw;
        public string Xml => _sw?.ToString();
        public XmlReaderSpy(XmlReader xmlReader)
        {
            _sw = new StringWriter();
            _baseXmlReader = xmlReader;
        }
     

        public override bool Read()
        {
//получаем прочитанную ноду
                var res = _baseXmlReader.Read();
//каждый тип ноды придется обрабатывать немного по-разному
                switch (_baseXmlReader.NodeType)
                {
                    case XmlNodeType.Element:
                        _sw.Write("<" + _baseXmlReader.Name);
                           while (_baseXmlReader.MoveToNextAttribute())
                                _sw.Write(" " + _baseXmlReader.Name + "='" + _baseXmlReader.Value + "'");
                            _sw.Write(_baseXmlReader.HasValue || _baseXmlReader.IsEmptyElement ? "/>" : ">");
                       //поскольку мы перемещались по элементу, надо вернуться на исходную
                        _baseXmlReader.MoveToElement();
                        break;
                 
                    case XmlNodeType.Text:
                        _sw.Write(_baseXmlReader.Value);
                        break;
                    case XmlNodeType.CDATA:
                        _sw.Write(_baseXmlReader.Value);
                        break;
                    case XmlNodeType.ProcessingInstruction:
                        _sw.Write("<?" + _baseXmlReader.Name + " " + _baseXmlReader.Value + "?>");
                        break;
                    case XmlNodeType.Comment:
                        _sw.Write("<!--" + _baseXmlReader.Value + "-->");
                        break;
                    case XmlNodeType.Document:
                        _sw.Write("<?xml version='1.0'?>");
                        break;
                    case XmlNodeType.Whitespace:
                        _sw.Write(_baseXmlReader.Value);
                        break;
                    case XmlNodeType.SignificantWhitespace:
                        _sw.Write(_baseXmlReader.Value);
                        break;
                    case XmlNodeType.EndElement:
                        _sw.Write("</" + _baseXmlReader.Name + ">");
                        break;
                }
                return res;
        }
    }


Все остальные методы переопределяем на простой вызов того же метода у _baseXmlReader.

Использование


Вспомним о сгенерированном классе, теперь нужно всего лишь унаследовать его от SoapHttpClientProtocolSpy. Стоит сказать, что такая реализация работает, даже если сервер вернет ошибку (то есть она будет тоже залогирована). Ну а далее вызов метода выглядит примерно так:


using (var worker = new Service())
                {
                    try
                    {
                        res = worker.Metod(....);
                        Log.Info((worker?.XmlRequest ?? "")+(worker?.XmlResponce ?? ""));
                    }
                    catch (System.Exception ex)
                    {
                        Log.Error((worker?.XmlRequest ?? "")+(worker?.XmlResponce ?? ""));
                        throw ex;
                    }
                }

PS. С моей точки зрения, достаточно удобным будет вынести это все в отдельную либу. Единственное, что будет постоянно напрягать, так это необходимость после обновления клиента лезть в файл и заменять там SoapHttpClientProtocol, но это мелочи.

Комментарии (11)


  1. eugenebb
    11.09.2017 18:58

    Для простых сценариев, будет проще использовать прокси, что то типа Fiddler. Не только для SOAP, но и для всего остального. Fiddler поддерживает скрипты, так что можно в принципе подмену контента делать и т.п.

    Не критика статьи, а альтернативный вариант, когда пользуешься сторонними библиотеками и т.п.


  1. nzeemin
    12.09.2017 01:35

    Пишется один раз свой класс, порождённый от SoapExtension, который включается/выключается через app.config. И не надо влезать с ногами внутрь, как это сделано здесь.


  1. mayorovp
    12.09.2017 09:16

    Если вы все равно собираете сообщение в текстовый буфер — то зачем было так все усложнять себе написанием оберток над XmlReader и XmlWriter?


    protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
    {
        Stream buffer;
        if (message.Stream.CanSeek) 
            buffer = message.Stream;
        else 
        {
            buffer = new MemoryStream();
            message.Stream.CopyTo(buffer);
            buffer.Position = 0;
        }
        // тут выводим содержимое буфера в лог или куда там было надо
        return XmlReader.Create(buffer); // базовый метод еще о кодировке думал, но в большинстве случаев хватит и такого вызова
    }

    Если же вам нужно сообщение в виде XmlDocument, то все еще проще:


    protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
    {
        var doc = new XmlDocument();
        doc.Load(base.GetReaderForMessage(message, bufferSize));
        // тут выводим doc в лог или куда там было надо
        return new XmlNodeReader(doc);
    }

    Правда, последний метод может поломаться если сериализуемое сообщение содержит массив байт, кодируемый в base64 (нужный метод XmlNodeReader не поддерживает).


    А еще можно написать наследника SoapExtension и подключить его через конфиг.


    PS почему вы отказались от WCF? Ведь WCF-клиентов wsdl.exe генерировать тоже умеет.


    1. XelMed Автор
      13.09.2017 06:10

      Ваш вариант переопределения даже в голову не пришёл если честно, на выходных прогоню тесты, если все пройдет и вы будете не против, то добавлю в статью как более краткую альтернативу.
      WCF — мы не отказывались, просто есть ряд проектов, где что называется «исторически сложилось». К тому же писал я не о разнице между ними, и именно о том как сделать для SoapHttpClientProtocol.
      SoapExtension — у меня не получилось сделать логирование только для конкретных методов, а с учетом того, что 90% как раз не надо логировать — просто запросы на получение данных, ни к чему не обязывающие, то это было очень критично. Сильно подозреваю, что просто не до разобрался с этим вариантом.
      Тем не менее, такой вариант тоже имеет право на жизнь с моей точки зрения.


  1. gosha-z
    12.09.2017 12:49

    Хм. А behavior extension тогда что???


    1. mayorovp
      12.09.2017 13:26

      Это вы к чему?


      1. gosha-z
        12.09.2017 17:34

        К фразе «Тем не менее есть у него один очень серьезный недостаток — это отсутствие штатной возможности получения текста запроса/ответа.». Можно через extension перехватывать запросы и ответы и модифицировать их. Если стоит задача только посмотреть, что улетает/возвращается — diagnostics — messageLogging logEntireMessage=«true»


        1. mayorovp
          12.09.2017 20:08

          А каким боком behavior extension относится к классу SoapHttpClientProtocol?


          1. gosha-z
            12.09.2017 21:32

            Ну у вас же стоит задача выцепить текст SOAP request/reply в варианте автогенерации класса клиента из wsdl. Behavior extension решает эту задачу. Либо настала моя очередь спрашивать «Это вы к чему?»


            1. mayorovp
              12.09.2017 21:45

              Как вы добавите behavior extension к SoapHttpClientProtocol?


              Вы вообще видите разницу между WCF и старым System.Web.Services?


              1. gosha-z
                12.09.2017 22:05

                Да, каюсь, промахнулся…