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.Compression namespace).

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

·         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.