Working with Streams

The System.IO namespace contains types that allow reading and writing to files and data streams, and types that provide basic file and directory support.

Looking at all the streams classes, I sometimes get this overwhelmed feeling: which one do I use? When writing/reading text or binary files it’s pretty easy, but what if I need to apply compression and encryption and other operations?

The C# 3.0 in a Nutshell book (written by Joseph Albahari; Ben Albahari) has a great chapter describing streams. The System.IO.Stream class provides a generic view of a sequence of bytes – and usually we don’t use it directly. The important thing to remember is that there are 3 layers:

1. Backing store stream – the classes that talk directly with the physical stores. These classes work with bytes (raw data). Some classes:

· FileStream Exposes a Stream around a file, supporting both synchronous and asynchronous read and write operations.

· MemoryStream Creates a stream whose backing store is memory.

· UnmanagedMemoryStream Provides access to unmanaged blocks of memory from managed code.

2. Stream decorators – they can be applied on top of backing store streams to transform the data in some way (like adding compression and encryption). Again they work with bytes. Some classes are:

· DeflateStream Provides methods and properties for compressing and decompressing streams using the Deflate algorithm (System.IO.Compressionnamespace).

· GZipStream Provides methods and properties used to compress and decompress streams (System.IO.Compressionnamespace).

· CryptoStream Defines a stream that links data streams to cryptographic transformations (System.Security.Cryptography namespace)

· BufferedStream Adds a buffering layer to read and write operations on another stream. This class cannot be inherited.

3. Stream adapters – the high level classes that provide abstractions on top of the backing store streams and decorators. Some classes:

· BinaryReader Reads primitive data types as binary values in a specific encoding.

· BinaryWriter Writes primitive types in binary to a stream and supports writing strings in a specific encoding.

· StreamReader Implements a TextReader that reads characters from a byte stream in a particular encoding.

· StreamWriter Implements a TextWriter for writing characters to a stream in a particular encoding.

We can chain streams – by passing a stream in the constructor of another stream. This way, we can accomplish multiple operations easily.

Let’s say we have a simple class called Person:

internal class Person

    {

        public Person() { }

        public Person(string name, int age, string address)

        {

            this.Name = name;

            this.Age = age;

            this.Address = address;

        }

        public string Name {get;set;}

        public int Age {get;set;}

        public string Address {get;set;}

……

    }

If we want to save a person to a stream, we could add a Serialize and a Deserialize method:

        public void Serialize(Stream stream){

            using (BinaryWriter writer = new BinaryWriter(stream)){

                writer.Write(Name);

                writer.Write(Age);

                writer.Write(Address);

                // closing the writer will also close the underlying stream

            }

        }

        internal static Person Deserialize(Stream stream) {

            Person p = new Person();

            using (BinaryReader reader = new BinaryReader(stream)) {

                p.Name = reader.ReadString();

                p.Age = reader.ReadInt32();

                p.Address = reader.ReadString();

            }

            return p;

        }

Similar, we could serialize and deserialize from byte[] (new code in bold):

        public void Serialize(byte[] buf, ref int bufLength){

            using (MemoryStream s = new MemoryStream()){

                using (BinaryWriter writer = new BinaryWriter(s)){

                    writer.Write(Name);

                    writer.Write(Age);

                    writer.Write(Address);

                    buf = s.GetBuffer();

                    bufLength = (int)s.Length;

                    // closing the writer will close the underlying stream

                }

            }

        }

        internal static Person Deserialize(byte[] buf) {

            Person p = new Person();

            using (MemoryStream stream = new MemoryStream(buf)) {

                using (BinaryReader reader = new BinaryReader(stream)){

                    p.Name = reader.ReadString();

                    p.Age = reader.ReadInt32();

                    p.Address = reader.ReadString();

                }

            }

            return p;

        }

And here comes the beautiful part. If we want to add compression / decompression on top of this, it’s this simple:

        public void SerializeAndCompress(ref byte[] buf, ref int bufLength)

        {

            using (MemoryStream s = new MemoryStream()) {

                using (BinaryWriter writer = new BinaryWriter(s)) {

                    writer.Write(Name);

                    writer.Write(Age);

                    writer.Write(Address);

                    byte[] uncompressedBuf = s.GetBuffer();

                    int uncompressedLength = (int)s.Length;

                    // truncate the stream to write the new compressed data

                    s.SetLength(0);

                    using (DeflateStream zipStream = new DeflateStream(s, CompressionMode.Compress, true)) {

                        // compress the serialized bytes

                        zipStream.Write(uncompressedBuf, 0, uncompressedLength);

                    }

                    buf = s.GetBuffer();

                    bufLength = (int)s.Length;

                }

            }

        }

        internal static Person DecompressAndDeserialize(byte[] buf) {

            using (MemoryStream stream = new MemoryStream(buf)) {

                // first decompress, then deserialize the bytes

                using (DeflateStream zipStream = new DeflateStream(stream, CompressionMode.Decompress, true)) {

                    return Person.Deserialize(zipStream);

                }

            }

        }

So, the important thing to remember: apply stream adapters on top of stream decorators and backing store streams. Chaining the streams can accomplish all stream operations your heart desires.