Understanding File and Stream IO in C# With Examples

shabbir's Avatar author of Understanding File and Stream IO in C# With Examples
This is an article on Understanding File and Stream IO in C# With Examples in C#.
Most of the time, a developer needs to read or write data from a location outside the memory of the program. This location can be a text file, a network connection a database or any other source of data. In order to deal with such sources of information, .NET Framework provides a set of classes that can be used to read, write and interact with such data sources. This is done via streams & IO classes which is the topic under discussion.

How Streams Work?



Before dwelling into the details of streams, let us first throw some light on its architecture. The foundation of .NET streams architecture is laid upon three fundamental concepts:
  1. Backing Source
  2. Decorators
  3. Adapters
Backing source is the source from where data is read or destination to which data is written. Backing source can be file or network connection. In order to deal with backing source a uniform interface is required. This is done via a Stream class in .NET framework. Stream class reads or writes data sequentially in the form of bytes or chunks of manageable size in the memory. This is different to the concept of array where all the data has to reside in memory. Thus, streams use less memory as compared to arrays.
Streams further divided into two types:
  • Backing Source Streams: These streams actually interact with data source or destination. For example, NetworkStream or FileStream.
  • Decorator Streams: These streams are used to transform data in some other form and then feed that data to other streams.
Both types of streams deal with data in terms of byte. They read and write data sequentially in terms of bytes. However, in real world programming, a higher level of abstraction is required where developers can use more concrete methods such as ReadLine or WriteAttribute methods which can read a line from a file and write XML attributes to file. This abstraction is provided via Adapters. Simply putting, Adapters wrap streams, hiding complexity of interacting with byte based data interaction.

To further understand the concept of streams, have a look at the first example of this article:

Example1
Code:
using System;
using System.IO;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            FileStream file = new FileStream(@"D:\MyFile.txt", FileMode.Create);

            Console.WriteLine("File can be read : "+ file.CanRead);
            Console.WriteLine("File can be written : " + file.CanWrite);
            Console.WriteLine("File can be seeked : " + file.CanSeek);

            file.WriteByte(150);
            file.WriteByte(200);

            byte[] byteblock = { 10, 20, 30, 40, 50, 60, 70 };

            file.Write(byteblock, 0, byteblock.Length);

            Console.WriteLine(file.Length);
            Console.WriteLine(file.Position);

            file.Position = 0;

            Console.WriteLine(file.ReadByte());
            Console.WriteLine(file.ReadByte());

            Console.WriteLine(file.Read(byteblock, 0, byteblock.Length));

            Console.WriteLine(file.Read(byteblock, 0, byteblock.Length));
            Console.ReadLine();
        }
    }
}
We have a FileStream which we named file, in the constructor we mentioned file name along with file mode which is create in this case. Next we verified that if this file is readable, writable and seek-able by calling CanRead, CanWrite and CanSeek properties, respectively. We then wrote two bytes by calling WriteByte method. Next, we wrote an array of bytes via Write method that takes an array to be written, the array offset and the length of the array as respective elements.

To get the length of the FileStream object, Length method can be called and to find out that to which byte stream is pointing at the moment, Position method can be used. In our example, the stream would be pointing to the 9th position since we have written two byte values and a byte array with 7 bytes which makes total of 9. So length and position will be both 9.

We next changed the position of stream pointer by setting Position property of the stream to 0. Stream now points at the starting index. To read single byte from the source, ReadByte method is used. We have used it twice, so it would print the value of first and two bytes and would change pointer position to first index. To read a block of array, Read method is used which takes an array to which data should be read, along with offset and array length. After we read the next 7 elements, the stream pointer would again point to 9th position which would be displayed in the output as shown below:

Output1



Reading and Writing Asynchronously



