Working with Files in C#

For those familiar with a language like PHP or C++, it might be easy to conclude that C# provides “way too many” mechanisms for working with files. Between StreamWriters, TextWriters, and FileStreams, one might not know where to start, or which of these is “the best” mechanism for processing files in C#.

The real difference lies in what kind of data is being read or written. C# – mainly because of its historical roots in the Windows Operating System – has always handled text and binary files differently, and this difference is manifest in the way files are accessed and processed in C#, as C# provides different sets of tools for working with text files and binary files.

This article will introduce a very basic example of how to work with files in C# and then build on that for the purpose of giving a thorough introductory lesson regarding file processing in the C# programming language. The examples below are built using Visual Studio 2019 Community Edition.

Read: Visual Studio versus Visual Studio Code IDE.

C# File Handling Basics

The simplest file processing task one could undertake as a C# developer is to read and write text files. With that in mind, the following code snippet provides a very simple example of how to write and read a text file in C#:

using System;
using System.IO;

namespace WriteAndRead
{
    class Program
    {
        static void Main(string[] args)
        {
            string testingFileName = "myFile.txt";

            StreamWriter myStreamWriter = 
              new StreamWriter(testingFileName, false);
            myStreamWriter.WriteLine("This is my file!");
            myStreamWriter.WriteLine("Here's another line in my file!");
            myStreamWriter.Close();

            StreamReader myStreamReader = new StreamReader(testingFileName);
            string fileContents = myStreamReader.ReadToEnd();
            myStreamReader.Close();

            string[] linesInFile = fileContents.Split("\r\n");

            for (int x = 0; x < linesInFile.Length; x++)
            {
                Console.WriteLine("Line " + (x + 1).ToString() + " - " +
                  linesInFile[x]);
            }
        }
    }
}

Note how the code opens the file, writes to it, closes the file, then reopens it for reading. Just for the sake of originality, the code reads the entire contents of the file into a string variable before displaying the contents of the same on the console. The output of this code is below:

Read and Write Files in C#

Figure 1 – Writing and Reading a File

It is crucial that any file that is being written to be closed before it is read again because depending on how output data is buffered, there is no guarantee that the data written to the file is actually present in the file prior to it being closed.

C# File Processing Issues

With any kind of file processing, the following questions need to be addressed:

  • Is a file being created anew or being appended to?
  • What happens if access to the file is not granted?
  • If a file is to be read, what happens if it does not exist?

The question about whether a file is being created anew or is being appended to is addressed when a file is opened for writing. C#, like most languages, will by default, blank out all of the data in an existing file if it is opened for writing without appending. However, it is a good practice to explicitly specify whether or not appending is desired, as shown in the line of code:

...
            StreamWriter myStreamWriter = 
              new StreamWriter(testingFileName, false);
...

The second question regarding what happens if a file cannot be opened is important because there are many reasons as to why a file cannot be opened and a developer needs to factor in how these failures will be managed. Some of these reasons could be:

  • File permissions restrictions
  • Part of the file path not existing
  • The file might be open by another process

The C# code example below adds some error handling to the process.

using System;
using System.IO;

namespace WriteAndReadExHandling
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Program started.");
            string testingFileName = "myFile.txt";
            try
            {
                StreamWriter myStreamWriter = 
                    new StreamWriter(testingFileName, false);
                myStreamWriter.WriteLine("This is my file!");
                myStreamWriter.WriteLine("Here's another line in my file!");
                myStreamWriter.Close();
            }
            catch (Exception ex)
            {
                Console.Write("Could not write file due to errors:\r\n\t" + 
                    ex.Message);
                if (null == ex.InnerException)
                    Console.WriteLine();
                else
                    Console.WriteLine("\r\n\t" + ex.InnerException.Message);
            }

            try
            {
                StreamReader myStreamReader = new StreamReader(testingFileName);
                string fileContents = myStreamReader.ReadToEnd();
                myStreamReader.Close();

                string[] linesInFile = fileContents.Split("\r\n");
                for (int x = 0; x < linesInFile.Length; x++)
                {
                    Console.WriteLine("Line " + (x + 1).ToString() + " - " +
                        linesInFile[x]);
                }
            }
            catch (Exception ex)
            {
                Console.Write("Could not read file due to errors:\r\n\t" + 
                    ex.Message);
                if (null == ex.InnerException)
                    Console.WriteLine();
                else
                    Console.WriteLine("\r\n\t" + ex.InnerException.Message);
            }
            Console.WriteLine("Program ended.");
        }
    }
}

