Streams are a very nice abstraction over a read/write loop. We can use them to represent the contents of a file, or a stream of bytes to or from a network socket. They make it easy to read and write large amounts of data without consuming large amounts of memory. Take this little code snippet:
using (var inputStream = File.OpenRead(@"TestFiles/Example.txt")) | |
using (var outputStream = File.OpenWrite(@"TestFiles/Output.txt")) | |
{ | |
inputStream.CopyTo(outputStream); | |
} |
Example.txt may be many GB in size, but this operation will only ever use the amount of memory configured for the buffer. As an aside, the .NET framework’s Stream class’s default buffer size is the maximum multiple of 4096 that is still smaller than the large object heap threshold (85K). This means it likely to be collected at gen zero by the garbage collector, but still gives good performance.
But what if we want to log or view the contents of Example.txt as it’s copied to the output file? Let me introduce my new invention: InterceptionStream. This is simple class that inherits and decorates Stream and takes an additional output stream. Each time the wrapped stream is read from, or written to, the additional output stream gets the same information written to it. You can use it like this:
using (var logStream = File.OpenWrite(@"TestFiles/Log.txt")) | |
using (var inputStream = File.OpenRead(@"TestFiles/Example.txt")) | |
using (var outputStream = new InterceptionStream(File.OpenWrite(@"TestFiles/Output.txt"), logStream)) | |
{ | |
inputStream.CopyTo(outputStream); | |
} |
I could just as well have wrapped the input stream with the InterceptionStream for the same result:
using (var logStream = File.OpenWrite(@"TestFiles/Log.txt")) | |
using (var inputStream = new InterceptionStream(File.OpenRead(@"TestFiles/Example.txt"), logStream)) | |
using (var outputStream = File.OpenWrite(@"TestFiles/Output.txt")) | |
{ | |
inputStream.CopyTo(outputStream); | |
} |
You can use a MemoryStream if you want to capture the log in memory and assign it to a string variable, but of course this negates the memory advantages of the stream copy since we’re now buffering the entire contents of the stream in memory:
using (var logStream = new MemoryStream()) | |
using (var inputStream = new InterceptionStream(File.OpenRead(@"TestFiles/Example.txt"), logStream)) | |
using (var outputStream = File.OpenWrite(@"TestFiles/Output.txt")) | |
{ | |
inputStream.CopyTo(outputStream); | |
var logText = Encoding.UTF8.GetString(logStream.GetBuffer()); | |
Console.Out.WriteLine(logText); | |
} |
Here is the InterceptionStream implementation. As you can see it’s very simple. All the work happens in the Read and Write methods:
public class InterceptionStream : Stream | |
{ | |
public Stream InnerStream { get; private set; } | |
public Stream CopyStream { get; private set; } | |
public InterceptionStream(Stream innerStream, Stream copyStream) | |
{ | |
if (innerStream == null) throw new ArgumentNullException("innerStream"); | |
if (copyStream == null) throw new ArgumentNullException("copyStream"); | |
if (!copyStream.CanWrite) | |
{ | |
throw new ArgumentException("copyStream is not writable"); | |
} | |
InnerStream = innerStream; | |
CopyStream = copyStream; | |
} | |
public override void Flush() | |
{ | |
InnerStream.Flush(); | |
} | |
public override long Seek(long offset, SeekOrigin origin) | |
{ | |
return InnerStream.Seek(offset, origin); | |
} | |
public override void SetLength(long value) | |
{ | |
InnerStream.SetLength(value); | |
} | |
public override int Read(byte[] buffer, int offset, int count) | |
{ | |
var bytesRead = InnerStream.Read(buffer, offset, count); | |
if (bytesRead != 0) | |
{ | |
CopyStream.Write(buffer, offset, bytesRead); | |
} | |
return bytesRead; | |
} | |
public override void Write(byte[] buffer, int offset, int count) | |
{ | |
InnerStream.Write(buffer, offset, count); | |
CopyStream.Write(buffer, offset, count); | |
} | |
public override bool CanRead | |
{ | |
get { return InnerStream.CanRead; } | |
} | |
public override bool CanSeek | |
{ | |
get { return InnerStream.CanSeek; } | |
} | |
public override bool CanWrite | |
{ | |
get { return InnerStream.CanWrite; } | |
} | |
public override long Length | |
{ | |
get { return InnerStream.Length; } | |
} | |
public override long Position | |
{ | |
get { return InnerStream.Position; } | |
set { InnerStream.Position = value; } | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
InnerStream.Dispose(); | |
} | |
} |