Read and Write methods have asynchronous counter parts i.e ReadAsync, WriteAsync. These methods can be used along with ‘await’ keyword to do read and write asynchronously (To learn more about Asynchronous Functions in C#, Check this tutorial).

To see how asynchronous methods are implemented while reading and writing, have a look at our next example of this tutorial.

Example2
Code:
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace CSharpTutorial
{
    class Program
    {
     public static async void AsynchReadWrite()
        {
            FileStream file = new FileStream(@"D:\MyFile.txt", FileMode.Create);
            Console.WriteLine("File created");
            byte[] byteblock = { 10, 20, 30, 40, 50, 60, 70 };
            await file.WriteAsync(byteblock, 0, byteblock.Length);
            file.Position = 0;
            Console.WriteLine(await file.ReadAsync(byteblock, 0, byteblock.Length));
            Console.ReadLine();
        }

        public static void Main()
        {
            AsynchReadWrite();
           
            Console.ReadLine();
        }
    }
}
In example2, we have an asynchronous function AsyncReadWrite. Note that we have to add keyword ‘async’ in the method declaration of AsyncReadWrite. Inside this function we call WriteAsync and ReadAsync.. Both of these functions return a Task on which we can wait by calling ‘await’ as it has been done in Example2. The output of the code in Example2 is as follows:

Output2



Note that in case of both Example1 and Example2, if you go and check your file that has been created, you will find that it contains some random character, this is due to the reason that calling WriteByte and Write by passing byte array, write bytes of data onto the file.

Streams interact with data sources outside the application domain; therefore it is always a good practice to flush and dispose streams once they have been used. For this purpose, ‘using’ statement can be used and all the code that involves streams and stream methods must be enclosed inside ‘using’ statement. This would help release resources such as Network connection and files, once data has been written or read.

FileStream class

File stream is a backing source stream as aforementioned. In Example1 and Example2, FileStream class has been used. Let’s explore some more advanced features of the FileStream class.

Apart from using FileStream constructor, there are three static methods of the File class which return a FileStream object.
  1. File.OpenRead: Opens a file for reading purposes.
  2. File.OpenWrite: Opens file for writing purposes.
  3. File.Create
The difference between Create and OpenWrite method is that Create method deletes any data on the file if already exists and OpenWrite appends the data to the file if already exists, starting from first index of the stream. In our next example, we are going to explain some of the more advanced and interesting static methods of the File class. Have a look at Example3 of this tutorial:

Example3
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            using (FileStream file = new FileStream(@"D:\MyFile.txt", FileMode.Create))
            {
                string content = "This is a temporary file demonstrating the concept of streams";
                File.WriteAllText(@"D:\file.txt", content);
                
                List<string> lines = new List<string> { "This is line1", "This is line2", "This is line3" };
                File.WriteAllLines(@"D:\file2.txt", lines);

                File.AppendAllText(@"D:\file.txt", " This is appended to file");
                Console.WriteLine(File.ReadAllText(@"D:\file.txt"));

                IEnumerable<string> linesread = File.ReadAllLines(@"D:\file2.txt");

                foreach (string line in linesread)
                {
                    Console.WriteLine(line);
                }
                Console.ReadLine();
            }
        }
    }
}
In Example3, few useful static methods of the File class have been explained. These methods are:
  • WriteAllText - This method takes two parameters: A file name to which text has to be written and string of text which has to be written. This method writes content to file in the form of one long strong.
  • WriteAllLines - This method also takes two parameters: First is the file path and second is any collection of type string. Each item in the collection is written as new line in the file.
  • AppendAllText - It also takes two parameters: File path and the string content which will be appended at the end of the already written text in the file.
  • ReadAllText - It takes file path as a parameter and returns all the contents of the file in a single string.
  • ReadAllLines - This method also takes one parameter i.e. file path and returns a collection of string where each string in the collection corresponds to each line in the file.
All of these methods have been implemented in Exmple3, if you compile and run Example3, you would see the following output:

Output3



Stream Adapters



As aforementioned, backing source streams and decorator streams are only capable of reading and writing data in the form of bytes. To read and write data in the form of text, xml or binary values, special wrappers have been designed which are known as stream adapters. There are three major types of stream adapters:

1. Text Adapters

These adapters are used for reading and writing string and character data. This category of adapters includes:
The TextReader and TextWriter classes are abstract and are inherited by StreamReader, StreamWriter, StringReader and StringWriter class. In this section, StreamReader/ StreamWriter and StringReader/String Writer classes have been explained.

2. Binary Adapters

These adapters are used to write primitive data types such as int, float, string and bool. There are two binary adapters: BinaryReader & BinaryAdapter. These binary adapters are used to read and write all the native data types such as bool, char, byte, float, double, decimal, long, short, int etc. Best thing about binary adapters is that they write data into files as if data is stored in the memory. For instance an integer will take 4 bytes in the file as it takes 4 bytes in the memory, a double variable will take 8 bytes and so on. To demonstrate the working of a binary adapter, let us see another example. Have a look at the 8th example of this tutorial:

3. XML Adapters

It contains XmlReader & XmlWriter classes which have been covered in detailed in this tutorial.

StreamReader/StreamWriter class



To see how text adapters StreamReader and StreamWriter are implemented in an application. Have a look at the fourth Example of this code:

Example4
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            using (FileStream file = new FileStream(@"D:\MyFile.txt", FileMode.Create))
            using (TextWriter tw = new StreamWriter(file))
            {
                tw.WriteLine("This is line 1");
                tw.WriteLine("This is line 2");
                tw.WriteLine("This is line 3");
            } 

            using (FileStream file = new FileStream(@"D:\MyFile.txt", FileMode.Open))
            using (TextReader tr= new StreamReader(file))
            {
               Console.WriteLine( tr.ReadLine());
               Console.WriteLine( tr.ReadLine());
               Console.WriteLine( tr.ReadLine());
            
            }
                Console.ReadLine(); 
        }
    }
}
This is simplest example of TextWriter and TextReader classes. We mentioned earlier that adapters act as a wrapper over streams. Here in this example we first declared a FileStream class that defines the path and the file mode. And then this FileStream object is passed to the StreamReader class as a parameter. You must be wondering that why we did not call the constructor of TextReader class directly, why we called the constructor of StreamReader class. The reason is that TextReader and TextWriter classes are abstract classes and StreamReader, StreamWriter, StringReader and StringWriter classes inherit these classes respectively.

Notice that how simply it is to write a line to the file. You simply have to call WriteLine and pass it a string. This is simple to writing on the console. This is where adapters are helpful. Since writing to a text file is a common task therefore File class has several static method for this purpose that have been defined in Example3.

Now come towards reading the content of the file, you use StreamReader object for that purpose which is stored in the instance of a TextReader class. Then you have to simply call ReadLine method which would return the first line and would shift the cursor to the next line. Calling ReadLine again would yield the second line and so on. The output of the code in Example4 is as follows:

Output4



Since reading and writing test is a common task. It is better to use static OpenText and OpenRead methods of File class which internally return StreamReader and StreamWriter objets respectively. Have a look at our 5th example to understand this concept:

Example5
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            using (TextWriter tw = File.CreateText(@"D:\File1.txt"))
            {
                tw.WriteLine("This is line 1");
                tw.WriteLine("This is line 2");
                tw.WriteLine("This is line 3");
            } 
            using (TextReader tr= File.OpenText(@"D:\File1.txt"))
            {
            
               Console.WriteLine( tr.ReadLine());
               Console.WriteLine( tr.ReadLine());
               Console.WriteLine( tr.ReadLine());
      
            }
                Console.ReadLine(); 
        }
    }
}
In example5, it can be seen that we have totally emitted the FileStream objects. Instead we are directly calling The CreateText method of the File class which takes a path and return a StreamWriter object that is having an anonymous internal FileStream object. The returned StreamReader object is stored in the instance variable of the TextReader object, the process of writing to the file is similar to what we did in Example4.

The process of reading a text via a File class is similar to writing. However, the OpenText method of the File class is called which returns the StreamReader object. The reading process is similar as in Example4. The output of Example5 is also similar to Example5 as shown below:

Output5



Using Peek with TextReader

Peek is a method which denotes the current position of the TextReader. Once all the lines in a text file have been read, Peek returns a negative value, usually -1. We don’t always know that how many lines are there in a text files. In such scenarios, Peek method can be used to read all the lines in the TextReader. A demonstration of how Peek method actually works has been given in 6th example of this tutorial:

Example6
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            using (TextWriter tw = File.CreateText(@"D:\File1.txt"))
            {
                tw.WriteLine("This is line 1");
                tw.WriteLine("This is line 2");
                tw.WriteLine("This is line 3");
            } 

            using (TextReader tr= File.OpenText(@"D:\File1.txt"))
            {
                while (tr.Peek() > -1)
                {
                    Console.WriteLine(tr.ReadLine());
                }
            }
                Console.ReadLine(); 
        }
    }
}
In Example6, jump straight to the TextReader object. Here Peek method has been used inside the while loop. Peek method doesn’t return the actual stream of bytes it only refers to position and until Peek returns a positive value it means that there are lines to read in a text.

Reading Int, bools and other variables



TextReader class needs content of file in the form of string. In order to read int and bool, one way is to read the string and then cast it to respective types. This concept is demonstrated in the next example of the tutorial. Have a look at the 7th example of this tutorial:

Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            using (TextWriter tw = File.CreateText(@"D:\File1.txt"))
            {
                tw.WriteLine("This is line 1");
                tw.WriteLine("123");
                tw.WriteLine("true");
            } 

            using (TextReader tr= File.OpenText(@"D:\File1.txt"))
            {
                    Console.WriteLine(tr.ReadLine());
                    int num = Convert.ToInt32(tr.ReadLine()) +5;
                    Console.WriteLine(num);
                    bool flag = Convert.ToBoolean(tr.ReadLine());
                    Console.WriteLine(flag); 
            }
                Console.ReadLine(); 
        }
    }
}
In the TextReader section of Example7, convert methods have been used to convert the string values in to integer and bool respectively. In the second line of the file we wrote 123 but when we read we added 5 to it. This is in order to verify that actually the read content has been converted into integer. If the above code is compiled and run, the output would contain 128 in the second line as shown here:

