Click here to Skip to main content
Click here to Skip to main content

EchoStream - An Echo/Tee Stream for .NET

, 6 Apr 2003
Rate this:
Please Sign up or sign in to vote.
Presents the EchoStream class and demonstrates its use.

Introduction

EchoStream is a full-featured echoing stream implementation for .NET. In a nutshell, an echoing stream "unions" two other streams, and anything that is written to it is in-turn written to the two underlying streams. This is sometimes called a "tee" stream. EchoStream supports this tee functionality as well as a particular kind of echoing read, in which reads always come from one of the two streams and anything that is read is echoed to the second. Finally, EchoStream includes extensive error handling options that allow a user to choose exactly how echoing errors should be handled.

Background

Back when I did a Java version of my current project, I found a class called TeeOutputStream (or TeeStream) invaluable. It was written by Anil Hemrajani and found somewhere on Sun's Java Developer Site. I modified and extended that class, and wrote an accompanying TeeInputStream, and life was good.

Now that our active development platform is .NET, I found myself needing the exact same functionality. However, I was unable to find a ready-made implementation myself, so I decided to write one from scratch. The result is EchoStream.

EchoStream is implemented in C#, and my code examples are in that language as well.

Using the Code

The simplest use of EchoStream is as a tee, or write-only, stream. One example of when you might need this is when doing network communications. You may have a requirement to log everything that you send over the network stream to a local file. The following code demonstrates this use case.

byte[] outBytes = GetDataToWrite();
NetStream netStream = GetNetworkStream();
FileStream logStream = GetLogStream();
EchoStream stream = new EchoStream(
  netStream, logStream, EchoStream.StreamOwnership.OwnNone
);

// Let's write to the echo stream.  The text that is written will end up
// in both netStream and logStream.
stream.Write(outBytes, 0, outBytes.Length);

// Now, we're done with the echo stream.  Closing it will flush the
// underlying streams, but will not close them because of
// EchoStream.StreamOwnership.OwnNone, above.
stream.Close();

The source code is pretty self-explanatory. As long as the EchoStream is open, writes to it are propagated into the two streams that were passed to its constructor.

As alluded to in the code above, you can use the EchoStream.StreamOwnership enumeration to control whether the EchoStream closes neither, either one, or both of its constituent streams whenever it is closed itself.

If you write anything directly to either of the underlying streams, EchoStream won't know anything about it, and the write will not be echoed. This may be a useful thing to do in some circumstances. EchoStream never buffers its input, so interspersing writes to the underlying streams with writes to an EchoStream should always be safe.

Now, it just so happens that if you have a requirement to log anything going out of your application through a stream, you will probably also have to log anything coming into it. EchoStream handles this as well, by identifying one of its input streams as its PrimaryStream, and the other as its SlaveStream. When writing, the primary stream and the slave stream work identically (well, almost; see below). However, when reading, the echo stream always reads from its primary stream, never from its slave stream. Anything that it reads from its primary stream is then "echoed", or written, into the slave stream. Thus the name of the class.

EchoStream takes the first stream passed to its constructor as the primary stream, and the second one as the slave stream. These streams are thereafter accessible through the PrimaryStream and SlaveStream properties.

This code shows how to read from an echo stream.

NetStream netStream = GetNetworkStream();
FileStream logStream = GetLogStream();
EchoStream stream = new EchoStream(
  netStream, logStream, EchoStream.StreamOwnership.OwnNone
);

// Read from the echo stream.  A read on the echo stream results in a
// read from the primary stream, and a write to the slave stream.
// NOTE: In many cases, you'd use a Reader instead of directly reading
// into a byte[], but I have not used one here for clarity.
byte[] inBytes = new byte[4096];    // Read 4k at a time.
int nRead = stream.Read(inBytes, 0, inBytes.Length);

// At this point, nRead bytes have been read from netStream, and nRead
// bytes have thus been written to logStream, as the input was logged to
// that stream.

As another example, say you need to read and write to the network stream, but you need your input and output to go to two separate streams. Because EchoStream never does any buffering itself, you can accomplish this simply by creating two EchoStream objects with the same primary stream, but different slave streams, as in the following code.

byte[] outBytes = GetDataToWrite();
NetStream netStream = GetNetworkStream();
FileStream outLogStream = GetOutLogStream();
FileStream inLogStream = GetInLogStream();

EchoStream outStream = new EchoStream(
  netStream, outLogStream, EchoStream.StreamOwnership.OwnNone
);
EchoStream inStream = new EchoStream(
  netStream, inLogStream, EchoStream.StreamOwnership.OwnNone
);

// Write to the output stream.  This will write to netStream and
// outLogStream.
outStream.Write(outBytes, 0, outBytes.Length);

// Read from the input stream.  This will read from netStream and
// write to inLogStream.
byte[] inBytes = new byte[4096];    // Read 4k at a time.
int nRead = stream.Read(inBytes, 0, inBytes.Length);

// Now, we're done with the echo stream.  Closing it will flush the
// underlying streams, but will not close them because of
// EchoStream.StreamOwnership.OwnNone, above.
stream.Close();

