My goal is to read from one stream, transform that stream, and use it as an input to a library that accepts a Stream to read.
I am using two different libraries. One takes an output Stream and transforms it. Let's call it TransformingOutputStream. Its intended use is:
var outputStream = new TransformingOutputStream(finalDestinationOutputStream);
inputStream.CopyTo(outputStream);
I'm using another library that accepts an input Stream. It does whatever it needs and then reads from that stream. Its intended use is:
MagicStreamReadingLibrary.ProcessStream(someInputStream);
I can't pass TransformingOutputStream to it because its intended use is to be written-to, not read-from. I do not have control over either library.
How do I hook-up the TransformingOutputStream to the library function that requires reading from an input Stream?
So far this is the best working example I have, using Anonymous Pipes:
using( var pipeServer = new AnonymousPipeServerStream( PipeDirection.Out ) ) {
var pipeServerTask = Task.Run(
async () => {
using( var stream = getInputStream() ) {
await stream.CopyToAsync( new TransformingOutputStream( pipeServer ) );
}
pipeServer.WaitForPipeDrain();
pipeServer.Dispose();
} );
using( var client = new AnonymousPipeClientStream( PipeDirection.In, pipeServer.ClientSafePipeHandle ) ) {
MagicStreamReadingLibrary.ProcessStream( client );
}
pipeServerTask.Wait();
}
Write it to a flat file then read it back out.
Here is something I just threw together, it should in theory work (untested, I just know it compiles correctly).
public class BufferingStream
{
private readonly Stream _readingStream;
private readonly Stream _writingStream;
private BlockingCollection<byte[]> _buffer;
public BufferingStream()
{
_buffer = new BlockingCollection<byte[]>(new ConcurrentQueue<byte[]>());
_readingStream = new InternalReadingStream(_buffer);
_writingStream = new InternalWritingStream(_buffer);
}
public BufferingStream(int maxQueueLength)
{
_buffer = new BlockingCollection<byte[]>(new ConcurrentQueue<byte[]>(), maxQueueLength);
_readingStream = new InternalReadingStream(_buffer);
_writingStream = new InternalWritingStream(_buffer);
}
public Stream GetReadingStream()
{
return _readingStream;
}
public Stream GetWritingStream()
{
return _writingStream;
}
public int QueueLength
{
get { return _buffer.Count; }
}
public class InternalWritingStream : Stream
{
private readonly BlockingCollection<byte[]> _queuedBytes;
public InternalWritingStream(BlockingCollection<byte[]> queuedBytes)
{
_queuedBytes = queuedBytes;
}
public override void Write(byte[] buffer, int offset, int count)
{
byte[] internalBuffer = new byte[count];
Array.Copy(buffer, offset, internalBuffer, 0, count);
_queuedBytes.Add(internalBuffer);
}
public override void Close()
{
_queuedBytes.CompleteAdding();
base.Close();
}
public override void Flush()
{
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override bool CanRead
{
get { return false; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
}
private sealed class InternalReadingStream : Stream
{
private readonly BlockingCollection<byte[]> _queuedBytes;
private byte[] _currentItem;
private int _currentItemOffset;
public InternalReadingStream(BlockingCollection<byte[]> queuedBytes)
{
_queuedBytes = queuedBytes;
_currentItem = new byte[0];
_currentItemOffset = 0;
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_currentItemOffset == _currentItem.Length)
{
//Try to take the next buffer, if we can't take a item it means we where done adding from the source.
var taken = _queuedBytes.TryTake(out _currentItem, Timeout.Infinite);
if (!taken)
return 0;
_currentItemOffset = 0;
}
var bytesToRead = Math.Min(count, _currentItem.Length - _currentItemOffset);
Array.Copy(_currentItem, _currentItemOffset, buffer, offset, bytesToRead);
_currentItemOffset += bytesToRead;
return bytesToRead;
}
public override void Flush()
{
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
}
}
The way it would work in your use case would be
var bufferingStream = new BufferingStream();
Task.Run(() =>
{
using(var inputStream = GetTheStreamFromSomewhere();
using(var finalDestinationOutputStream = bufferingStream.GetWritingStream())
using(var outputStream = new TransformingOutputStream(finalDestinationOutputStream))
{
inputStream.CopyTo(outputStream);
}
}
using(var someInputStream = bufferingStream.GetReadingStream()) //Technically a using is not necessary on the reading stream but it is good to keep good habits.
{
MagicStreamReadingLibrary.ProcessStream(someInputStream);
}
Initially calls to .Read( calls ProcessStream makes will block until data becomes available. As bytes become available .Read( unblocks and passes along the data. Once finalDestinationOutputStream is disposed it will mark the queue as completed adding and once outputStream finishes its last read it will just return 0 for any subsequent calls.
If you find that your writer is much faster than your reader you may want to pass in a max queue length so writes will block till the reader has a chance to read.
Related
I am using StreamReader over NetworkStream and I want just read one or more lines and another data is byte array (like file data) and I dont want to read that file data in StreamReader, for example I need to read text header line by line and when I see one empty line, header reads must end and next I must read bytes of file,
When I test this with StreamReader I get problems because StreamReader read bytes before I calling ReadLine (after first ReadLine) and after dispose StreamReader and using NetworkStream to read bytes I get block of bytes that is not start of file byte array after header, because StreamReader readed block of bytes in ReadLine and not called ReadLine.
What is wrong in StreamReader or my code settings?
using (var reader = new StreamReader(tcpClient.GetStream()))
{
while (true)
{
var line = reader.ReadLine();
headerResponse += line + "\r\n";
if (line == "")
break;
}
using (var fileStreamReader = tcpClient.GetStream())
{
byte[] bytes = new byte[1024];
var readCount = fileStreamReader.Read(bytes, 0, bytes.Length);
}
}
I created one CustomStreamReader and I read bytes one by one in ReadLine Method and fixed my problem:
public class CustomStreamReader : Stream
{
NetworkStream CurrentStream { get; set; }
public CustomStreamReader(NetworkStream currentStream)
{
CurrentStream = currentStream;
}
public override bool CanRead => CurrentStream.CanRead;
public override bool CanSeek => CurrentStream.CanSeek;
public override bool CanWrite => CurrentStream.CanWrite;
public override long Length => CurrentStream.Length;
public override long Position { get => CurrentStream.Position; set => CurrentStream.Position = value; }
public override void Flush()
{
CurrentStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return CurrentStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return CurrentStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
CurrentStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
CurrentStream.Write(buffer, offset, count);
}
public string ReadLine()
{
List<byte> result = new List<byte>();
do
{
int data = CurrentStream.ReadByte();
if (data == -1)
break;
result.Add((byte)data);
if (data == 13)
{
data = CurrentStream.ReadByte();
if (data == -1)
break;
result.Add((byte)data);
if (data == 10)
break;
}
}
while (true);
return Encoding.UTF8.GetString(result.ToArray());
}
}
Using: C#, MVC 5, IIS 8
I am trying to implement an ActionFilter that will minify html. The basic approach here is to substitute the response's Stream with a custom Stream that writes input into a MemoryStream and then on the Close method minifies the content stored in the MemoryStream and writes out the (minified) content.
The problem I am having is that although the response's type is 'text/html', the content passed to the custom Stream does not look like text or html, it looks like binary. I should add that my site's pages are rendering just fine, so whatever that content is, it's not complete garbage. I added some logging statements to debug, and this is what they look like:
Minification Error | Chunks: 1 | Url: /Login/iFrameLogin.aspx |
Encoding: System.Text.UTF8Encoding | MediaType: text/html | Content:
�
I have also tried turning off dynamic compression on my site, and that made no change either. Does anyone have any ideas why my 'text/html' looks like binary?
FilterAttribute
public class MinifyHtmlFilterAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
HttpContextBase context = filterContext.HttpContext;
HttpRequestBase request = context.Request;
HttpResponseBase response = context.Response;
Encoding encoding = response.ContentEncoding;
string mediaType = response.ContentType;
string currentUrl = request.RawUrl;
var minificationManager = HtmlMinificationManager.Current;
if (response.Filter != null
&& response.StatusCode == 200
&& minificationManager.IsSupportedMediaType(mediaType) //text/html
&& minificationManager.IsProcessablePage(currentUrl))
{
response.Filter = new HtmlMinificationFilterStream(response, minificationManager, currentUrl, encoding, mediaType);
}
}
}
Custom Stream
public class HtmlMinificationFilterStream : Stream
{
private readonly HttpResponseBase _response;
private readonly Stream _stream;
private readonly MemoryStream _cacheStream = new MemoryStream();
private readonly IMarkupMinificationManager _minificationManager;
private readonly string _currentUrl;
private readonly Encoding _encoding;
private readonly string _mediaType;
private int _chunkCount = 0;
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { return 0; }
}
public override long Position
{
get;
set;
}
public HtmlMinificationFilterStream(HttpResponseBase response,
IMarkupMinificationManager minificationManager,
string currentUrl,
Encoding encoding,
string mediaType)
{
_response = response;
_stream = response.Filter;
_minificationManager = minificationManager;
_currentUrl = currentUrl;
_encoding = encoding;
_mediaType = mediaType;
}
public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return _stream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_stream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
_cacheStream.Write(buffer, 0, count);
_chunkCount++;
}
public override void Flush()
{
_stream.Flush();
}
public override void Close()
{
byte[] cacheBytes = _cacheStream.ToArray();
int cacheSize = cacheBytes.Length;
string content = _encoding.GetString(cacheBytes);
var log = $" | Chunks: {_chunkCount} | Url: {_currentUrl} | Encoding: {_encoding} | MediaType: {_mediaType} | Content: {content}";
IMarkupMinifier minifier = _minificationManager.CreateMinifier();
MarkupMinificationResult minificationResult = minifier.Minify(content, _currentUrl, _encoding, false);
bool isMinified = false;
if (minificationResult.Errors.Count == 0)
{
ExceptionHandler.LogException("MINIFICATION SUCCESS" + log, System.Diagnostics.EventLogEntryType.Warning);
using (var writer = new StreamWriter(_stream, _encoding))
{
writer.Write(minificationResult.MinifiedContent);
}
isMinified = true;
}
else
{
foreach (var error in minificationResult.Errors)
{
ExceptionHandler.LogException("Minification Error" + log + " | " + error.SourceFragment, System.Diagnostics.EventLogEntryType.Warning);
}
}
if (!isMinified)
{
_cacheStream.Seek(0, SeekOrigin.Begin);
_cacheStream.CopyTo(_stream);
}
_cacheStream.SetLength(0);
_stream.Close();
}
}
Turning off compression on IIS (8) fixes the issue.
I need to serialize a list of strings as CDATA and thought I would follow an answer of How do you serialize a string as CDATA using XmlSerializer.
It works like a charm for serializing. My XML file looks as desired:
<?xml version="1.0" encoding="utf-8"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<tlist>
<item><![CDATA[First string]]></item>
<item><![CDATA[Second string]]></item>
</tlist>
</root>
But deserialization does not work. The TestList remains empty; value in the setter has count 0. What have I missed?
[XmlRootAttribute("root")]
public class TestConfig
{
public TestConfig()
{
TestList = new List<string>();
CdataList = new List<XmlCDataSection>();
}
[XmlIgnore]
public List<string> TestList { get; set; }
[XmlArray("tlist")]
[XmlArrayItem("item")]
public List<XmlCDataSection> CdataList
{
get { return TestList.Select(a => new XmlDocument().CreateCDataSection(a)).ToList(); }
set
{
TestList = value.Select(s => s.Value).ToList();
}
}
public void Save(string path)
{
var serializer = new XmlSerializer(GetType());
using (var stream = new StreamWriter(path))
{
serializer.Serialize(stream, this);
}
}
public static TestConfig Load(string path)
{
var serializer = new XmlSerializer(typeof(TestConfig));
using (var stream = new StreamReader(path))
{
return (TestConfig)serializer.Deserialize(stream);
}
}
}
Executing:
var t = new TestConfig();
t.TestList.Add("First string");
t.TestList.Add("Second string");
t.Save(#"C:\Test\cdatatest.xml");
var r = TestConfig.Load(#"C:\Test\cdatatest.xml");
Console.WriteLine("Testlist size is {0}", r.TestList.Count);
I thought I would "improve" on the answer by softwariness by reducing the length of the XMLWriter subclass.
/// <summary>
/// Custom XmlWriter.
/// Wraps up another XmlWriter to intercept string writes within
/// elements and writes them as CDATA instead.
/// </summary>
public class XmlCDataWriter : XmlTextWriter
{
public override void WriteString(string text)
{
if (WriteState == WriteState.Element)
{
WriteCData(text);
}
else
{
base.WriteString(text);
}
}
/// <summary>
/// Creates an instance of the XmlTextWriter class using the specified <see cref="T:System.IO.TextWriter"/>.
/// </summary>
/// <param name="w">The TextWriter to write to. It is assumed that the TextWriter is already set to the correct encoding. </param>
public XmlCDataWriter( [NotNull] TextWriter w ) : base( w )
{
}
}
It can then be used with a StringBuffer like so:
using (StringWriter textWriter = new StringWriter())
{
XmlSerializer serializer = new XmlSerializer( typeof( ... ) );
serializer.Serialize(new XmlCDataWriter(textWriter), ... );
return textWriter.ToString();
}
Seems to work so far for me, and it is a MUCH smaller subclass :)
Whilst simple proxies work with single values, you have to do a deeper proxying for collections, because of the way the .NET XML serialization machinery works:
[XmlRootAttribute("root")]
public class TestConfig
{
public TestConfig()
{
TestList = new List<string>();
}
private List<string> testList;
[XmlIgnore]
public List<string> TestList
{
get
{
if (this.testList == null)
{
var newCollection = new List<string>();
if (this.cdataList != null)
{
foreach (var x in this.cdataList)
{
newCollection.Add(x.Value);
}
}
this.testList = newCollection;
this.cdataList = null;
}
return this.testList;
}
set
{
this.testList = value;
this.cdataList = null;
}
}
private List<XmlCDataSection> cdataList;
[XmlArray("tlist")]
[XmlArrayItem("item")]
public List<XmlCDataSection> CdataList
{
get
{
if (this.cdataList == null)
{
var newCollection = new List<XmlCDataSection>();
if (this.testList != null)
{
foreach (var x in this.testList)
{
newCollection.Add(new XmlDocument().CreateCDataSection(x));
}
}
this.cdataList = newCollection;
this.testList = null;
}
return this.cdataList;
}
set
{
this.cdataList = value;
this.testList = null;
}
}
public void Save(string path)
{
var serializer = new XmlSerializer(GetType());
using (var stream = new StreamWriter(path))
{
serializer.Serialize(stream, this);
}
}
public static TestConfig Load(string path)
{
var serializer = new XmlSerializer(typeof(TestConfig));
using (var stream = new StreamReader(path))
{
return (TestConfig)serializer.Deserialize(stream);
}
}
}
The problem is that the serialization code doesn't just get and set the collections in one go. For example, when it's deserializing, it either creates a new collection, or gets one that's already set on the property, and adds to it. If you've created a new collection here computed from the "real" collection that your application needs to deal with, then any changes to the computed collection won't be reflected in the "real" collection.
To work around this, what I've done in the code above is to transfer ownership of the collection from the "real" collection to the "proxy" collection, and back again, depending on which collection property is being accessed. The cost of transferring ownership is incurred only when switching from one property to the other, so successive accesses to the "real" TestList collection in your application won't incur that expense.
This is somewhat inelegant though if you have many such collections. If you wanted to have all your element text serialized as CDATA, you could implement a custom XmlWriter, like the following:
/// <summary>
/// Custom XmlWriter.
/// Wraps up another XmlWriter to intercept string writes within
/// elements and writes them as CDATA instead.
/// </summary>
public class XmlCDataWriter : XmlWriter
{
XmlWriter w;
public XmlCDataWriter(XmlWriter baseWriter)
{
this.w = baseWriter;
}
public override void Close()
{
w.Close();
}
public override void Flush()
{
w.Flush();
}
public override string LookupPrefix(string ns)
{
return w.LookupPrefix(ns);
}
public override void WriteBase64(byte[] buffer, int index, int count)
{
w.WriteBase64(buffer, index, count);
}
public override void WriteCData(string text)
{
w.WriteCData(text);
}
public override void WriteCharEntity(char ch)
{
w.WriteCharEntity(ch);
}
public override void WriteChars(char[] buffer, int index, int count)
{
w.WriteChars(buffer, index, count);
}
public override void WriteComment(string text)
{
w.WriteComment(text);
}
public override void WriteDocType(string name, string pubid, string sysid, string subset)
{
w.WriteDocType(name, pubid, sysid, subset);
}
public override void WriteEndAttribute()
{
w.WriteEndAttribute();
}
public override void WriteEndDocument()
{
w.WriteEndDocument();
}
public override void WriteEndElement()
{
w.WriteEndElement();
}
public override void WriteEntityRef(string name)
{
w.WriteEntityRef(name);
}
public override void WriteFullEndElement()
{
w.WriteFullEndElement();
}
public override void WriteProcessingInstruction(string name, string text)
{
w.WriteProcessingInstruction(name, text);
}
public override void WriteRaw(string data)
{
w.WriteRaw(data);
}
public override void WriteRaw(char[] buffer, int index, int count)
{
w.WriteRaw(buffer, index, count);
}
public override void WriteStartAttribute(string prefix, string localName, string ns)
{
w.WriteStartAttribute(prefix, localName, ns);
}
public override void WriteStartDocument(bool standalone)
{
w.WriteStartDocument(standalone);
}
public override void WriteStartDocument()
{
w.WriteStartDocument();
}
public override void WriteStartElement(string prefix, string localName, string ns)
{
w.WriteStartElement(prefix, localName, ns);
}
public override WriteState WriteState
{
get { return w.WriteState; }
}
public override void WriteString(string text)
{
if (WriteState == WriteState.Element)
{
w.WriteCData(text);
}
else
{
w.WriteString(text);
}
}
public override void WriteSurrogateCharEntity(char lowChar, char highChar)
{
w.WriteSurrogateCharEntity(lowChar, highChar);
}
public override void WriteWhitespace(string ws)
{
w.WriteWhitespace(ws);
}
}
You'd then use it like follows:
var serializer = new XmlSerializer(...));
using (var cdataWriter = new XmlCDataWriter(XmlWriter.Create("somepath.xml")))
{
serializer.Serialize(cdataWriter, myDocumentObject);
}
Again, this only makes sense as an option if you want to write everything as CDATA.
I'm attempting to inject a decoding filter in the request pipeline using IHttpAsyncHandler, but am finding that the Request.Filter property is being ignored.
Has anyone successfully used Request.Filter along with IHttpAsyncHandler?
public class DecodeHttpHandler : IHttpAsyncHandler {
public void ProcessRequest(HttpContext context) {
context.Request.Filter = new DecodeStream(context.Request.Filter);
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {
var decodeContext = new DecodeContext(context);
var w = new HttpContextWrapper(context);
w.Request.Filter = new DecodeStream(w.Request.Filter);
return RequestHandler.BeginProcessRequestBase(w, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result){
RequestHandler.EndProcessRequestBase(result);
}
public bool IsReusable {
get {
return true;
}
}
}
public class DecodeStream : Stream {
... details ...
public override int Read(byte[] buffer, int offset, int count) {
// never gets here
int c = _sink.Read(buffer, offset, count);
return c;
}
}
EDIT: I found another good way to do this not involving Request.Filter, pasted below. However, it's puzzling that Request.Filter simply doesn't work. FYI, my use case was to do URL decoding prior to calling a 3rd-party RequestHandler.
public class UrlDecodeHttpHandler : IHttpAsyncHandler {
public void ProcessRequest(HttpContext context) {
throw new NotImplementedException();
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) {
var decodeContext = new DecodeContext(context);
return RequestHandler.BeginProcessRequestBase(decodeContext, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result){
RequestHandler.EndProcessRequestBase(result);
}
public bool IsReusable {
get {
return false;
}
}
}
public class DecodeContext : HttpContextWrapper {
private readonly HttpContext _context;
public DecodeContext(HttpContext httpContext) : base(httpContext){
_context = httpContext;
}
public override HttpRequestBase Request {
get { return new DecodeRequest(_context.Request); }
}
}
public class DecodeRequest : HttpRequestWrapper {
public DecodeRequest(HttpRequest request) : base(request) {}
public override Stream InputStream {
get {
string result;
using (var sr = new StreamReader(base.InputStream)) {
result = HttpUtility.UrlDecode(sr.ReadToEnd());
}
return GenerateStreamFromString(result);
}
}
private static Stream GenerateStreamFromString(string s) {
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(s);
writer.Flush();
stream.Position = 0;
return stream;
}
}
There are a lot of things you can't change in a httphandler because it's too late. You could try put an HTTPModule in there to intercept and see if that helps. Note I haven't tried this yet if I get time at home I will..
(If anything here needs clarification/ more detail please let me know.)
I have an application (C#, 2.* framework) that interfaces with a third-party webservice using SOAP. I used thinktecture's WSCF add-in against a supplied WSDL to create the client-side implementation. For reasons beyond my control the SOAP message exchange uses WSE2.0 for security (the thinctecture implementation had to be modified to include the WSE2.0 reference). In addition to the 'normal' data package I attach a stored X509 cert and a binary security token from a previous call to a different web service. We are using SSL encryption of some sort - I don't know the details.
All the necessary serialization/deserialization is contained in the web service client - meaning when control is returned to me after calling the client the entire XML string contained in the SOAP response is not available to me - just the deserialized components. Don't get me wrong - I think that's good because it means I don't have to do it myself.
However, in order for me to have something worth storing/archiving I am having to re-serialize the data at the root element. This seems like a waste of resources since my result was in the SOAP response.
Now for my question:
How can I get access to a 'clear' version of the SOAP response so that I don't have to re-serialize everything for storage/archiving?
Edit- My application is a 'formless' windows app running as a network service - triggered by a WebsphereMQ client trigger monitor. I don't think ASP.NET solutions will apply.
Edit - Since the consensus so far is that it doesn't matter whether my app is ASP.NET or not then I will give CodeMelt's (and by extension Chris's) solution a shot.
You can utilize SoapExtension from existing WSE2.0 framework to intercept the responses from the server.
public class MyClientSOAPExtension : SoapExtension
{
Stream oldStream;
Stream newStream;
// Save the Stream representing the SOAP request or SOAP response into
// a local memory buffer.
public override Stream ChainStream( Stream stream )
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeDeserialize:
// before the XML deserialized into object.
break;
case SoapMessageStage.AfterDeserialize:
break;
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
break;
default:
throw new Exception("Invalid stage...");
}
}
}
At stage of SoapMessageStage.BeforeDeserialize,
You can read the expected data you want from oldstream (e.g. use XmlReader).
Then store the expected data somewhere for yourself to use and also you need
forward the old stream data to the newstream for web service later stage to use the data, e.g. deserialize XML into objects.
The sample of logging all the traffic for the web service from MSDN
Here is an example you can setup using Visual studio web reference to http://footballpool.dataaccess.eu/data/info.wso?WSDL
Basically, you must insert in the webservice call chain a XmlReader spyer that will reconstruct the raw XML.
I believe this way is somehow simpler that using SoapExtensions.
Solution solution was inspired by http://orbinary.com/blog/2010/01/getting-the-raw-soap-xml-sent-via-soaphttpclientprotocol/
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Reflection;
using System.Xml;
namespace ConsoleApplication1 {
public class XmlReaderSpy : XmlReader {
XmlReader _me;
public XmlReaderSpy(XmlReader parent) {
_me = parent;
}
/// <summary>
/// Extracted XML.
/// </summary>
public string Xml;
#region Abstract method that must be implemented
public override XmlNodeType NodeType {
get {
return _me.NodeType;
}
}
public override string LocalName {
get {
return _me.LocalName;
}
}
public override string NamespaceURI {
get {
return _me.NamespaceURI;
}
}
public override string Prefix {
get {
return _me.Prefix;
}
}
public override bool HasValue {
get { return _me.HasValue; }
}
public override string Value {
get { return _me.Value; }
}
public override int Depth {
get { return _me.Depth; }
}
public override string BaseURI {
get { return _me.BaseURI; }
}
public override bool IsEmptyElement {
get { return _me.IsEmptyElement; }
}
public override int AttributeCount {
get { return _me.AttributeCount; }
}
public override string GetAttribute(int i) {
return _me.GetAttribute(i);
}
public override string GetAttribute(string name) {
return _me.GetAttribute(name);
}
public override string GetAttribute(string name, string namespaceURI) {
return _me.GetAttribute(name, namespaceURI);
}
public override void MoveToAttribute(int i) {
_me.MoveToAttribute(i);
}
public override bool MoveToAttribute(string name) {
return _me.MoveToAttribute(name);
}
public override bool MoveToAttribute(string name, string ns) {
return _me.MoveToAttribute(name, ns);
}
public override bool MoveToFirstAttribute() {
return _me.MoveToFirstAttribute();
}
public override bool MoveToNextAttribute() {
return _me.MoveToNextAttribute();
}
public override bool MoveToElement() {
return _me.MoveToElement();
}
public override bool ReadAttributeValue() {
return _me.ReadAttributeValue();
}
public override bool Read() {
bool res = _me.Read();
Xml += StringView();
return res;
}
public override bool EOF {
get { return _me.EOF; }
}
public override void Close() {
_me.Close();
}
public override ReadState ReadState {
get { return _me.ReadState; }
}
public override XmlNameTable NameTable {
get { return _me.NameTable; }
}
public override string LookupNamespace(string prefix) {
return _me.LookupNamespace(prefix);
}
public override void ResolveEntity() {
_me.ResolveEntity();
}
#endregion
protected string StringView() {
string result = "";
if (_me.NodeType == XmlNodeType.Element) {
result = "<" + _me.Name;
if (_me.HasAttributes) {
_me.MoveToFirstAttribute();
do {
result += " " + _me.Name + "=\"" + _me.Value + "\"";
} while (_me.MoveToNextAttribute());
//Let's put cursor back to Element to avoid messing up reader state.
_me.MoveToElement();
}
if (_me.IsEmptyElement) {
result += "/";
}
result += ">";
}
if (_me.NodeType == XmlNodeType.EndElement) {
result = "</" + _me.Name + ">";
}
if (_me.NodeType == XmlNodeType.Text || _me.NodeType == XmlNodeType.Whitespace) {
result = _me.Value;
}
if (_me.NodeType == XmlNodeType.XmlDeclaration) {
result = "<?" + _me.Name + " " + _me.Value + "?>";
}
return result;
}
}
public class MyInfo : ConsoleApplication1.eu.dataaccess.footballpool.Info {
protected XmlReaderSpy _xmlReaderSpy;
public string Xml {
get {
if (_xmlReaderSpy != null) {
return _xmlReaderSpy.Xml;
}
else {
return "";
}
}
}
protected override XmlReader GetReaderForMessage(System.Web.Services.Protocols.SoapClientMessage message, int bufferSize) {
XmlReader rdr = base.GetReaderForMessage(message, bufferSize);
_xmlReaderSpy = new XmlReaderSpy((XmlReader)rdr);
return _xmlReaderSpy;
}
}
class Program {
static void Main(string[] args) {
MyInfo info = new MyInfo();
string[] rest = info.Cities();
System.Console.WriteLine("RAW Soap XML response :\n"+info.Xml);
System.Console.ReadLine();
}
}
}
Old thread, but in case others are looking to do this today: these ideas of leveraging SoapExtension or creating 'spy' classes are great, but don't work in .NET Core.
#mting923's suggestion to use IClientMessageInspector approach works in .NET Core 3.1; see here: Get SOAP Message before sending it to the WebService in .NET.
A generated SOAP proxy class is still just a WCF client under the hood, and so the IClientMessageInspector approach works a treat, even for an .NET Core Azure Function calling an older SOAP web service. The following works for me in a .NET Core 3.1 Azure Function:
public class SoapMessageInspector : IClientMessageInspector
{
public string LastRequestXml { get; private set; }
public string LastResponseXml { get; private set; }
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
LastRequestXml = request.ToString();
return request;
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
LastResponseXml = reply.ToString();
}
}
public class SoapInspectorBehavior : IEndpointBehavior
{
private readonly SoapMessageInspector inspector_ = new SoapMessageInspector();
public string LastRequestXml => inspector_.LastRequestXml;
public string LastResponseXml => inspector_.LastResponseXml;
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
}
public void Validate(ServiceEndpoint endpoint)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(inspector_);
}
}
And then it can be set up like this:
var client = new ServiceClient();
var soapInspector = new SoapInspectorBehavior();
client.Endpoint.EndpointBehaviors.Add(soapInspector);
After invoking a web service call on the client proxy, soapInspector.LastRequestXml and soapInspector.LastResponseXml will contain the raw SOAP request and response (as strings).
Inspired by jfburdet, I wanted to see if it was possible to directly intercept at stream/byte level rather than reconstructing XML. And it is! See code below:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Services.Protocols;
using System.Xml;
using Test.MyWebReference;
namespace Test {
/// <summary>
/// Adds the ability to retrieve the SOAP request/response.
/// </summary>
public class ServiceSpy : OriginalService {
private StreamSpy writerStreamSpy;
private XmlTextWriter xmlWriter;
private StreamSpy readerStreamSpy;
private XmlTextReader xmlReader;
public MemoryStream WriterStream {
get { return writerStreamSpy == null ? null : writerStreamSpy.ClonedStream; }
}
public XmlTextWriter XmlWriter {
get { return xmlWriter; }
}
public MemoryStream ReaderStream {
get { return readerStreamSpy == null ? null : readerStreamSpy.ClonedStream; }
}
public XmlTextReader XmlReader {
get { return xmlReader; }
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
DisposeWriterStreamSpy();
DisposeReaderStreamSpy();
}
protected override XmlWriter GetWriterForMessage(SoapClientMessage message, int bufferSize) {
// Dispose previous writer stream spy.
DisposeWriterStreamSpy();
writerStreamSpy = new StreamSpy(message.Stream);
// XML should always support UTF8.
xmlWriter = new XmlTextWriter(writerStreamSpy, Encoding.UTF8);
return xmlWriter;
}
protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize) {
// Dispose previous reader stream spy.
DisposeReaderStreamSpy();
readerStreamSpy = new StreamSpy(message.Stream);
xmlReader = new XmlTextReader(readerStreamSpy);
return xmlReader;
}
private void DisposeWriterStreamSpy() {
if (writerStreamSpy != null) {
writerStreamSpy.Dispose();
writerStreamSpy.ClonedStream.Dispose();
writerStreamSpy = null;
}
}
private void DisposeReaderStreamSpy() {
if (readerStreamSpy != null) {
readerStreamSpy.Dispose();
readerStreamSpy.ClonedStream.Dispose();
readerStreamSpy = null;
}
}
/// <summary>
/// Wrapper class to clone read/write bytes.
/// </summary>
public class StreamSpy : Stream {
private Stream wrappedStream;
private long startPosition;
private MemoryStream clonedStream = new MemoryStream();
public StreamSpy(Stream wrappedStream) {
this.wrappedStream = wrappedStream;
startPosition = wrappedStream.Position;
}
public MemoryStream ClonedStream {
get { return clonedStream; }
}
public override bool CanRead {
get { return wrappedStream.CanRead; }
}
public override bool CanSeek {
get { return wrappedStream.CanSeek; }
}
public override bool CanWrite {
get { return wrappedStream.CanWrite; }
}
public override void Flush() {
wrappedStream.Flush();
}
public override long Length {
get { return wrappedStream.Length; }
}
public override long Position {
get { return wrappedStream.Position; }
set { wrappedStream.Position = value; }
}
public override int Read(byte[] buffer, int offset, int count) {
long relativeOffset = wrappedStream.Position - startPosition;
int result = wrappedStream.Read(buffer, offset, count);
if (clonedStream.Position != relativeOffset) {
clonedStream.Position = relativeOffset;
}
clonedStream.Write(buffer, offset, result);
return result;
}
public override long Seek(long offset, SeekOrigin origin) {
return wrappedStream.Seek(offset, origin);
}
public override void SetLength(long value) {
wrappedStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count) {
long relativeOffset = wrappedStream.Position - startPosition;
wrappedStream.Write(buffer, offset, count);
if (clonedStream.Position != relativeOffset) {
clonedStream.Position = relativeOffset;
}
clonedStream.Write(buffer, offset, count);
}
public override void Close() {
wrappedStream.Close();
base.Close();
}
protected override void Dispose(bool disposing) {
if (wrappedStream != null) {
wrappedStream.Dispose();
wrappedStream = null;
}
base.Dispose(disposing);
}
}
}
}
The MSDN Library includes example code for obtaining the XML of both the request and the response that you can use to archive it. Obviously you'll have to make some changes since the example stores data in a text file, but it isn't too complicated.