Cook Computing

Tee Stream in C#

December 12, 2010 Written by Charles Cook

I recently needed to add logging to some code which reads from one stream, processes the data, and then writes it to another stream. I wanted to log the data being read and the data being written. I remembered the tee command from my Unix programming days and looked for something conceptually similar for .NET but while there are plenty of Java tee stream examples I couldn't find anything suitable.

I needed two classes: TeeInputStream to write to a secondary stream as I read from an input stream; and TeeOutputStream, to write to a secondary stream as I wrote to an output stream.

I had to make a couple of design decisions. First, how are exceptions handled, in particular how to distinguish between exceptions from the two streams. I decided that I would only catch exceptions from the secondary stream and re-throw them as the inner exception of a new TeeException class. This meant that exceptions from the primary stream were exposed to calling code in the same way whether or not the tee stream was used.

The second design decision was what to do when Close() or Dispose() is called on the tee stream. I decided that the tee stream shouldn't make any assumptions about how the secondary stream is used and so the default should be that Close() or Dispose() are not called on the secondary stream, but that the constructor of the tee stream has an optional parameter to specify whether they are called automatically.

I started with a base class to override the abstract methods of System.IO.Stream:

using System;
using System.IO;
using System.Runtime.Serialization;

public class TeeStream : Stream
{
    protected TeeStream(Stream primary, Stream secondary, bool autoClose)
    {
        Primary = primary;
        Secondary = secondary;
        AutoClose = autoClose;
    }

    protected Stream Primary { get; private set; }
    protected Stream Secondary { get; private set; }
    protected bool AutoClose { get; set; }

    public override void Flush()
    {
        Primary.Flush();
        try { Secondary.Flush(); }
        catch (Exception ex) { throw new TeeException(ex); }
    }

    public override void Close()
    {
        Primary.Close();
        if (AutoClose)
        {
          try { Secondary.Close(); }
          catch (Exception ex) { throw new TeeException(ex); }
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            Primary.Dispose();
            if (AutoClose)
            {
              try { Secondary.Dispose(); }
              catch (Exception ex) { throw new TeeException(ex); }
            }
        }
    }

    // all other abstract methods implemented to throw NotSupportedException
    // or false in the case of the Can* methods
    // ...
}

The TeeException class just wraps exceptions from the secondary stream:

[Serializable]
public TeeException : System.Exception
{
    public TeeException() {}

    public TeeException(Exception ex)
        : base(string.Format("Secondary stream: {0}", ex.Message), ex) {}

    protected TeeException(SerializationInfo info, StreamingContext context)
        : base(info, context) {}
}

Implementing the two classes is now trivial:

public class TeeInputStream : TeeStream
{
    public TeeInputStream(Stream inputStream, Stream secondary, 
        bool autoClose = false) : base(inputStream, secondary, autoClose) {}

    public override int Read(byte[] buffer, int offset, int count)
    {
        int read = Primary.Read(buffer, offset, count);
        try { Secondary.Write(buffer, offset, read); }
        catch (Exception ex) { throw new TeeException(ex); }
        return read;
    }

    public override bool CanRead { get { return true; } }
}

And TeeOutputStream:

public class TeeOutputStream : TeeStream
{
    public TeeOutputStream(Stream outputStream, Stream secondary, 
        bool autoClose = false) : base(outputStream, secondary, autoClose) {}

    public override void Write(byte[] buffer, int offset, int count)
    {
      try { Secondary.Write(buffer, offset, count); }
      catch (Exception ex) { throw new TeeException(ex); }
      Primary.Write(buffer, offset, count);
    }

    public override bool CanWrite { get { return true; } }
}

Note that in Write() the secondary stream is written to first, so that if we are using the secondary stream for logging we get to log the data read from the input stream before it potentially causes any exceptions when it is written to the primary stream.

One flaw with this implementation is that while BeginRead() and BeginWrite() are implemented in the System.IO.Stream, the base class simply makes calls to Read() and Write() on a thread from the managed thread pool, possibly blocking this thread. Ideally the implementation of the tee streams should override the Begin*() functions and make calls to the corresponding functions of the primary and secondary streams but that is a task for another time.

Source code: TeeStream.cs