Output7



StringReader/StringWriter



These classes are different from StreamReader and StreamWriter in terms of the types they wrap. StreamReader and StreamWriter wrap streams whereas StringReader and StringWriter classes wrap string or StringBuilder classes. There is not much use of these classes but the fact that they inherit TextReader and TextWriter class makes useful with the XmlReader class which accepts a URI, a Stream or a TextReader object.

Example8
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Vehicle
    {
        public string type;
        public  int number;
        public int model;

        public Vehicle(string type, int number, int model)
        {
                this.type = type;
                this.number = number;
                this.model = model;
        }
    
        public void SaveVehicle(Stream vstream)
        {
            using (BinaryWriter vehiclewriter = new BinaryWriter(vstream))
            {
                vehiclewriter.Write(type);
                vehiclewriter.Write(number);
                vehiclewriter.Write(model);
                vehiclewriter.Flush();
            }
        }

        public void LoadVehicle(Stream vstream)
        {
            BinaryReader vehiclereader = new BinaryReader(vstream);
            Console.WriteLine(vehiclereader.ReadString());
            Console.WriteLine(vehiclereader.ReadInt32());
            Console.WriteLine(vehiclereader.ReadInt32());
        }
    }

    class Program
    {
        public static void Main()
        {

            Vehicle v = new Vehicle("car", 9952, 2014);
            Stream s = File.Open(@"D:\Vehicle.txt", FileMode.Create);
            v.SaveVehicle(s);
            s = File.Open(@"D:\Vehicle.txt", FileMode.Open);
            v.LoadVehicle(s);
            Console.ReadLine(); 
        }
    }
}
Carefully look at the above example. Here we have a class named Vehicle. It contains three member variables name, number and model of type string, int and int respectively. We have a parameterized constructor that takes three parameters and can be used to initialize the three member variable.

Next, there are two methods: SaveVehicle and LoadVehicel, both of these methods take a Stream object as a parameter. Inside the SaveVehicle method we have a BinaryWriter named vehiclewriter and the stream object passed to the SaveVehicle method is forwarded to this vehiclewriter. Notice how simple is it to write all data types via single Write method by passing the data type to the method. No transformation into string is required in this case.

Similarly, the LoadVehicle method contains a BinaryWriter object that takes a stream. Real efficiency of binary adapters lay in their read methods. In order to read different data types from the file, BinaryReader contains different method. For instance the first element in the file is name which is of type string. To read this value ReadString method of BinaryReader object can be used. Similarly the next two integer values can be read via ReadInt32 methods. Thus binary adapters save you from converting and parsing strings to other data types as was the case with text adapters.

Performing File & Directory Operations



Often times you need to create files and directories from your application; you want to copy files from one directory to the other directory or you want to set permissions on files or directories. The types that allow you to perform these operations reside in System.IO namespace. There are two types of classes for these purposes: Static classes such as Path & Directory and Instance classes such as FileInfo and DirectoryInfo. We have seen static methods of File class in action in previous examples. Here we will explore this class a bit more.

The File Class

Let us start discussing the File Class with an example. Have a look at the 8th example of this tutorial.

Example8
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            string path1 = @"D:\file1.txt";
            string path2 = @"E:\file2.txt";

            using (TextWriter tw = File.CreateText(path1))
            {
                tw.WriteLine("This is line1");
                tw.WriteLine("This is line2");
            }

                if (File.Exists(path1))
                {
                    
                    Console.WriteLine("File exists ...");

                    if (File.Exists(path2))
                    {
                    File.Delete(path2);
                    }

                    File.Copy(path1, path2);
                    Console.WriteLine("File has been copied");

                    Console.WriteLine(File.ReadAllText(path2));  
                }

                Console.WriteLine(File.GetCreationTime(path2));
                Console.WriteLine(File.GetLastWriteTime(path1));

                File.Delete(path1);

                Console.WriteLine(File.Exists(path1)); 
            Console.ReadLine(); 
        }
    }
}
Carefully look at the code in Example8, several File methods have been explained here in this example. First of all we defined two file paths: Path1 points to the file.txt file in the root director “D” and Path2 points to the file2.txt in the root directory “E”. Using a TextWriter, we wrote a content of couple of lines to Path1.