Exception Handling

The example above will hum right along until one day it runs on a client's machine who has 20 megs of free disk space. Suddenly, it will throw an exception and the network operation will fail, even though nothing bad happened with the network. In post-mortem analysis, it may come up that the 25 megs of data you were transferring were never stored on disk, but yet somehow a disk space error occurred. The answer, of course, is the log files. The way that code is written, writes to your log files must be successful in order for the network operation to also be successful.

For some cases, you may find that writing to the slave stream is just as important as writing to the primary stream, and both must succeed for the operation to succeed. However, in the case I just described above, let's assume that communication over the network stream should not be interrupted even if something had happens while writing to the log streams.

EchoStream treats the primary and slave streams differently in terms of exception handling, even in the write case. (This is the one difference for the write case noted above). Whenever both streams will be modified by an operation, EchoStream always modifies the primary stream first, and always propogates any exception that is thrown back to the caller. After the primary stream has been successfully modified, however, EchoStream's error handling abilities come into play. EchoStream supports a set of properties that control its behavior when exceptions occur while writing to the slave stream. These properties are as follows.

  • SlaveReadFailAction
  • SlaveReadFailFilter
  • SlaveWriteFailAction
  • SlaveWriteFailFilter
  • SlaveSeekFailAction
  • SlaveSeekFailFilter
  • LastReadResult

For each of the primary operations on a stream, EchoStream supports a pair of properties that specify an action to take on failure, and an optional filter to use. The action can be one of Propogate, Ignore or Filter.

The default action is Propogate. When this action is set, any exception caused by an operation on a slave stream after the primary stream has already been modified is allowed to propagate out of EchoStream. This is the most efficient behavior because EchoStream does not need to enter an expensive try block at any point during the operation.

The action that is most useful for the scenario described above is Ignore. When this action is set, any exception caused by an operation on a slave stream after the primary stream has been modified is silently caught and ignored by EchoStream. So for the example described above, adding these lines to the code before the first read or write would have solved the problem of a logging failure causing the entire operation to fail.

outStream.SlaveReadFailAction = EchoStream.SlaveFailAction.Ignore;
outStream.SlaveWriteFailAction = EchoStream.SlaveFailAction.Ignore;
outStream.SlaveSeekFailAction = EchoStream.SlaveFailAction.Ignore;

inStream.SlaveReadFailAction = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveWriteFailAction = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveSeekFailAction = EchoStream.SlaveFailAction.Ignore;

Also, in this particular case, where we simply want to ignore all possible exceptions that the logging streams can cause, we could use the following write-only shortcut property.

outStream.SlaveFailActions = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveFailActions = EchoStream.SlaveFailAction.Ignore;

This is more maintainable for this common case because you don't have to go back and change your code later if new exception-related properties are added to EchoStream; using the shortcut properties shields you from that.

The final action, Filter, is probably the least common but definitely the most flexible of the possible actions. You cannot set any of the "action" properties to this value directly. Instead, it is set implicitly whenever you set one of the filter properties, as in the following code.

inStream.SlaveWriteFailFilter = new EchoStream.SlaveFailHandler(OnWriteFail);
Debug.Assert(inStream.SlaveWriteFailAction == EchoStream.SlaveFailAction.Filter);
...
private EchoStream.SlaveFailAction OnWriteFail(
			object oSender, EchoStream.SlaveFailMethod failMethod,
			Exception exc)
{
  // Here, examine the given failMethod and the exception that occurred, and
  // return one of SlaveFailAction.Propogate or SlaveFailAction.Ignore.
  // Returning SlaveFailAction.Filter will cause an InvalidOperationException.
}

As you can see, using a filter allows you to examine the exception that occurred and instruct the EchoStream on how to proceed. You can use the same method to handle read, write and seek failures by using the failMethod parameter to distinguish between them, or you can register different methods for each case. Also, just as with the "action" properties, you can set a handler for all filters at once by using the SlaveFailFilters method.

A final note is in order for the Propogate case. You may decide that, for maximum efficiency, you want to avoid the expensive try blocks inside EchoStream and instead allow exceptions to propogate back out to a handler that you install in your own code, somewhere such that it doesn't have to be entered once for each read and write operation. You handler may handle the exception somehow (perhaps by detaching the EchoStream and moving to writing only to the primary stream, for instance if the logging streams have "gone bad" due to bad disk space) and then restart your read or write loop. This will work fine, except in the case where the exception happened during a read operation. In that case, the exception caused the return value from Read to be lost from your point of view, so you can't tell how much was read from the stream in order to process the results of the successful read on the primary stream that happened before the slave stream threw an exception. That is where the LastReadResult property comes in. LastReadResult always reflects the result of the last Read operation that occurred on the stream, allowing you to pick up where you left off.

Additional Methods and Properties

Read, Write have been covered extensively now, but there are other methods that can potentially modify the underlying streams.