While exception handling is a nice thing to have, there are situations in which the exception by itself does not provide all of the information needed to properly troubleshoot a problem. This is why it is a good idea to check for inner exceptions as the code above does. Note that if no inner exception exists, then this object will be null, not blank. Not checking for null will result in an exception in the code’s exception handling, which can be as humorous as it is frustrating.

The best way to test exceptions is to simulate the conditions which could cause an exception. In the case of the above code, one effective way would be to remove access permissions for “Everyone” to the file myFile.txt:

C# File Handling Tutorial

Figure 2 – Locking out myFile.txt

These permission settings result in the following output when the code is run:

C# File Handling Code Exceptions

Figure 3 – Code listing showing exceptions which indicate that the file is locked out.

The using Statement in C#

Generally speaking, the using statement in C# makes sure that any sort of garbage collection and other housekeeping tasks related to any objects which require the same are indeed taken care of, without the need for the developer to actively code against the same. This is one of the many places where C# shines when compared to other languages which require that such tasks be manually done.

When used with reading and writing files, the using statement can ensure that any and all housekeeping tasks related to files are taken care of. With regards to files in particular, these can sometimes be a shared resource between programs, and due to Windows file locking rules, a given file may be inaccessible to another process if it is not properly closed and released by the process which is working with the file.

using System;
using System.IO;

namespace WriteAndReadUsing
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Program started.");
            string testingFileName = "myFile.txt";
            try
            {
                using (StreamWriter myStreamWriter =
                    new StreamWriter(testingFileName, false))
                {
                    myStreamWriter.WriteLine("This is my file!");
                    myStreamWriter.WriteLine("Here's another line in my file!");
                }

            }
            catch (Exception ex)
            {
                Console.Write("Could not write file due to errors:\r\n\t" +
                    ex.Message);
                if (null == ex.InnerException)
                    Console.WriteLine();
                else
                    Console.WriteLine("\r\n\t" + ex.InnerException.Message);
            }

            try
            {
                using (StreamReader myStreamReader = 
                    new StreamReader(testingFileName))
                {
                    string line = "";
                    int index = 1;
                    while (null != (line = myStreamReader.ReadLine()))
                        {
                            Console.WriteLine("Line " + index.ToString() + 
                                " - " + line);
                        }
                }

            }
            catch (Exception ex)
            {
                Console.Write("Could not read file due to errors:\r\n\t" +
                    ex.Message);
                if (null == ex.InnerException)
                    Console.WriteLine();
                else
                    Console.WriteLine("\r\n\t" + ex.InnerException.Message);
            }
            Console.WriteLine("Program ended.");
        }
    }
}

Read: Getting Image Data from Files in .Net.

Working with Binary Files in C#

In the context of Windows, a binary file can be any file that is not a text file. While text files, in a manner of speaking, are able to be read by humans, binary files can only be read by computer programs. Some examples of binary files include images, audio files, proprietary format files such as Word documents or Excel sheets, PDF files, encrypted data files, and many others.

Practically speaking, while C# provides a FileStream library that can read and write binary data, it is better to use the many C# libraries which can handle particular types of binary data natively. Another important thing to keep in mind is that, if C# does not natively provide the library to handle the particular kind of data being worked on, there are external libraries or assemblies which can be added to a project via the NuGet functionality in Visual Studio.

Read: How to Work with Office Interop Objects in C#.

For example, if one wishes to read the content of PDF files, there exist external assemblies which work directly on these kinds of files. The same goes for audio files or other kinds of specialized data files.

With this in mind, the example below uses C#’s built-in image libraries to read and write binary image files, as opposed to the more generalized binary file processing libraries. Note that Visual Studio might display an error message indicating that the System.Drawing assembly is not installed, but this prompt will provide an opportunity to install it.

using System;
using System.Drawing;
using System.Drawing.Imaging;

namespace ImageFlipper
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Program started.");
            string testingFileName = "corn.jpeg";
            string testingOutFileName = "corn-flipped.jpeg";
            string currentOp = "";
            try
            {
                // Note that even though the Bitmap library provides a means
                // to load an image file, the data must be cast as a bitmap
                // image.
                currentOp = "read the file";
                Bitmap workingImageFile = 
                    (Bitmap)Bitmap.FromFile(testingFileName);
                currentOp = "rotate the image";
                workingImageFile.RotateFlip(RotateFlipType.Rotate90FlipNone);
                currentOp = "write the file";
                workingImageFile.Save(testingOutFileName, ImageFormat.Jpeg);
            }
            catch (Exception ex)
            {
                Console.Write("Could not " + currentOp + 
                    " due to errors:\r\n\t" + ex.Message);
                if (null == ex.InnerException)
                    Console.WriteLine();
                else
                    Console.WriteLine("\r\n\t" + ex.InnerException.Message);
            }

            Console.WriteLine("Program ended.");
        }
    }
}

One of the nice features of the Bitmap library is that it is reasonably good at figuring out what kind of image data is being read based on the information in the file. It usually is not necessary to explicitly specify the type of image, be it a BMP, PNG, JPG or whatnot. However, the same is not true when writing out image data. By default, the Bitmap library will output BMP data, regardless of the extension specified for the file.

C# Binary File Tutorial

Figure 4 – Original Image (scaled)

C# Binary File Handlng
Figure 5 – Output Image (scaled)

If this data was to be written using the TextWriter or StreamWriter libraries – or any other C# file processing functionality that handles text data – the image data would likely become corrupted and the file would be unusable in other programs which work with or display images. The same caveat applies to any other kind of binary data file.

Conversely, while it is possible to write out text data as binary data, this may result in problems with other programs which are intended to read text data. It is critical that C# programs handle text data using text-related file processing functions, and binary data using binary-related file processing functions.

Sharing a File in C#

Given the ubiquity of multi-user operating systems such as Windows, a very frequent problem arises when multiple processes have to write data to the same file. In such situations, programs have to rely on the operating system to govern such access, and they must be coded to gracefully process failures when the operating system denies the process write access to the file.

Windows has always had a very robust and forceful mandatory locking policy when it comes to governing access to shared files. Mandatory locking means that if a file is already open by one process, it is locked out from access by other processes, with the operating system reporting an error when another process tries to access the file. Once the original process has closed the file, another process may access it.

The code sample below simulates asynchronous writing to a shared file. While there are more elegant ways to handle multithreading in C#, those examples go far beyond the scope of this simple introduction. For this example, multithreading is performed by opening two Windows Command Prompts and running the same executable with different parameters from both windows.

using System;
using System.IO;
using System.Threading;

namespace File_Lock_Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            int currentInstance = 0;
            if (args.Length > 0)
            {
                try
                {
                    currentInstance = Int32.Parse(args[0]);
                    Console.WriteLine("Current Instance is [{0}]", 
                        currentInstance.ToString());
                }
                catch (Exception ex)
                {
                    Console.WriteLine("First parameter was not an integer. {0}", 
                        ex.Message);
                    currentInstance = 0;
                }
            }
            WriteDemoOutput(currentInstance);
        }

        static async void WriteDemoOutput(int currentInstance)
        {
            for (int x = 0; x < 10; x++)
            {
                // Generate a "random" number between 1 and 5.
                int randomNumber = new Random().Next(1, 6); 

                // Pause the thread for the specified number of milliseconds.
                Thread.Sleep(1000 * randomNumber);

                // Use StreamWriter overload to append, not overwrite.
                using (StreamWriter myStreamWriter = 
                    new StreamWriter("output.txt", true)) 
                {
                    string output = "This was written by instance [" + 
                        currentInstance.ToString() + "] at [" + 
                        DateTime.Now.ToString() + "].";
                    await myStreamWriter.WriteLineAsync(output);
                }
            }
        }
    }
}

The setup for running 2 threads of this almost simultaneously:

File Handling in C# Guide

Figure 6 – Simulating multithreading via multiple Windows Command Prompts

Below is the output of both instances of this code:

Guide to File Handling with C#

Figure 7 – Multiple processes writing to the same file.

One important thing to keep in mind is that this example could not work using “traditional” synchronous file processing. Each process would be waiting for the other to finish and no output could be written. Furthermore, C# generously provides very robust tools to easily navigate the complexities introduced with shared resources. It is possible to say that it really is as “easy” as it looks.

C# File Handling Tutorial Conclusion

This article scratches the surface of file processing in C#. As stated initially, C# generously provides many file processing functions, both for traditional data manipulation, as well as for data management involving access to shared resources. The examples here provide a starting point for developers to go much further with file processing for their specific application needs.

Read more C# programming language tutorials and guides.

Phil Hajjar
Phil Hajjar
Phil is usually seen making things work together that shouldn’t be, but need to be. He describes himself as a marriage counselor for software and other technology systems. He appropriated this moniker way back in college as he first experimented with making disparate software work together back then, and he continues doing so in his over 20 years of professional IT experience now.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read