Next we called static Exists method of the File class and passed it Path1. The Exists method will check that if the file is located at this location or not. It returns true if file exists. We said that if file at Path1 exists then check that if file at Path2 exists and if it exists, delete that file. Next we called the static Copy method of the File class which takes two parameters: source path and destination path, Path1 and Path2 were passed to it respectively. The copy method would create a new file at Path2 and would copy the content of Path1 to this method.

Next, in order to verify that if the contents of Path1 have been copied to Path2, we called ReadAllText method and passed it Path2. Finally GetCreationTime and GetLastWriteTime methods have been called to get the creation time and last written time of Path1 and Path2 respectively. In the end file at Path1 is deleted using Delete method. The output of the code in Example8 is as follows:

Output8



The Directory Class

Most of the methods that belong to File class can also be used with static Directory class. In the next example of this code, we will demonstrate this concept. Have a look at the 9th example of this article.

Example9
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            string path1 = @"D:\Directory1";
            string path2 = @"D:\Directory2";

            Directory.CreateDirectory(path1);
       
                if (Directory.Exists(path1))
                {
                    Console.WriteLine("Directory exists ...");

                    if (Directory.Exists(path2))
                    {
                    Directory.Delete(path2);
                    }

                    Directory.Move(path1, path2);
                    Console.WriteLine("Directory has been copied");  
                }

                Console.WriteLine(Directory.GetCreationTime(path2));
                Console.WriteLine(Directory.GetLastWriteTime(path1));

                Console.WriteLine(File.Exists(path1)); 
            Console.ReadLine(); 
        }
    }
}
Here in Example8, we again have two paths: Path1 that refers to Directory1 in the root “D” directory and Path2 that refers to the Directory2 in the same root “D” directory. Now, in case of File class, you simply called Create to create a file. In case of directories, there exists CreateDirectory method that creates a directory at the specified path. We used it to create a directory at Path1.

Like File class, Exists method also exists in Directory class and returns true if a directory exists at the specified path. We used this method to check if a directory exists at Path1 and then we again used to check if directory exists at Path2 or not. If it exists delete it. Notice Delete method is also common in both File and Directory classes.

We then called Move method which would remove the Directory from Path1 and would Move it to Path2 along with all of its contents. File class also contains Move method with same functionality. Also, GetCreationTime, GetLastCreation time methods are also same in both File and Directory class as shown in Example8 & Example9. The output of the code in Example9 is given below:

Output9



The FileInfo & DirectoryInfo Classes

FileInfo & DirectoryInfo classes are kind of instance type classes of their static counterparts File & Directory. FileInfo and DirectoryInfo classes contain many of the methods contained by File and Directory classes and they introduce some more methods intrinsic to them. The next example demonstrates the working of both FileInfo and DirectoryInfo class. Have a look at the 10th tutorial of this article:

Example10
Code:
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace CSharpTutorial
{
    class Program
    {
        public static void Main()
        {
            string path1 = @"D:\file1.txt";
            string path2 = @"E:\file2.txt";

            FileInfo finfo = new FileInfo(path1);
            {
                using (TextWriter tw = finfo.CreateText())
                {
                    tw.WriteLine("This is line1");
                    tw.WriteLine("This is line2");
                }

                if (finfo.Exists)
                {

                    Console.WriteLine("File exists ...");
                    if (File.Exists(path2))
                        File.Delete(path2);
                    finfo.CopyTo(path2);
                    Console.WriteLine("File has been copied");
                }

                Console.WriteLine(finfo.Name);
                Console.WriteLine(finfo.FullName);
                Console.WriteLine(finfo.Directory);
                Console.WriteLine(finfo.DirectoryName);
                Console.WriteLine(finfo.Directory.Name);
                Console.WriteLine(finfo.Length);
                Console.WriteLine(finfo.Extension);

                DirectoryInfo dinf = finfo.Directory;
                Console.WriteLine(dinf.Parent);
                Console.WriteLine(dinf.Name);
                Console.ReadLine();
            }
        }
    }
}
Here in the above case we have finfo object of FileInfo class and this object points to the File at Path1. Note the similarities between the finfo object and the File static class in 8th example. Many methods are common with slight variations in their parameters. Come straight to the properties section. We can get the Name, FullName, Directory, DirectoryName, Length and Extension of the FileInfo object which was not possible in case of static class.

Similarly in Example10, there is a DirectoryInfo class dinf which performs same functionality as the FileInfo class but the former does it with directories. There are many other methods, such as attributes, encryptions and decryptions etch which can be used to set permissions on the files and directories. However, I would like you to explore them yourself. The output of the code in Example10 is as follows:

Output10