The Seek and Position properties both work in exactly the same way. They use the SlaveSeekFailAction and SlaveSeekFailFilter properties in exactly the same way as described for Read and Write. Additionally, these properties attempt to work correctly on both underlying streams. For example, imagine that you write "I see a little silhoueto" to a stream and then make it the primary stream in an EchoStream. Imagine also that you have not yet written anything to your slave stream. Writing " of a goat." to the echo stream results in the primary stream containing "I see a little silhouette of a goat.", and the slave stream containing simply "of a goat." Now you realize that you've badly misspelled "lamb" and written "goat" instead, so you decide to fix the mistake. If you set the position of the stream to the length of the "I see..." string, that position will be set directly on the primary stream, but a relative position will be computed for the slave stream based on the change in position of the primary stream. The result is that you get what you expect: the stream pointer for the primary stream goes back to the position just after the 'o' in "silhouette", and the stream pointer for the slave stream goes back to position zero.

Similar to Seek and Position is SetLength. This method changes the size of the slave stream by the same amount that it changes the size of the primary stream, rather than setting them both to the same size. This is in the same spirit as the implementation of Seek and Position.

The final method of Stream that can modify the stream itself is Flush. Flush, however, is just a special case of write, delayed due to buffering. EchoStream never buffers reads or writes, but its constituent streams may, so it is still possible for calling Flush on an EchoStream to cause an exception in one of the underlying streams. Because Flush is just a special case of Write, however, EchoStream just uses the same exception-handling properties used by Write. In this way, any buffering that is done by the underlying streams is relatively transparent to your error handling code, as long you remember that Flush may cause the Write exception-handling framework to be called into action.

Points of Interest

The initial implementation of EchoStream was very simple and straightforward. It was not until I began to write documentation for the methods and properties of the class that I realized just how woefully inadequate the class was in terms of exception handling. The nature of streams, such as the fact that many streams do not support seek operations or those operations are slow, prevent me from writing the methods of the class in an entirely exception-safe manner (in which either both streams are successfully changed, or neither are). However, because one of the two streams in the echo stream is so often going to be subordinate to the other (at least in the use cases I have thought of), it made sense to try to isolate callers from certain failures, or to give them control over what happens when failures occur with that subordinate stream. Thus was born the exception handling facilities for EchoStream, which were entirely absent in the Java Tee streams that were the inspiration for EchoStream.

Adding this error handling code increased the size of the source (including documentation) from ~360 lines to ~850 lines, and greatly increased the complexity of some of the core methods of the class. I feel confident, however, that with these error handling capabilities the class is much more robust and that, after any hidden bugs have been plied out of it through use by myself and any of you in the CodeProject community, it will be a very useful and reliable tool.

Finally, by necessity, the code downloads for EchoStream also include my Covidimus.Diagnostics.Debug class. That class is not covered in this article, but you may want to take a look at it; it may prove useful to you.

Enhancements

The most likely enhancement I can see for this class is actually something that I probably will never add to it due to efficiency concerns, and that is the ability to have multiple slave streams. I do not wish to add this capability directly to EchoStream because it implies keeping an array of slave streams and iterating over them for each modifying operation. Each slave stream would need to have errors handled separately for maximum robustness (meaning that any try blocks that must be entered will need to be entered once for each stream in the list). Even in the Propogate case, I just don't want to add the overhead of iterating over an array to the EchoStream methods when the most common case will be iteration over a single item. It is possible to chain multiple EchoStream objects together to get this behavior, but this is less efficient still than iterating over an array would be. The real solution, in my opinion, would be to develop a class parallel to EchoStream called MulticastEchoStream, which adds support for having multiple slaves. I will leave development of such a beast as an exercise for the reader.

History

  • April 7, 2003 - Initial Posting

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Stephen Quattlebaum
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
QuestionWhy do you say "try" blocks are expensive? PinmemberM. Shawn Dillon14-Apr-03 9:47 
AnswerRe: Why do you say "try" blocks are expensive? PinmemberTim Smith14-Apr-03 10:14 
AnswerRe: Why do you say "try" blocks are expensive? PinmemberStephen Quattlebaum15-Apr-03 5:31 
GeneralA Suggestion PinmemberChoppa9-Apr-03 7:02 
GeneralRe: A Suggestion PinmemberStephen Quattlebaum9-Apr-03 16:23 
QuestionWhat is it good for? PinsitebuilderUwe Keim9-Apr-03 1:04 
AnswerRe: What is it good for? PinmemberStephen Quattlebaum9-Apr-03 3:03 
GeneralRe: What is it good for? PinsitebuilderUwe Keim9-Apr-03 3:10 
Ah, I think I understand now. So you could make EchoStreams out of EchoStreams and write to 2..n targets with one stream?!?!
 
--
- Free Windows-based CMS: www.zeta-software.de/enu/producer/freeware/download.html
- See me: www.magerquark.de
GeneralRe: What is it good for? PinmemberStephen Quattlebaum9-Apr-03 3:12 
GeneralRe: What is it good for? PinmemberDaniel Turini9-Apr-03 7:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141015.1 | Last Updated 7 Apr 2003
Article Copyright 2003 by Stephen Quattlebaum
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid