Chapter 11. Files and Streams

At some stage in your development cycle, you need to store data on some persistent media so that when the computer is restarted the data is still be available. In most cases, you either store the data in a database or in files. A file is basically a sequence of characters stored on storage media such as your hard disks, thumb drives, and so on. When you talk about files, you need to understand another associated term — streams. A stream is a channel in which data is passed from one point to another. In .NET, streams are divided into various types: file streams for files held on permanent storage, network streams for data transferred across the network, memory streams for data stored in internal storage, and so forth.

With streams, you can perform a wide range of tasks, including compressing and decompressing data, serializing and deserializing data, and encrypting and decrypting data. This chapter examines:

  • Manipulating files and directories

  • How to quickly read and write data to files

  • The concepts of streams

  • Using the BufferedStream class to improve the performance of applications reading from a stream

  • Using the FileStream class to read and write to files

  • Using the MemoryStream class to use the internal memory store as a buffer

  • Using the NetworkStream class for network programming

  • The various types of cryptographic classes available in .NET

  • Performing compressions and decompression on streams

  • Serializing and deserializing objects into binary and XML data

Working with Files and Directories

The System.IO namespace in the .NET Framework contains a wealth of classes that allow synchronous and asynchronous reading and writing of data on streams and files. In the following sections, you will explore the various classes for dealing with files and directories.

Note

Remember to import the System.IO namespace when using the various classes in the System.IO namespace.

Working with Directories

The .NET Framework class library provides two classes for manipulating directories:

  • DirectoryInfo class

  • Directory class

The DirectoryInfo class exposes instance methods for dealing with directories while the Directory class exposes static methods.

DirectoryInfo Class

The DirectoryInfo class provides various instance methods and properties for creating, deleting, and manipulating directories. The following table describes some of the common methods you can use to programmatically manipulate directories.

Method

Description

Create

Creates a directory.

CreateSubdirectory

Creates a subdirectory.

Delete

Deletes a directory.

GetDirectories

Gets the subdirectories of the current directory.

GetFiles

Gets the file list from a directory.

And here are some of the common properties:

Properties

Description

Exists

Indicates if a directory exists.

Parent

Gets the parent of the current directory.

FullName

Gets the full path name of the directory.

CreationTime

Gets or sets the creation time of current directory.

Refer to the MSDN documentation for a full list of methods and properties.

To see how to use the DirectoryInfo class, consider the following example:

static void Main(string[] args)
        {
            string path = @"C:My Folder";
            DirectoryInfo di = new DirectoryInfo(path);

            try
            {
                //---if directory does not exists---
                if (!di.Exists)
                {
                    //---create the directory---
                    di.Create();  //---c:My Folder---

                    //---creates subdirectories---
                    di.CreateSubdirectory("Subdir1"); //---c:My FolderSubdir1---
                    di.CreateSubdirectory("Subdir2"); //---c:My FolderSubdir2---
                }

                //---print out some info about the directory---
                Console.WriteLine(di.FullName);
                Console.WriteLine(di.CreationTime);

                //---get and print all the subdirectories---
                DirectoryInfo[] subDirs = di.GetDirectories();
                foreach (DirectoryInfo subDir in subDirs)
                    Console.WriteLine(subDir.FullName);

                //---get the parent of C:My folder---
                DirectoryInfo parent = di.Parent;
                if (parent.Exists)
                {
                    //---prints out C:---
                    Console.WriteLine(parent.FullName);
                }

                //---creates C:My FolderSubdir3---
                DirectoryInfo newlyCreatedFolder =
                    di.CreateSubdirectory("Subdir3");

                //---deletes C:My FolderSubdir3---
                newlyCreatedFolder.Delete();
            }
            catch (IOException ex)
            {
                Console.WriteLine(ex.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            Console.ReadLine();
        }

In this example, you first create an instance of the DirectoryInfo class by instantiating it with a path (C:My Folder). You check if the path exists by using the Exist property. If it does not exist, you create the folder (C:My Folder) and then create two subdirectories underneath it (Subdir1 and Subdir2).

Next, you print out the full pathname (using the FullName property) of the folder and its creation date (using the CreationTime property). You then get all the subdirectories under C:My Folder and display their full pathnames. You can get the parent of the C:My Folder using the Parent property.

Finally, you create a subdirectory named Subdir3 under C:My Folder and pass a reference to the newly created subdirectory to the newlyCreatedFolder object. You then delete the folder, using the Delete() method.

Directory Class

The Directory class is similar to DirectoryInfo class. The key difference between is that Directory exposes static members instead of instance members. The Directory class also exposes only methods — no properties. Some of the commonly used methods are described in the following table.

Method

Description

CreateDirectory

Creates a subdirectory.

Delete

Deletes a specified directory.

Exists

Indicates if a specified path exists.

GetCurrentDirectory

Gets the current working directory.

GetDirectories

Gets the subdirectories of the specified path.

GetFiles

Gets the file list from a specified directory.

SetCurrentDirectory

Sets the current working directory.

Refer to the MSDN documentation for a full list of methods and properties.

Here's the previous program using the DirectoryInfo class rewritten to use the Directory class:

static void Main(string[] args)
        {
            string path = @"C:My Folder";
            try
            {
                //---if directory does not exists---
                if (!Directory.Exists(path))
                {
                    //---create the directory---
                    Directory.CreateDirectory(path);

                    //---set the current directory to C:My Folder---
                    Directory.SetCurrentDirectory(path);
//---creates subdirectories---
                    //---c:My FolderSubdir1---
                    Directory.CreateDirectory("Subdir1");
                    //---c:My FolderSubdir2---
                    Directory.CreateDirectory("Subdir2");
                }

                //---set the current directory to C:My Folder---
                Directory.SetCurrentDirectory(path);

                //---print out some info about the directory---
                Console.WriteLine(Directory.GetCurrentDirectory());
                Console.WriteLine(Directory.GetCreationTime(path));

                //---get and print all the subdirectories---
                string[] subDirs = Directory.GetDirectories(path);
                foreach (string subDir in subDirs)
                    Console.WriteLine(subDir);

                //---get the parent of C:My folder---
                DirectoryInfo parent = Directory.GetParent(path);
                if (parent.Exists)
                {
                    //---prints out C:---
                    Console.WriteLine(parent.FullName);
                }

                //---creates C:My FolderSubdir3---
                Directory.CreateDirectory("Subdir3");

                //---deletes C:My FolderSubdir3---
                Directory.Delete("Subdir3");
            }
            catch (IOException ex)
            {
                Console.WriteLine(ex.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadLine();
        }

As you can see, most of the methods in the Directory class require you to specify the directory you are working with. If you like to specify the directory path by using relative path names, you need to set the current working directory using the SetCurrentDirectory() method; if not, the default current directory is always the location of your program. Also, notice that some methods (such as GetParent()) still return DirectoryInfo objects.

In general, if you are performing a lot of operations with directories, use the DirectoryInfo class. Once it is instantiated, the object has detailed information about the directory you are currently working on. In contrast, the Directory class is much simpler and is suitable if you are occasionally dealing with directories.

Working with Files Using the File and FileInfo Classes

The .NET Framework class library contains two similar classes for dealing with files — FileInfo and File.

The File class provides static methods for creating, deleting, and manipulating files, whereas the FileInfo class exposes instance members for files manipulation.

Like the Directory class, the File class only exposes static methods and does not contain any properties.

Consider the following program, which creates, deletes, copies, renames, and sets attributes in files, using the File class:

static void Main(string[] args)
        {
            string filePath = @"C:	emp	extfile.txt";
            string fileCopyPath = @"C:	emp	extfile_copy.txt";
            string newFileName = @"C:	emp	extfile_newcopy.txt";

            try
            {
                //---if file already existed---
                if (File.Exists(filePath))
                {
                    //---delete the file---
                    File.Delete(filePath);
                }

                //---create the file again---
                FileStream fs = File.Create(filePath);
                fs.Close();

                //---make a copy of the file---
                File.Copy(filePath, fileCopyPath);

                //--rename the file---
                File.Move(fileCopyPath, newFileName);

                //---display the creation time---
                Console.WriteLine(File.GetCreationTime(newFileName));

                //---make the file read-only and hidden---
                File.SetAttributes(newFileName, FileAttributes.ReadOnly);
                File.SetAttributes(newFileName, FileAttributes.Hidden);
            }
            catch (IOException ex)
            {
                Console.WriteLine(ex.Message);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadLine();
        }

This program first checks to see if a file exists by using the Exists() method. If the file exists, the program deletes it using the Delete() method. It then proceeds to create the file by using the Create() method, which returns a FileStream object (more on this in subsequent sections). To make a copy of the file, you use the Copy() method. The Move() method moves a file from one location to another. Essentially, you can use the Move() method to rename a file. Finally, the program sets the ReadOnly and Hidden attribute to the newly copied file.

In addition to the File class, you have the FileInfo class that provides instance members for dealing with files. Once you have created an instance of the FileInfo class, you can use its members to obtain more information about a particular file. Figure 11-1 shows the different methods and properties exposed by an instance of the FileInfo class, such as the Attributes property, which retrieves the attributes of a file, the Delete() method that allows you to delete a file, and so on.

Figure 11-1

Figure 11.1. Figure 11-1

Reading and Writing to Files

The File class contains four methods to write content to a file:

  • WriteAllText() — Creates a file, writes a string to it, and closes the file

  • AppendAllText() — Appends a string to an existing file

  • WriteAllLines() — Creates a file, writes an array of string to it, and closes the file

  • WriteAllBytes() — Creates a file, writes an array of byte to it, and closes the file

The following statements show how to use the various methods to write some content to a file:

string filePath = @"C:	emp	extfile.txt";
                string strTextToWrite = "This is a string";
                string[] strLinesToWrite = new string[] { "Line1", "Line2" };
                byte[] bytesToWrite =
                    ASCIIEncoding.ASCII.GetBytes("This is a string");

                File.WriteAllText(filePath, strTextToWrite);
                File.AppendAllText(filePath, strTextToWrite);
                File.WriteAllLines(filePath, strLinesToWrite);
                File.WriteAllBytes(filePath,bytesToWrite);

The File class also contains three methods to read contents from a file:

  • ReadAllText() — Opens a file, reads all text in it into a string, and closes the file

  • ReadAllLines() — Opens a file, reads all the text in it into a string array, and closes the file

  • ReadAllBytes() — Opens a file, reads all the content in it into a byte array, and closes the file

The following statements show how to use the various methods to read contents from a file:

string filePath = @"C:	emp	extfile.txt";
                string strTextToRead = (File.ReadAllText(filePath));
                string[] strLinestoRead = File.ReadAllLines(filePath);
                byte[] bytesToRead = File.ReadAllBytes(filePath);

The beauty of these methods is that you need not worry about opening and closing the file after reading or writing to it; they close the file automatically after they are done.

StreamReader and StreamWriter Classes

When dealing with text files, you may also want to use the StreamReader and StreamWriter classes. StreamReader is derived from the TextReader class, an abstract class that represents a reader that can read a sequential series of characters.

You'll see more about streams in the "The Stream Class" section later in this chapter.

The following code snippet uses the StreamReader class to read lines from a text file:

try
                {
                    using (StreamReader sr = new StreamReader(filePath))
                    {
                        string line;
                        while ((line = sr.ReadLine()) != null)
                        {
                            Console.WriteLine(line);
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }

In addition to the ReadLine() method, the StreamReader class supports the following methods:

  • Read() — Reads the next character from the input stream

  • ReadBlock() — Reads a maximum of specified characters

  • ReadToEnd() — Reads from the current position to the end of the stream

The StreamWriter class is derived from the abstract TextWriter class and is used for writing characters to a stream. The following code snippet uses the StreamWriter class to write lines to a text file:

try
                {
                    using (StreamWriter sw = new  StreamWriter(filePath))
                    {
                        sw.Write("Hello, ");
                        sw.WriteLine("World!");
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }

BinaryReader and BinaryWriter Classes

If you are dealing with binary files, you can use the BinaryReader and BinaryWriter classes. The following example reads binary data from one file and writes it into another, essentially making a copy of the file:

string filePath = @"C:	empVS2008Pro.png";
            string filePathCopy = @"C:	empVS2008Pro_copy.png";

            //---open files for reading and writing---
            FileStream fs1 = File.OpenRead(filePath);
            FileStream fs2 = File.OpenWrite(filePathCopy);

            BinaryReader br = new BinaryReader(fs1);
            BinaryWriter bw = new BinaryWriter(fs2);

            //---read and write individual bytes---
            for (int i = 0; i <= br.BaseStream.Length - 1; i++)
                bw.Write(br.ReadByte());

            //---close the reader and writer---
            br.Close();
            bw.Close();

This program first uses the File class to open two files — one for reading and one for writing. The BinaryReader class is then used to read the binary data from the FileStream, and the BinaryWriter is used to write the binary data to the file.

The BinaryReader class contains many different read methods for reading different types of data — Read(), Read7BitEncodedInt(), ReadBoolean(), ReadByte(), ReadBytes(), ReadChar(), ReadChars(), ReadDecimal(), ReadDouble(), ReadInt16(), ReadInt32(), ReadInt64(), ReadSByte(), ReadSingle(), ReadString(), ReadUInt16(), ReadUInt32(), and ReadUInt64().

Creating a FileExplorer

Now that you have seen how to use the various classes to manipulate files and directories, let's put them to good use by building a simple file explorer that displays all the subdirectories and files within a specified directory.

The following program contains the PrintFoldersinCurrentDirectory() function, which recursively traverses a directory's subdirectories and prints out its contents:

class Program
    {
        static string path = @"C:Program FilesMicrosoft Visual Studio 9.0VC#";
        static void Main(string[] args)
        {
            DirectoryInfo di = new DirectoryInfo(path);
            Console.WriteLine(di.FullName);
            PrintFoldersinCurrentDirectory(di, −1);
            Console.ReadLine();
        }

        private static void PrintFoldersinCurrentDirectory(
            DirectoryInfo directory, int level)
        {
            level++;

            //---print all the subdirectories in the current directory---
            foreach (DirectoryInfo subDir in directory.GetDirectories())
            {
                for (int i = 0; i <= level * 3; i++)
                    Console.Write(" ");
                Console.Write("|__");

                //---display subdirectory name---
                Console.WriteLine(subDir.Name);

                //---display all the files in the subdirectory---
                FileInfo[] files = subDir.GetFiles();
                foreach (FileInfo file in files)
                {
                    //---display the spaces---
                    for (int i = 0; i <= (level+1) * 3; i++)
                        Console.Write(" ");

                    //---display filename---
                    Console.WriteLine("* " + file.Name);
                }

                //---explore its subdirectories recursively---
                PrintFoldersinCurrentDirectory(subDir, level);
            }
        }
    }

Figure 11-2 shows the output of the program.

Figure 11-2

Figure 11.2. Figure 11-2

The Stream Class

A stream is an abstraction of a sequence of bytes. The bytes may come from a file, a TCP/IP socket, or memory. In .NET, a stream is represented, aptly, by the Stream class. The Stream class provides a generic view of a sequence of bytes.

The Stream class forms the base class of all other streams, and it is also implemented by the following classes:

  • BufferedStream — Provides a buffering layer on another stream to improve performance

  • FileStream — Provides a way to read and write files

  • MemoryStream — Provides a stream using memory as the backing store

  • NetworkStream — Provides a way to access data on the network

  • CryptoStream — Provides a way to supply data for cryptographic transformation

  • Streams fundamentally involve the following operations:

    • Reading

    • Writing

    • Seeking

Note

The Stream class is defined in the System.IO namespace. Remember to import that namespace when using the class.

The following code copies the content of one binary file and writes it into another using the Stream class:

try
                {
                    const int BUFFER_SIZE = 8192;
                    byte[] buffer = new byte[BUFFER_SIZE];
                    int bytesRead;

                    string filePath = @"C:	empVS2008Pro.png";
                    string filePath_backup = @"C:	empVS2008Pro_bak.png";

                    Stream s_in = File.OpenRead(filePath);
                    Stream s_out = File.OpenWrite(filePath_backup);

                    while ((bytesRead = s_in.Read(buffer, 0, BUFFER_SIZE)) > 0)
                    {
                        s_out.Write(buffer, 0, bytesRead);
                    }
                    s_in.Close();
                    s_out.Close();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }

This first opens a file for reading using the static OpenRead() method from the File class. In addition, it opens a file for writing using the static OpenWrite() method. Both methods return a FileStream object.

While the OpenRead() and OpenWrite() methods return a FileStream object, you can actually assign the returning type to a Stream object because the FileStream object inherits from the Stream object.

To copy the content of one file into another, you use the Read() method from the Stream class and read the content from the file into an byte array. Read() returns the number of bytes read from the stream (in this case the file) and returns 0 if there are no more bytes to read. The Write() method of the Stream class writes the data stored in the byte array into the stream (which in this case is another file). Finally, you close both the Stream objects.

In addition to the Read() and Write() methods, the Stream object supports the following methods:

  • ReadByte() — Reads a byte from the stream and advances the position within the stream by one byte, or returns −1 if at the end of the stream

  • WriteByte() — Writes a byte to the current position in the stream and advances the position within the stream by 1 byte

  • Seek() — Sets the position within the current stream

The following example writes some text to a text file, closes the file, reopens the file, seeks to the fourth position in the file, and reads the next six bytes:

try
      {
          const int BUFFER_SIZE = 8192;
          string text = "The Stream class is defined in the System.IO namespace.";
          byte[] data = ASCIIEncoding.ASCII.GetBytes(text);
          byte[] buffer = new byte[BUFFER_SIZE];
          string filePath = @"C:	emp	extfile.txt";

          //---writes some text to file---
          Stream s_out = File.OpenWrite(filePath);
          s_out.Write(data, 0, data.Length);
          s_out.Close();

          //---opens the file for reading---
          Stream s_in = File.OpenRead(filePath);

          //---seek to the fourth position---
          s_in.Seek(4, SeekOrigin.Begin);

          //---read the next 6 bytes---
          int bytesRead = s_in.Read(buffer, 0, 6);
          Console.WriteLine(ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead));

          s_in.Close();
          s_out.Close();
      }
      catch (Exception ex)
      {
          Console.WriteLine(ex.ToString());
      }

BufferedStream

To improve its performance, the BufferedStream class works with another Stream object. For instance, the previous example used a buffer size of 8192 bytes when reading from a text file. However, that size might not be the ideal size to yield the optimum performance from your computer. You can use the BufferedStream class to let the operating system determine the optimum buffer size for you. While you can still specify the buffer size to fill up your buffer when reading data, your buffer will now be filled by the BufferedStream class instead of directly from the stream (which in the example is from a file). The BufferedStream class fills up its internal memory store in the size that it determines is the most efficient.

The BufferedStream class is ideal when you are manipulating large streams. The following shows how the previous example can be speeded up using the BufferedStream class:

try
                {
                    const int BUFFER_SIZE = 8192;
                    byte[] buffer = new byte[BUFFER_SIZE];
                    int bytesRead;

                    string filePath = @"C:	empVS2008Pro.png";
                    string filePath_backup = @"C:	empVS2008Pro_bak.png";

                    Stream s_in = File.OpenRead(filePath);
                    Stream s_out = File.OpenWrite(filePath_backup);

                    BufferedStream bs_in = new BufferedStream(s_in);
                    BufferedStream bs_out = new BufferedStream(s_out);

                    while ((bytesRead = bs_in.Read(buffer, 0, BUFFER_SIZE)) > 0)
                    {
                        bs_out.Write(buffer, 0, bytesRead);
                    }
                    bs_out.Flush();
                    bs_in.Close();
                    bs_out.Close();

                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }

You use a BufferedStream object over a Stream object, and all the reading and writing is then done via the BufferedStream objects.

The FileStream Class

The FileStream class is designed to work with files, and it supports both synchronous and asynchronous read and write operations. Earlier, you saw the use of the Stream object to read and write to file. Here is the same example using the FileStream class:

try
                {
                    const int BUFFER_SIZE = 8192;
                    byte[] buffer = new byte[BUFFER_SIZE];
                    int bytesRead;

                    string filePath = @"C:	empVS2008Pro.png";
                    string filePath_backup = @"C:	empVS2008Pro_bak.png";

                    FileStream fs_in = File.OpenRead(filePath);
                    FileStream fs_out = File.OpenWrite(filePath_backup);
while ((bytesRead = fs_in.Read(buffer, 0, BUFFER_SIZE)) > 0)
                    {
                        fs_out.Write(buffer, 0, bytesRead);
                    }

                    fs_in.Dispose();
                    fs_out.Dispose();
                    fs_in.Close();
                    fs_out.Close();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }

If the size of the file is large, this program will take a long time because it uses the blocking Read() method. A better approach would be to use the asynchronous read methods BeginRead() and EndRead().

BeginRead() starts an asynchronous read from a FileStream object. Every BeginRead() method called must be paired with the EndRead() method, which waits for the pending asynchronous read operation to complete. To read from the stream synchronously, you call the BeginRead() method as usual by providing it with the buffer to read, the offset to begin reading, size of buffer, and a call back delegate to invoke when the read operation is completed. You can also provide a custom object to distinguish different asynchronous operations (for simplicity you just pass in null here):

IAsyncResult result =
                    fs_in.BeginRead(buffer, 0, BUFFER_SIZE,
                    new AsyncCallback(readCompleted), null);

The following program shows how you can copy the content of a file into another asynchronously:

class Program
    {
        static FileStream fs_in;
        static FileStream fs_out;
        const int BUFFER_SIZE = 8192;
        static byte[] buffer = new byte[BUFFER_SIZE];

        static void Main(string[] args)
        {
            try
            {
                string filePath = @"C:	empVS2008Pro.png";
                string filePath_backup = @"C:	empVS2008Pro_bak.png";

                //---open the files for reading and writing---
                fs_in = File.OpenRead(filePath);
                fs_out = File.OpenWrite(filePath_backup);

                Console.WriteLine("Copying file...");
//---begin to read asynchronously---
                IAsyncResult result =
                    fs_in.BeginRead(buffer, 0, BUFFER_SIZE,
                    new AsyncCallback(readCompleted), null);

                //---continue with the execution---
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("Continuing with the execution...{0}", i);
                    System.Threading.Thread.Sleep(250);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            Console.ReadLine();
        }

        //---when a block of data is read---
        static void readCompleted(IAsyncResult result)
        {
            //---simulate slow reading---
            System.Threading.Thread.Sleep(500);

            //---reads the data---
            int bytesRead = fs_in.EndRead(result);

            //---writes to another file---
            fs_out.Write(buffer, 0, bytesRead);

            if (bytesRead > 0)
            {
                //---continue reading---
                result =
                   fs_in.BeginRead(buffer, 0, BUFFER_SIZE,
                   new AsyncCallback(readCompleted), null);
            }
            else
            {
                //---reading is done!---
                fs_in.Dispose();
                fs_out.Dispose();
                fs_in.Close();
                fs_out.Close();
                Console.WriteLine("File copy done!");
            }
        }
    }

Because the reading may happen so fast for a small file, you can insert Sleep() statements to simulate reading a large file. Figure 11-3 shows the output.

Figure 11-3

Figure 11.3. Figure 11-3

MemoryStream

Sometimes you need to manipulate data in memory without resorting to saving it in a file. A good example is the PictureBox control in a Windows Form. For instance, you have a picture displayed in the PictureBox control and want to send the picture to a remote server, say a Web Service. The PictureBox control has a Save() method that enables you to save the image to a Stream object.

Instead of saving the image to a FileStream object and then reloading the data from the file into a byte array, a much better way would be to use a MemoryStream object, which uses the memory as a backing store (which is more efficient compared to performing file I/O; file I/O is relatively slower).

The following code shows how the image in the PictureBox control is saved into a MemoryStream object:

//---create a MemoryStream object---
            MemoryStream ms1 = new MemoryStream();

            //---save the image into a MemoryStream object---
            pictureBox1.Image.Save(ms1, System.Drawing.Imaging.ImageFormat.Jpeg);

To extract the image stored in the MemoryStream object and save it to a byte array, use the Read() method of the MemoryStream object:

//---read the data in ms1 and write to buffer---
            ms1.Position = 0;
            byte[] buffer = new byte[ms1.Length];
            int bytesRead = ms1.Read(buffer, 0, (int)ms1.Length);

With the data in the byte array, you can now proceed to send the data to the Web Service. To verify that the data stored in the byte array is really the image in the PictureBox control, you can load it back to another MemoryStream object and then display it in another PictureBox control, like this:

//---read the data in buffer and write to ms2---
            MemoryStream ms2 = new MemoryStream();
            ms2.Write(buffer,0,bytesRead);

            //---load it in another PictureBox control---
            pictureBox2.Image = new Bitmap(ms2);

NetworkStream Class

The NetworkStream class provides methods for sending and receiving data over Stream sockets in blocking mode. Using the NetworkStream class is more restrictive than using most other Stream implementations. For example, the CanSeek() properties of the NetworkStream class are not supported and always return false. Similarly, the Length() and Position() properties throw NotSupportedException. It is not possible to perform a Seek() operation, and the SetLength() method also throws NotSupportedException.

Despite these limitations, the NetworkStream class has made network programming very easy and encapsulates much of the complexity of socket programming. Developers who are familiar with streams programming can use the NetworkStream class with ease.

This section leads you through creating a pair of socket applications to illustrate how the NetworkStream class works. The server will listen for incoming TCP clients and send back to the client whatever it receives.

Building a Client-Server Application

The following code is for the server application:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace Server
{
    class Program
    {
        const int PORT_NO = 5000;
        const string SERVER_IP = "127.0.0.1";

        static void Main(string[] args)
        {
            //---listen at the specified IP and port no.---
            IPAddress localAdd = IPAddress.Parse(SERVER_IP);
            TcpListener listener = new TcpListener(localAdd, PORT_NO);
            Console.WriteLine("Listening...");
listener.Start();

            //---incoming client connected---
            TcpClient client = listener.AcceptTcpClient();

            //---get the incoming data through a network stream---
            NetworkStream nwStream = client.GetStream();
            byte[] buffer = new byte[client.ReceiveBufferSize];

            //---read incoming stream---
            int bytesRead = nwStream.Read(buffer, 0, client.ReceiveBufferSize);

            //---convert the data received into a string---
            string dataReceived = Encoding.ASCII.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received : " + dataReceived);

            //---write back the text to the client---
            Console.WriteLine("Sending back : " + dataReceived);
            nwStream.Write(buffer, 0, bytesRead);

            client.Close();
            listener.Stop();
            Console.ReadLine();
        }
    }
}

Basically, you use the TcpListener class to listen for an incoming TCP connection. Once a connection is made, you use a NetworkStream object to read data from the client, using the Read() method as well as write data to the client by using the Write() method.

For the client, you use the TcpClient class to connect to the server using TCP and, as with the server, you use the NetworkStream object to write and read data to and from the client:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace Client
{
    class Program
    {
        const int PORT_NO = 5000;
        const string SERVER_IP = "127.0.0.1";

        static void Main(string[] args)
        {
            //---data to send to the server---
            string textToSend = DateTime.Now.ToString();

            //---create a TCPClient object at the IP and port no.---
TcpClient client = new TcpClient(SERVER_IP, PORT_NO);

            NetworkStream nwStream = client.GetStream();
            byte[] bytesToSend = ASCIIEncoding.ASCII.GetBytes(textToSend);

            //---send the text---
            Console.WriteLine("Sending : " + textToSend);
            nwStream.Write(bytesToSend, 0, bytesToSend.Length);

            //---read back the text---
            byte[] bytesToRead = new byte[client.ReceiveBufferSize];
            int bytesRead = nwStream.Read(bytesToRead, 0,
                client.ReceiveBufferSize);

            Console.WriteLine("Received : " +
                Encoding.ASCII.GetString(bytesToRead, 0, bytesRead));
            Console.ReadLine();

            client.Close();
        }
    }
}

Figure 11-4 shows how the server and client look like when you run both applications.

Figure 11-4

Figure 11.4. Figure 11-4

Building a Multi-User Server Application

The client-server applications built in the previous section can accept only a single client. A client connects and sends some data to the server; the server receives it, sends the data back to the client, and then exits. While this is a simple demonstration of a client-server application, it isn't a very practical application because typically a server should be able to handle multiple clients simultaneously and runs indefinitely. So let's look at how you can extend the previous server so that it can handle multiple clients simultaneously.

To do so, you can create a class named Client and code it as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;

namespace Server
{
    class Client
    {
        //---create a TCPClient object---
        TcpClient _client = null;

        //---for sending/receiving data---
        byte[] buffer;

        //---called when a client has connected---
        public Client(TcpClient client)
        {
            _client = client;
            //---start reading data asynchronously from the client---
            buffer = new byte[_client.ReceiveBufferSize];
            _client.GetStream().BeginRead(
                buffer, 0, _client.ReceiveBufferSize,
                receiveMessage, null);
        }

        public void receiveMessage(IAsyncResult ar)
        {
            int bytesRead;
            try
            {
                lock (_client.GetStream())
                {
                    //---read from client---
                    bytesRead = _client.GetStream().EndRead(ar);
                }

                //---if client has disconnected---
                if (bytesRead < 1)
                    return;
                else
                {
                    //---get the message sent---
                    string messageReceived =
                        ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead);
                    Console.WriteLine("Received : " + messageReceived);
//---write back the text to the client---
                    Console.WriteLine("Sending back : " + messageReceived);
                    byte[] dataToSend =
                        ASCIIEncoding.ASCII.GetBytes(messageReceived);
                    _client.GetStream().Write(dataToSend, 0, dataToSend.Length);
                }

                //---continue reading from client---
                lock (_client.GetStream())
                {
                    _client.GetStream().BeginRead(
                        buffer, 0, _client.ReceiveBufferSize,
                        receiveMessage, null);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
}

Here, the constructor of the Client class takes in a TcpClient object and starts to read from it asynchronously using the receiveMessage() method (via the BeginRead() method of the NetworkStream object). Once the incoming data is read, the constructor continues to wait for more data.

To ensure that the server supports multiple users, you use a TcpListener class to listen for incoming client connections and then use an infinite loop to accept new connections. Once a client is connected, you create a new instance of the Client object and continue waiting for the next client. So the Main() function of your application now looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace Server
{
    class Program
    {
        const int PORT_NO = 5000;
        const string SERVER_IP = "127.0.0.1";

        static void Main(string[] args)
        {
            //---listen at the specified IP and port no.---
            IPAddress localAddress = IPAddress.Parse(SERVER_IP);
            TcpListener listener = new TcpListener(localAddress, PORT_NO);
            Console.WriteLine("Listening...");
            listener.Start();
while (true)
            {
                //---incoming client connected---
                Client user = new Client(listener.AcceptTcpClient());
            }
        }
    }
}

Figure 11-5 shows the server with two clients connected to it.

Figure 11-5

Figure 11.5. Figure 11-5

Cryptography

The .NET framework contains a number of cryptography services that enable you to incorporate security services into your .NET applications. These libraries are located under the System.Security.Cryptography namespace and provide various functions such as encryption and decryption of data, as well as other operations such as hashing and random-number generation. One of the core classes that support the cryptographic services is the CryptoStream class, which links data streams to cryptographic transformations.

This section explores how to use some of the common security APIs to make your .NET applications more secure.

Hashing

The most common security function that you will perform is hashing. Consider the situation where you need to build a function to authenticate users before they can use your application. You would require the user to supply a set of login credentials, generally containing a user name and a password. This login information needs to be persisted to a database. Quite commonly, developers store the passwords of users verbatim on a database. That's a big security risk because hackers who get a chance to glance at the users' database would be able to obtain the passwords of your users. A better approach is to store the hash values of the users' passwords instead of the passwords themselves. A hashing algorithm has the following properties:

  • It maps a string of arbitrary length to small binary values of a fixed length, known as a hash value.

  • The hash value of a string is unique, and small changes in the original string will produce a different hash value.

  • It is improbable that you'd find two different strings that produce the same hash value.

  • It is impossible to use the hash value to find the original string.

Then, when the user logs in to your application, the hash value of the password provided is compared with the hash value stored in the database. In this way, even if hackers actually steal the users' database, the actual password is not exposed. One downside to storing the hash values of users' passwords is that in the event that a user loses her password, there is no way to retrieve it. You'd need to generate a new password for the user and request that she change it immediately. But this inconvenience is a small price to pay for the security of your application.

There are many hashing algorithms available in .NET, but the most commonly used are the SHA1 and MD5 implementations. Let's take a look at how they work in .NET.

Using Visual Studio 2008, create a new Console application project. Import the following namespaces:

using System.IO;
using System.Security.Cryptography;

Define the following function:

static void Hashing_SHA1()
        {
            //---ask the user to enter a password---
            Console.Write("Please enter a password: ");
            string password = Console.ReadLine();

            //---hash the password---
            byte[] data = ASCIIEncoding.ASCII.GetBytes(password);
            byte[] passwordHash;
            SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
            passwordHash = sha.ComputeHash(data);

            //---ask the user to enter the same password again---
            Console.Write("Please enter password again: ");
            password = Console.ReadLine();
//---hash the second password and compare it with the first---
            data = System.Text.Encoding.ASCII.GetBytes(password);

            if (ASCIIEncoding.ASCII.GetString(passwordHash) ==
               ASCIIEncoding.ASCII.GetString(sha.ComputeHash(data)))
                Console.WriteLine("Same password");
            else
                Console.WriteLine("Incorrect password");
        }

You first ask the user to enter a password, after which you will hash it using the SHA1 implementation. You then ask the user to enter the same password again. To verify that the second password matches the first, you hash the second password and then compare the two hash values. For the SHA1 implementation, the hash value generated is 160 bits in length (the byte array passwordHash has 20 members: 8 bits × 20 = 160 bits). In this example, you convert the hash values into strings and perform a comparison. You could also convert them to Base64 encoding and then perform a comparison. Alternatively, you can also evaluate the two hash values by using their byte arrays, comparing them byte by byte. As soon as one byte is different, you can conclude that the two hash values are not the same.

To test the function, simply call the Hashing_SHA1() function in Main():

static void Main(string[] args)
        {
            Hashing_SHA1();
            Console.Read();
        }

Figure 11-6 shows the program in action.

Figure 11-6

Figure 11.6. Figure 11-6

You can also use the MD5 implementation to perform hashing, as the following function shows:

static void Hashing_SHA1()
        {
            //---ask the user to enter a password---
            Console.Write("Please enter a password: ");
            string password = Console.ReadLine();

            //---hash the password---
            byte[] data = ASCIIEncoding.ASCII.GetBytes(password);
            byte[] passwordHash;
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
            passwordHash = md5.ComputeHash(data);

            //---ask the user to enter the same password again---
            Console.Write("Please enter password again: ");
            password = Console.ReadLine();

            //---hash the second password and compare it with the first---
            data = System.Text.Encoding.ASCII.GetBytes(password);

            if (ASCIIEncoding.ASCII.GetString(passwordHash) ==
               ASCIIEncoding.ASCII.GetString(md5.ComputeHash(data)))
                Console.WriteLine("Same password");
            else
                Console.WriteLine("Incorrect password");
        }

The main difference is that the hash value for MD5 is 128 bits in length.

Salted Hash

With hashing, you simply store the hash value of a user's password in the database. However, if two users use identical passwords, the hash values for these two passwords will be also identical. Imagine a hacker seeing that the two hash values are identical; it would not be hard for him to guess that the two passwords must be the same. For example, users often like to use their own names or birth dates or common words found in the dictionary as passwords. So, hackers often like to use dictionary attacks to correctly guess users' passwords. To reduce the chance of dictionary attacks, you can add a "salt" to the hashing process so that no two identical passwords can generate the same hash values. For instance, instead of hashing a user's password, you hash his password together with his other information, such as email address, birth date, last name, first name, and so on. The idea is to ensure that each user will have a unique password hash value. While the idea of using the user's information as a salt for the hashing process sounds good, it is quite easy for hackers to guess. A better approach is to randomly generate a number to be used as the salt and then hash it together with the user's password.

The following function, Salted_Hashing_SHA1(), generates a random number using the RNGCryptoServiceProvider class, which returns a list of randomly generated bytes (the salt). It then combines the salt with the original password and performs a hash on it.

static void Salted_Hashing_SHA1()
        {
            //---Random Number Generator---
            byte[] salt = new byte[8];

            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            rng.GetBytes(salt);

            //---ask the user to enter a password---
            Console.Write("Please enter a password: ");
            string password = Console.ReadLine();
//---add the salt to the password---
            password += ASCIIEncoding.ASCII.GetString(salt);

            //---hash the password---
            byte[] data = ASCIIEncoding.ASCII.GetBytes(password);
            SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
            byte[] passwordHash;
            passwordHash = sha.ComputeHash(data);

            //---ask the user to enter the same password again---
            Console.Write("Please enter password again: ");
            password = Console.ReadLine();
            Console.WriteLine(ASCIIEncoding.ASCII.GetString(salt));

            //---adding the salt to the second password---
            password += ASCIIEncoding.ASCII.GetString(salt);

            //---hash the second password and compare it with the first---
            data = ASCIIEncoding.ASCII.GetBytes(password);
            if (ASCIIEncoding.ASCII.GetString(passwordHash) ==
               ASCIIEncoding.ASCII.GetString(sha.ComputeHash(data)))
                Console.WriteLine("Same password");
            else
                Console.WriteLine("Incorrect password");
        }

If you use salted hash for storing passwords, the salt used for each password should be stored separately from the main hash database so that hackers do not have a chance to obtain it easily.

Encryption and Decryption

Hashing is a one-way process, which means that once a value is hashed, you can't obtain its original value by reversing the process. This characteristic is particularly well suited for authentications as well as digitally signing a document.

In reality, there are many situations that require information to be performed in a two-way process. For example, to send a secret message to a recipient, you need to "scramble" it so that only the recipient can see it. This process of scrambling is known as encryption. Undoing the scrambling process to obtain the original message is known as decryption. There are two main types of encryption: symmetric and asymmetric.

Symmetric Encryption

Symmetric encryption is also sometimes known as private key encryption. You encrypt a secret message using a key that only you know. To decrypt the message, you need to use the same key. Private key encryption is effective only if the key can be kept a secret. If too many people know the key, its effectiveness is reduced, and if the key's secrecy is compromised somehow, then the message is no longer secure.

Despite the potential weakness of private key encryption, it is very easy to implement and, computationally, it does not take up too many resources.

For private key encryption (symmetric), the .NET Framework supports the DES, RC2, Rijndael, and TripleDES algorithms.

To see how symmetric encryption works, you will use the RijndaelManaged class in the following SymmetricEncryption() function. Three parameters are required — the string to be encrypted, the private key, and the initialization vector (IV). The IV is a random number used in the encryption process to ensure that no two strings will give the same cipher text (the encrypted text) after the encryption process. You will need the same IV later on when decrypting the cipher text.

To perform the actual encryption, you initialize an instance of the CryptoStream class with a MemoryStream object, the cryptographic transformation to perform on the stream, and the mode of the stream (Write for encryption and Read for decryption):

static string SymmetricEncryption(string str, byte[] key, byte[] IV)
        {
            MemoryStream ms = new MemoryStream();
            try
            {
                //---creates a new instance of the RijndaelManaged class---
                RijndaelManaged RMCrypto = new RijndaelManaged();

                //---creates a new instance of the CryptoStream class---
                CryptoStream cryptStream =
                    new CryptoStream(
                    ms, RMCrypto.CreateEncryptor(key, IV),
                    CryptoStreamMode.Write);

                StreamWriter sWriter = new StreamWriter(cryptStream);

                //---encrypting the string---
                sWriter.Write(str);
                sWriter.Close();
                cryptStream.Close();

                //---return the encrypted data as a string---
                return System.Convert.ToBase64String(ms.ToArray());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return (String.Empty);
            }
        }

The encrypted string is returned as a Base64-encoded string. You can check the allowable key sizes for the RijndaelManaged class by using the following code:

KeySizes[] ks;
            RijndaelManaged RMCrypto = new RijndaelManaged();
            ks = RMCrypto.LegalKeySizes;

            //---print out the various key sizes---
            Console.WriteLine(ks[0].MaxSize);   // 256
            Console.WriteLine(ks[0].MinSize);   // 128
            Console.WriteLine(ks[0].SkipSize);  //  64

The valid key sizes are: 16 bytes (128 bit), 24 bytes (128 bits + 64 bits), and 32 bytes (256 bits).

You can get the system to generate a random key and IV (which you need to supply in the current example) automatically:

//---generate key---
            RMCrypto.GenerateKey();
            byte[] key = RMCrypto.Key;
            Console.WriteLine("Key : " + System.Convert.ToBase64String(key));

            //---generate IV---
            RMCrypto.GenerateIV();
            byte[] IV = RMCrypto.IV;
            Console.WriteLine("IV : " + System.Convert.ToBase64String(IV));

If the IV is null when it is used, the GenerateIV() method is called automatically. Valid size for the IV is 16 bytes.

To decrypt a string encrypted using the RijndaelManaged class, you can use the following SymmetricDecryption() function:

static string SymmetricDecryption(string str, byte[] key, byte[] IV)
        {
            try
            {
                //---converts the encrypted string into a byte array---
                byte[] b = System.Convert.FromBase64String(str);

                //---converts the byte array into a memory stream for decryption---
                MemoryStream ms = new MemoryStream(b);

                //---creates a new instance of the RijndaelManaged class---
                RijndaelManaged RMCrypto = new RijndaelManaged();

                //---creates a new instance of the CryptoStream class---
                CryptoStream cryptStream =
                    new CryptoStream(
                        ms, RMCrypto.CreateDecryptor(key, IV),
                        CryptoStreamMode.Read);

                //---decrypting the stream---
                StreamReader sReader = new StreamReader(cryptStream);

                //---converts the decrypted stream into a string---
                String s = sReader.ReadToEnd();
                sReader.Close();

                return s;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return String.Empty;
            }
        }

The following code snippet shows how to use the SymmetricEncryption() and SymmetricDecryption() functions to encrypt and decrypt a string:

RijndaelManaged RMCrypto = new RijndaelManaged();

            //---generate key---
            RMCrypto.GenerateKey();
            byte[] key = RMCrypto.Key;
            Console.WriteLine("Key : " + System.Convert.ToBase64String(key));

            //---generate IV---
            RMCrypto.GenerateIV();
            byte[] IV = RMCrypto.IV;
            Console.WriteLine("IV : " + System.Convert.ToBase64String(IV));

            //---encrypt the string---
            string cipherText =
               SymmetricEncryption("This is a test string.", key, IV);
            Console.WriteLine("Ciphertext: " + cipherText);

            //---decrypt the string---
            Console.WriteLine("Original string: " +
               SymmetricDecryption(cipherText, key, IV));

Figure 11-7 shows the output.

Figure 11-7

Figure 11.7. Figure 11-7

Asymmetric Encryption

Private key encryption requires the key used in the encryption process to be kept a secret. A more effective way to transport secret messages to your intended recipient is to use asymmetric encryption (also known as public key encryption), which involves a pair of keys involved. This pair, consisting of a private key and a public key, is related mathematically such that messages encrypted with the public key can only be decrypted with the corresponding private key. The reverse is also true; messages encrypted with the private key can only be decrypted with the public key. Let's see an example for each scenario.

Before you send a message to your friend Susan, Susan needs to generate the key pair containing the private key and the public key. Susan then freely distributes the public key to you (and all her other friends) but keeps the private key to herself. When you want to send a message to Susan, you use her public key to encrypt the message. Upon receiving the encrypted message, Susan proceeds to decrypt it with her private key. Susan is the only one who can decrypt the message because the key pair works in such a way that only messages encrypted with the public key can be decrypted with the private key. And there is no need to exchange keys, thus eliminating the risk of compromising the secrecy of the key.

Now suppose that Susan sends a message encrypted with her private key to you. To decrypt the message, you need the public key. The scenario may seem odd because the public key is not a secret; everyone knows it. But using this method guarantees that the message has not been tampered with and confirms that it indeed comes from Susan. If the message had been modified, you would not be able to decrypt it. The fact that you can decrypt the message using the public key proves that the message has not been modified.

In computing, public key cryptography is a secure way to encrypt information, but it's computationally expensive because it is time-consuming to generate the key pairs and to perform encryption and decryption. Therefore, it's generally used only for encrypting a small amount of sensitive information.

For public key (asymmetric) encryptions, the .NET Framework supports the DSA and RSA algorithms. The RSA algorithm is used in the following AsymmetricEncryption() function. This function takes in two parameters: the string to be encrypted and the public key:

static string AsymmetricEncryption(string str, string publicKey)
        {
            try
            {
                //---Creates a new instance of RSACryptoServiceProvider---
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();

                //---Loads the public key---
                RSA.FromXmlString(publicKey);

                //---Encrypts the string---
                byte[] encryptedStr =
                    RSA.Encrypt(ASCIIEncoding.ASCII.GetBytes(str), false);

                //---Converts the encrypted byte array to string---
                return System.Convert.ToBase64String(encryptedStr);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return String.Empty;
            }
        }

The encrypted string is returned as a Base64-encoded string. To decrypt a string encrypted with the public key, define the following AsymmetricDecryption() function. It takes in two parameters (the encrypted string and the private key) and returns the decrypted string.

static string AsymmetricDecryption(string str, string privateKey)
        {
            try
            {
                //---Creates a new instance of RSACryptoServiceProvider---
                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();

                //---Loads the private key---
                RSA.FromXmlString(privateKey);
//---Decrypts the string---
                byte[] DecryptedStr =
                    RSA.Decrypt(System.Convert.FromBase64String(str), false);

                //---Converts the decrypted byte array to string---
                return ASCIIEncoding.ASCII.GetString(DecryptedStr);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return String.Empty;
            }
        }

The following code snippet shows how to use the AsymmetricEncryption() and AsymmetricDecryption() functions to encrypt and decrypt a string:

string publicKey, privateKey;
            RSACryptoServiceProvider RSA =
                new RSACryptoServiceProvider();

            //---get public key---
            publicKey = RSA.ToXmlString(false);
            Console.WriteLine("Public key: " + publicKey);
            Console.WriteLine();

            //---get private and public key---
            privateKey = RSA.ToXmlString(true);
            Console.WriteLine("Private key: " + privateKey);
            Console.WriteLine();

            //---encrypt the string---
            string cipherText =
               AsymmetricEncryption("C# 2008 Programmer's Reference", publicKey);
            Console.WriteLine("Ciphertext: " + cipherText);
            Console.WriteLine();

            //---decrypt the string---
            Console.WriteLine("Original string: " +
               AsymmetricDecryption(cipherText, privateKey));
            Console.WriteLine();

You can obtain the public and private keys generated by the RSA algorithm by using the ToXmlString() method from the RSACryptoServiceProvider class. This method takes in a Bool variable, and returns a public key if the value false is supplied. If the value true is supplied, it returns both the private and public keys.

Figure 11-8 shows the output.

Figure 11-8

Figure 11.8. Figure 11-8

Compressions for Stream Objects

The System.IO.Compression namespace contains classes that provide basic compression and decompression services for streams. This namespace contains two classes for data compression: DeflateStream and GZipStream. Both support lossless compression and decompression and are designed for dealing with streams.

Compression is useful for reducing the size of data. If you have huge amount of data to store in your SQL database, for instance, you can save on disk space if you compress the data before saving it into a table. Moreover, because you are now saving smaller blocks of data into your database, the time spent in performing disk I/O is significantly reduced. The downside of compression is that it takes additional processing power from your machine (and requires additional processing time), and you need to factor in this additional time before deciding you want to use compression in your application.

Compression is extremely useful in cases where you need to transmit data over networks, especially slow and costly networks such as General Packet Radio Service (GPRS).connections. In such cases, using compression can drastically cut down the data size and reduce the overall cost of communication. Web Services are another area where using compression can provide a great advantage because XML data can be highly compressed.

But once you've decided the performance cost is worth it, you'll need help deciphering the utilization of these two compression classes, which is what this section is about.

Compression

The compression classes read data (to be compressed) from a byte array, compress it, and store the results in a Stream object. For decompression, the compressed data stored in a Stream object is decompressed and then stored in another Stream object.

Let's see how you can perform compression. First, define the Compress() function, which takes in two parameters: algo and data. The first parameter specifies which algorithm to use (GZip or Deflate), and the second parameter is a byte array that contains the data to compress. A MemoryStream object will be used to store the compressed data. The compressed data stored in the MemoryStream is then copied into another byte array and returned to the calling function. The Compress() function is defined as follows:

static byte[] Compress(string algo, byte[] data)
        {
            try
            {
                //---the ms is used for storing the compressed data---
                MemoryStream ms = new MemoryStream();
                Stream zipStream = null;
                switch (algo)
                {
                    case "Gzip": zipStream =
                        new GZipStream(ms, CompressionMode.Compress, true);
                        break;
                    case "Deflat": zipStream =
                        new DeflateStream(ms, CompressionMode.Compress, true);
                        break;
                    default: return null;
                }

                //---compress the data stored in the data byte array---
                zipStream.Write(data, 0, data.Length);
                zipStream.Close();

                //---store the compressed data into a byte array---
                ms.Position = 0;
                byte[] c_data = new byte[ms.Length];

                //---read the content of the memory stream into the byte array---
                ms.Read(c_data, 0, (int)ms.Length);
                return c_data;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }

Decompression

The following Decompress() function decompresses the data compressed by the Compress() function. The first parameter specifies the algorithm to use, while the byte array containing the compressed data is passed in as the second parameter, which is then copied into a MemoryStream object.

static byte[] Decompress(string algo, byte[] data)
        {
            try
            {
                //---copy the data (compressed) into ms---
                MemoryStream ms = new MemoryStream(data);
Stream zipStream = null;
                //---decompressing using data stored in ms---

                switch (algo)
                {
                    case "Gzip": zipStream =
                        new GZipStream(ms, CompressionMode.Decompress, true);
                        break;
                    case "Deflat": zipStream =
                        new DeflateStream(ms, CompressionMode.Decompress, true);
                        break;
                    default: return null;
                }

                //---used to store the de-compressed data---
                byte[] dc_data;

                //---the de-compressed data is stored in zipStream;
                // extract them out into a byte array---
                dc_data = RetrieveBytesFromStream(zipStream, data.Length);

                return dc_data;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }

The compression classes then decompress the data stored in the memory stream and store the decompressed data into another Stream object. To obtain the decompressed data, you need to read the data from the Stream object. This is accomplished by the RetrieveBytesFromStream() function, which is defined next:

static byte[] RetrieveBytesFromStream(Stream stream, int bytesblock)
        {
            //---retrieve the bytes from a stream object---
            byte[] data = null;
            int totalCount = 0;
            try
            {
                while (true)
                {
                    //---progressively increase the size of the data byte array---
                    Array.Resize(ref data, totalCount + bytesblock);
                    int bytesRead = stream.Read(data, totalCount, bytesblock);
                    if (bytesRead == 0)
                    {
                        break;
                    }
                    totalCount += bytesRead;
                }
//---make sure the byte array contains exactly the number
                // of bytes extracted---
                Array.Resize(ref data, totalCount);
                return data;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }

The RetrieveBytesFromStream() function takes in two parameters — a Stream object and an integer — and returns a byte array containing the decompressed data. The integer parameter is used to determine how many bytes to read from the stream object into the byte array at a time. This is necessary because you do not know the exact size of the decompressed data in the stream object. And hence it is necessary to dynamically expand the byte array in blocks to hold the decompressed data during runtime. Reserving too large a block wastes memory, and reserving too small a block loses valuable time while you continually expand the byte array. It is therefore up to the calling routine to determine the optimal block size to read.

The block size is the size of the compressed data (data.Length):

//---the de-compressed data is stored in zipStream;
                // extract them out into a byte array---
                dc_data = RetrieveBytesFromStream(zipStream, data.Length);

In most cases, the uncompressed data is a few times larger than the compressed data, so you would at most expand the byte array dynamically during runtime a couple of times. For instance, suppose that the compression ratio is 20% and the size of the compressed data is 2MB. In that case, the uncompressed data would be 10MB, and the byte array would be expanded dynamically five times. Ideally, the byte array should not be expanded too frequently during runtime because it severely slows down the application. Using the size of the compressed data as a block size is a good compromise.

Use the following statements to test the Compress() and Decompress() functions:

static void Main(string[] args)
        {
            byte[] compressedData = Compress("Gzip",
                System.Text.Encoding.ASCII.GetBytes(
                "This is a uncompressed string"));
            Console.WriteLine("Compressed: {0}",
                ASCIIEncoding.ASCII.GetString(compressedData));
            Console.WriteLine("Uncompressed: {0}",
                ASCIIEncoding.ASCII.GetString(Decompress("Gzip", compressedData)));
            Console.ReadLine();
        }

The output is as shown in Figure 11-9.

Figure 11-9

Figure 11.9. Figure 11-9

The compressed data contains some unprintable characters, so you may hear some beeps when it prints. To display the compressed data using printable characters, you can define two helper functions — byteArrayToString() and stringToByteArray():

//---converts a byte array to a string---
        static string byteArrayToString(byte[] data)
        {
            //---copy the compressed data into a string for presentation---
            System.Text.StringBuilder s = new System.Text.StringBuilder();
            for (int i = 0; i <= data.Length - 1; i++)
            {
                if (i != data.Length - 1)
                    s.Append(data[i] + " ");
                else
                    s.Append(data[i]);
            }
            return s.ToString();
        }

        //---converts a string into a byte array---
        static byte[] stringToByteArray(string str)
        {
            //---format the compressed string into a byte array---
            string[] eachByte = str.Split(" ");
            byte[] data = new byte[eachByte.Length];
            for (int i = 0; i <= eachByte.Length - 1; i++)
                data[i] = Convert.ToByte(eachByte[i]);
            return data;
        }

To use the two helper functions, make the following changes to the statements:

static void Main(string[] args)
        {
            byte[] compressedData = Compress("Gzip",
                System.Text.Encoding.ASCII.GetBytes(
                "This is a uncompressed string"));

            string compressedDataStr = byteArrayToString(compressedData);
            Console.WriteLine("Compressed: {0}", compressedDataStr);

            byte[] data = stringToByteArray(compressedDataStr);
            Console.WriteLine("Uncompressed: {0}",
                ASCIIEncoding.ASCII.GetString(Decompress("Gzip", data)));

            Console.ReadLine();
        }

Figure 11-10 shows the output when using the two helper functions.

Figure 11-10

Figure 11.10. Figure 11-10

Alternatively, you can also convert the compressed data to a Base64-encoded string, like this:

byte[] compressedData = Compress("Gzip",
                System.Text.Encoding.ASCII.GetBytes(
                "This is a uncompressed string"));

            string compressedDataStr = Convert.ToBase64String(compressedData);
            Console.WriteLine("Compressed: {0}", compressedDataStr);

            byte[] data = Convert.FromBase64String((compressedDataStr));
            Console.WriteLine("Uncompressed: {0}",
                ASCIIEncoding.ASCII.GetString(Decompress("Gzip", data)));

Figure 11-11 shows the output using the base64 encoding.

Figure 11-11

Figure 11.11. Figure 11-11

Serialization

Many a time you may need to persist the value of an object to secondary storage. For example, you may want to save the values of a couple of Point objects representing the positioning of an item on-screen to secondary storage. The act of "flattening" an object into a serial form is known as serialization. The .NET Framework supports binary and XML serialization.

Binary Serialization

Consider the following class, BookMark, which is used to stored information about web addresses and their descriptions:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace Serialization
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }

    class BookMark
    {
        private DateTime _dateCreated;
        public BookMark()
        {
            _dateCreated = DateTime.Now;
        }
        public DateTime GetDateCreated()
        {
            return _dateCreated;
        }
        public string URL { get; set; }
        public string Description { get; set; }
        public BookMark NextURL { get; set; }
    }
}

The BookMark class contains properties as well as private variables. The NextURL property links multiple BookMark objects, much like a linked list. Let's now create two BookMark objects and link them:

static void Main(string[] args)
        {
            BookMark bm1, bm2;

            bm1 = new BookMark
            {
                URL = "http://www.amazon.com",
                Description = "Amazon.com Web site"
            };

            bm2 = new BookMark()
            {
                URL = "http://www.wrox.com",
                Description = "Wrox.com Web site",
                NextURL = null
            };

            //---link the first BookMark to the next---
            bm1.NextURL = bm2;
        }

You can serialize the objects into a binary stream by writing the Serialize() function:

static void Main(string[] args)
        {
            //...
        }

        static MemoryStream Serialize(BookMark bookMark)
        {
            MemoryStream ms = new MemoryStream();
            FileStream fs = new FileStream(
                @"C:Bookmarks.dat",
                FileMode.Create,
                FileAccess.Write);

            BinaryFormatter formatter = new BinaryFormatter();
            //---serialize to memory stream---
            formatter.Serialize(ms, bookMark);
            ms.Position = 0;

            //---serialize to file stream---
            formatter.Serialize(fs, bookMark);
            return ms;
        }

Note

For binary serialization, you need to import the System.Runtime.Serialization.Formatters.Binary namespace.

The Serialize() function takes in a single parameter (the BookMark object to serialize) and returns a MemoryStream object representing the serialized BookMark object. You use the BinaryFormatter class from the System.Runtime.Serialization.Formatters.Binary namespace to serialize an object. One side effect of this function is that it also serializes the BookMark object to file, using the FileStream class.

Before you serialize an object, you need to prefix the class that you want to serialize name with the [Serializable] attribute:

[Serializable]
    class BookMark
    {
        private DateTime _dateCreated;
        public BookMark()
        {
            _dateCreated = DateTime.Now;
        }
        public DateTime GetDateCreated()
        {
            return _dateCreated;
        }
        public string URL { get; set; }
        public string Description { get; set; }
        public BookMark NextURL { get; set; }
    }

The following statement serializes the bm1 BookMark object, using the Serialize() function:

static void Main(string[] args)
        {
            BookMark bm1, bm2;

            bm1 = new BookMark
            {
                URL = "http://www.amazon.com",
                Description = "Amazon.com Web site"
            };

            bm2 = new BookMark()
            {
                URL = "http://www.wrox.com",
                Description = "Wrox.com Web site",
                NextURL = null
            };

            //---link the first BookMark to the next---
            bm1.NextURL = bm2;

            //========Binary Serialization=========
            //---serializing an object graph into a memory stream---
            MemoryStream ms = Serialize(bm1);
        }

To prove that the object is serialized correctly, you deserialize the memory stream (that is, "unflatten" the data) and assign it back to a BookMark object:

static void Main(string[] args)
        {
            BookMark bm1, bm2;

            bm1 = new BookMark
            {
                URL = "http://www.amazon.com",
                Description = "Amazon.com Web site"
            };

            bm2 = new BookMark()
            {
                URL = "http://www.wrox.com",
                Description = "Wrox.com Web site",
                NextURL = null
            };

            //---link the first BookMark to the next---
            bm1.NextURL = bm2;


            //========Binary Serialization=========
            //---serializing an object graph into a memory stream---
            MemoryStream ms = Serialize(bm1);

            //---deserializing a memory stream into an object graph---
            BookMark bm3 = Deserialize(ms);
        }

Here is the definition for the DeSerialize() function:

static void Main(string[] args)
        {
            //...
        }

        static MemoryStream Serialize(BookMark bookMark)
        {
            //...
        }

        static BookMark Deserialize(MemoryStream ms)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            return (BookMark)formatter.Deserialize(ms);
        }

To display the values of the deserialized BookMark object, you can print out them out like this:

static void Main(string[] args)
        {
            BookMark bm1, bm2;

            bm1 = new BookMark
            {
                URL = "http://www.amazon.com",
                Description = "Amazon.com Web site"
            };

            bm2 = new BookMark()
            {
                URL = "http://www.wrox.com",
                Description = "Wrox.com Web site",
                NextURL = null
            };

            //---link the first BookMark to the next---
            bm1.NextURL = bm2;


            //========Binary Serialization=========
            //---serializing an object graph into a memory stream---
            MemoryStream ms = Serialize(bm1);


            //---deserializing a memory stream into an object graph---
            BookMark bm3 = Deserialize(ms);

            //---print out all the bookmarks---
            BookMark tempBookMark = bm3;
            do
            {
                Console.WriteLine(tempBookMark.URL);
                Console.WriteLine(tempBookMark.Description);
                Console.WriteLine(tempBookMark.GetDateCreated());
                Console.WriteLine("---");
                tempBookMark = tempBookMark.NextURL;
            } while (tempBookMark != null);

            Console.ReadLine();
        }

If the objects are serialized and deserialized correctly, the output is as shown in Figure 11-12.

Figure 11-12

Figure 11.12. Figure 11-12

But what does the binary stream look like? To answer that question, take a look at the c:BookMarks.dat file that you have created in the process. To view the binary file, simply drag and drop the BookMarks.dat file into Visual Studio 2008. You should see something similar to Figure 11-13.

Figure 11-13

Figure 11.13. Figure 11-13

A few observations are worth noting at this point:

  • Private variables and properties are all serialized. In binary serialization, both the private variables and properties are serialized. This is known as deep serialization, as opposed to shallow serialization in XML serialization (which only serializes the public variables and properties). The next section discusses XML serialization.

  • Object graphs are serialized and preserved. In this example, two BookMark objects are linked, and the serialization process takes care of the relationships between the two objects.

There are times when you do not want to serialize all of the data in your object. If you don't want to persist the date and time that the BookMark objects are created, for instance, you can prefix the variable name (that you do not want to serialize) with the [NonSerialized] attribute:

[Serializable]
    class BookMark
    {
        [NonSerialized]
        private DateTime _dateCreated;
        public BookMark()
        {
            _dateCreated = DateTime.Now;
        }
        public DateTime GetDateCreated()
{
            return _dateCreated;
        }
        public string URL { get; set; }
        public string Description { get; set; }
        public BookMark NextURL { get; set; }
    }

The dateCreated variable will not be serialized. Figure 11-14 shows that when the dateCreated variable is not serialized, its value is set to the default date when the object is deserialized.

Figure 11-14

Figure 11.14. Figure 11-14

XML Serialization

You can also serialize an object into an XML document. There are many advantages to XML serialization. For instance, XML documents are platform-agnostic because they are in plain text format and that makes cross-platform communication quite easy. XML documents are also easy to read and modify, which makes XML a very flexible format for data representation.

The following example illustrates XML serialization and shows you some of its uses.

Defining a Sample Class

Let's define a class so that you can see how XML serialization works. For this example, you define three classes that allow you to store information about a person, such as name, address, and date of birth. Here are the class definitions:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualBasic;
using System.IO;
using System.Xml.Serialization;
using System.Xml;

namespace Serialization
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
public class Member
    {
        private int age;
        public MemberName Name;
        public MemberAddress[] Addresses;
        public DateTime DOB;
        public int currentAge
        {
            get
            {
                //---add a reference to Microsoft.VisualBasic.dll---
                age = (int)DateAndTime.DateDiff(
                    DateInterval.Year, DOB,
                    DateTime.Now,
                    FirstDayOfWeek.System,
                    FirstWeekOfYear.System);
                return age;
            }
        }
    }

    public class MemberName
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class MemberAddress
    {
        public string Line1;
        public string Line2;
        public string City;
        public string Country;
        public string Postal;
    }
}

Note

The various classes are deliberately designed to illustrate the various aspects of XML serialization. They may not adhere to the best practices for defining classes.

Here are the specifics for the classes:

  • The Member class contains both private and public members. It also contains a read-only property.

  • The Member class contains a public array containing the address of a Member.

  • The Member class contains a variable of Date data type.

  • The MemberName class contains two properties.

  • The MemberAddress class contains only public members.

Serializing the Class

To serialize a Member object into a XML document, you can use the XMLSerializer class from the System.Xml.Serialization namespace:

static void Main(string[] args)
        {
        }

        //========XML Serialization=========
        static void XMLSerialize(Member mem)
        {
            StreamWriter sw = new StreamWriter(@"c:Members.xml");
            try
            {
                XmlSerializer s = new XmlSerializer(typeof(Member));
                s.Serialize(sw, mem);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                sw.Close();
            }
        }

Note

For XML serialization, you need to import the System.Xml.Serialization namespace.

In the XMLSerialize() function, you first create a new StreamWriter object so that you can save the serialized XML string to a file. The Serialize() method from the XMLSerializer class serializes the Member object into an XML string, which is then written to file by using the StreamWriter class.

To test the XMLSerialize() function, assume that you have the following object declarations:

static void Main(string[] args)
        {
            MemberAddress address1 = new MemberAddress()
            {
                Line1 = "One Way Street",
                Line2 = "Infinite Loop",
                Country = "SINGAPORE",
                Postal = "456123"
            };

            MemberAddress address2 = new MemberAddress()
            {
                Line1 = "Two Way Street",
                Country = "SINGAPORE",
                Postal = "456123"
            };

            Member m1 = new Member()
            {
                Name = new MemberName()
                {
                    FirstName = "Wei-Meng",
                    LastName = "Lee"
                },
                DOB = Convert.ToDateTime(@"5/1/1972"),
                Addresses = new MemberAddress[] { address1, address2 }
            };
        }

To serialize the Member object, invoke the XMLSerialize() method like this:

static void Main(string[] args)
        {
            MemberAddress address1 = new MemberAddress()
            {
                Line1 = "One Way Street",
                Line2 = "Infinite Loop",
                Country = "SINGAPORE",
                Postal = "456123"
            };

            MemberAddress address2 = new MemberAddress()
            {
                Line1 = "Two Way Street",
                Country = "SINGAPORE",
                Postal = "456123"
            };

            Member m1 = new Member()
            {
                Name = new MemberName()
                {
FirstName = "Wei-Meng",
                    LastName = "Lee"
                },
                DOB = Convert.ToDateTime(@"5/1/1972"),
                Addresses = new MemberAddress[] { address1, address2 }
            };

            XMLSerialize(m1);
        }

Figure 11-15 shows the XML document generated by the XMLSerialize() function.

Figure 11-15

Figure 11.15. Figure 11-15

As you can see, the object is serialized into an XML document with a format corresponding to the structure of the object. Here are some important points to note:

  • The City information is not persisted in the XML document (nor as the Line2 in the second Address element) because it was not assigned in the objects. You will see later how to persist empty elements, even though a value is not assigned.

  • All read/write properties in the object are persisted in the XML document, except the read-only currentAge property in the Member class.

  • Only public variables are persisted; private variables are not persisted in XML serialization.

  • The default name for each element in the XML document is drawn from the variable (or class) name. In most cases this is desirable, but sometimes the element names might not be obvious.

Deserializing the Class

To deserialize the XML document, simply use the Deserialize() method from the XMLSerializer class. Define the XMLDeserialize() function as follows:

static void Main(string[] args)
        {
            //...
        }

        //========XML Serialization=========
        static Member XMLDeserialize(string xmlFile)
        {
            Member obj;
            XmlReader xr = XmlReader.Create(xmlFile);
            try
            {
                XmlSerializer s = new XmlSerializer(typeof(Member));
                obj = (Member)s.Deserialize(xr);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                obj = null;
            }
            finally
            {
                xr.Close();
            }

            return obj;
        }

Here, you can use the XmlReader class's Create() method to open an XML file for reading. The XmlReader class is used to read the data from the XML file. The deserialized object is then returned to the calling function.

Note

Remember to import the System.Xml namespace for the XmlReader class.

To test the XMLDeserialize() function, call it directly after an object has been serialized, like this:

static void Main(string[] args)
        {
            MemberAddress address1 = new MemberAddress()
            {
                Line1 = "One Way Street",
                Line2 = "Infinite Loop",
                Country = "SINGAPORE",
                Postal = "456123"
};
            MemberAddress address2 = new MemberAddress()
            {
                Line1 = "Two Way Street",
                Country = "SINGAPORE",
                Postal = "456123"
            };

            Member m1 = new Member()
            {
                Name = new MemberName()
                {
                    FirstName = "Wei-Meng",
                    LastName = "Lee"
                },
                DOB = Convert.ToDateTime(@"5/1/1972"),
                Addresses = new MemberAddress[] { address1, address2 }
            };

            XMLSerialize(m1);

            Member m2 = XMLDeserialize(@"c:Members.xml");

            Console.WriteLine("{0}, {1}", m2.Name.FirstName, m2.Name.LastName);
            Console.WriteLine("{0}", m2.currentAge);
            foreach (MemberAddress a in m2.Addresses)
            {
                Console.WriteLine("{0}", a.Line1);
                Console.WriteLine("{0}", a.Line2);
                Console.WriteLine("{0}", a.Country);
                Console.WriteLine("{0}", a.Postal);
                Console.WriteLine();
            }
            Console.ReadLine();
        }

The output of these statements is shown in Figure 11-16.

Figure 11-16

Figure 11.16. Figure 11-16

Customizing the Serialization Process

Despite the fairly automated task performed by the XMLSerializer object, you can customize the way the XML document is generated. Here's an example of how you can modify classes with a few attributes:

[XmlRoot("MemberInformation",
        Namespace = "http://www.learn2develop.net",
        IsNullable = true)]
    public class Member
    {
        private int age;

        //---specify the element name to be MemberName---
        [XmlElement("MemberName")]
        public MemberName Name;

        //---specify the sub-element(s) of Addresses to be Address---
        [XmlArrayItem("Address")]
        public MemberAddress[] Addresses;
        public DateTime DOB;
        public int currentAge
        {
            get
            {
                //---add a reference to Microsoft.VisualBasic.dll---
                age = (int)DateAndTime.DateDiff(
                    DateInterval.Year, DOB,
                    DateTime.Now,
                    FirstDayOfWeek.System,
                    FirstWeekOfYear.System);
                return age;
            }
        }
    }

    public class MemberName
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class MemberAddress
    {
        public string Line1;
        public string Line2;

        //---empty element if city is not specified---
        [XmlElement(IsNullable = true)]
        public string City;

        //---specify country and postal as attribute---
[XmlAttributeAttribute()]
        public string Country;

        [XmlAttributeAttribute()]
        public string Postal;
    }

When the class is serialized again, the XML document will look like Figure 11-17.

Figure 11-17

Figure 11.17. Figure 11-17

Notice that the root element of the XML document is now <MemberInformation>. Also, <MemberAddress> has now been changed to <Address>, and the <Country> and <Postal> elements are now represented as attributes. Finally, the <City> element is always persisted regardless of whether or not it has been assigned a value.

Here are the uses of each attribute:

  • [XmlRoot("MemberInformation",
            Namespace = "http://www.learn2develop.net",
            IsNullable = true)]
        public class Member
        {
        ...

    Sets the root element name of the XML document to MemberInformation (default element name is Member, which follows the class name), with a specific namespace. The IsNullable attribute indicates if empty elements must be displayed.

  • //---specify the element name to be MemberName---
            [XmlElement("MemberName")]
            public MemberName Name;
            ...

    Specifies that the element name MemberName be used in place of the current variable name (as defined in the class as Name).

  • //---specify the sub-element(s) of Addresses to be Address---
            [XmlArrayItem("Address")]
            public MemberAddress[] Addresses;
            ...

    Specifies that the following variable is repeating (an array) and that each repeating element be named as Address.

  • //---empty element if city is not specified---
            [XmlElement(IsNullable = true)]
            public string City;
            ...
            Indicates that the document must include the City element even if it is empty.
  • //---specify country and postal as attribute---
            [XmlAttributeAttribute()]
            public string Country;
    
            [XmlAttributeAttribute()]
            public string Postal;
            ...

    Indicates that the Country and Postal property be represented as an attribute.

XML Serialization Needs a Default Constructor

There is one more thing that you need to note when doing XML serialization. If your class has a constructor (as in the following example), you also need a default constructor:

[XmlRoot("MemberInformation",
        Namespace = "http://www.learn2develop.net",
        IsNullable = true)]
    public class Member
    {
        private int age;
public Member(MemberName Name)
        {
            this.Name = Name;
        }

        //---specify the element name to be MemberName---
        [XmlElement("MemberName")]
        public MemberName Name;
        ...

This example results in an error when you try to perform XML serialization on it. To solve the problem, simply add a default constructor to your class definition:

[XmlRoot("MemberInformation",
        Namespace = "http://www.learn2develop.net",
        IsNullable = true)]
    public class Member
    {
        private int age;

        public Member() { }

        public Member(MemberName Name)
        {
            this.Name = Name;
        }
        ...

Uses of XML Serialization

XML serialization can help you to preserve the state of your object (just like the binary serialization that you saw in previous section) and makes transportation easy. More significantly, you can use XML serialization to manage configuration files. You can define a class to store configuration information and use XML serialization to persist it on file. By doing so, you have the flexibility to modify the configuration information easily because the information is now represented in XML; at the same time, you can programmatically manipulate the configuration information by accessing the object's properties and methods.

Summary

In this chapter, you explored the basics of files and streams and how to use the Stream object to perform a wide variety of tasks, including network communication, cryptography, and compression. In addition, you saw how to preserve the state of objects using XML and binary serialization. In the .NET Framework, the Stream object is extremely versatile and its large number of derived classes is designed to deal with specific tasks such as file I/O, memory I/O, network I/O, and so on.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.20.101