Retrieving Query Text from SoapHttpClientProtocol
From my point of view, this is a fairly convenient way, in addition, the client itself can be subdivided using sgen.exe (a very good example ). Nevertheless, he has one very serious drawback - the lack of regular ability to receive the text of the request / response. And this would be extremely convenient during the initial debugging of services, analysis of errors and, most importantly, in possible litigation from the side that provides these services.
However, if you really want to, then you need to do it.
main idea
It is quite a good way out to redefine SoapHttpClientProtocol in some way and add this very ability to log. The option for receiving client requests presented here was taken as a basis, after which the possibility of logging and server responses was “screwed” to it.
Override SoapHttpClientProtocol
We are releasing our class, let it be SoapHttpClientProtocolSpy, inherited from SoapHttpClientProtocol, respectively. To intercept client requests, we override the GetWriterForMessage method, and to intercept server responses - GetReaderForMessage. The first returns XmlWriter, the second - XmlReader; instead, we will return our own implementations, which will allow us to get the XMl that passes through them.
We get the following class:
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 and XmlReaderSpy are decorators for XmlWriter and XmlReader.
XmlWriterSpy
In essence, the implementation itself is to add a StringWriter, into which we will store the request text as it is processed. That is, it is necessary to redefine all writing methods so that they call the regular handler and pass the result to 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); }
//И так далее, в том же духе
}Thanks again to this article for this.
XmlWriterSpy
Here the idea is exactly the same. But only with the implementation will have to tinker a bit. On the one hand, it’s enough to get into only 1 method (Read), on the other hand, there is reading from the stream, and it’s not so easy to break it. I took the idea from here .
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("");
break;
case XmlNodeType.Comment:
_sw.Write("");
break;
case XmlNodeType.Document:
_sw.Write("");
break;
case XmlNodeType.Whitespace:
_sw.Write(_baseXmlReader.Value);
break;
case XmlNodeType.SignificantWhitespace:
_sw.Write(_baseXmlReader.Value);
break;
case XmlNodeType.EndElement:
_sw.Write("");
break;
}
return res;
}
}We override all other methods with a simple call to the same method with _baseXmlReader.
Using
Recall the generated class, now you just need to inherit it from SoapHttpClientProtocolSpy. It is worth saying that such an implementation works even if the server returns an error (that is, it will also be logged in). Well, then the method call looks something like this:
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. From my point of view, it will be convenient enough to make this all in a separate lib. The only thing that will constantly bother is the need to climb into the file after updating the client and replace SoapHttpClientProtocol there, but these are trifles.