File I/O in C#

File I/O

The Crash Course

  • Reading and writing to files is often called file input and output, or file I/O for short.
  • Everything you do will start with the File class.
  • File.WriteAllLines(string filePath, string[] lines); lets you write out a whole bunch of lines to a file all at once.
  • File.WriteAllText(string filePath, string allText); lets you write out the contents of the entire file in a single all at once.
  • When you're doing file I/O, you will likely need to take your data and turn it into a format that works for one of the output methods and then parse the text coming back in so that you can reconstruct your objects. In other words, there's more to the typical file writing process than just dumping stuff into a file—you'll also need to get your data into the right format.
  • You can also read and write text-based and binary files in a different way (a little at a time, instead of all at once). Either way, you start with File.OpenRead or File.OpenWrite, wrap the returned FileStream in a TextReader, TextWriter, BinaryReader, or BinaryWriter, depending on what you want. Then go crazy reading or writing. When you're done writing, it is a good idea to call the writer's Flush if you are writing and call the reader or writer's Close method.

Introduction

One very common task in any programming language is the ability to write information to a file and read it back in. This is often called file input and output or simply file I/O.

Writing to files and reading from them is actually a very simple task in C#. There are several ways it can be done. We'll cover the simplest way here and touch on a few more advanced ways after.

The Easy Way

Writing the File

There's one very simple way to read from a file in C#. A File class has already been created for you that you can use to quickly write stuff to a file.

Here are two variations of this method to write data to a file:

string informationToWrite = "Hello persistent file storage world!";
File.WriteAllText("C:/Users/RB/Desktop/test1.txt", informationToWrite); // Change the file path here to where you want it.
 
string[] arrayOfInformation = new string[2];
arrayOfInformation[0] = "This is line 1";
arrayOfInformation[1] = "This is line 2";
File.WriteAllLines("C:/Users/RB/Desktop/test2.txt", arrayOfInformation);

In the first set, we prepare a single string that contains the entire contents of what we want to be stored in the file and then use the {WriteAllText}} method in the File class to write the whole thing out. The WriteAllText method requires a file location as well as the text to write out.

In the second set, we prepare an array of strings. We then use the WriteAllLines method, which will output the lines, one at a time, separated by a new line, so the file will end up containing this:

This is line 1
This is line 2

Reading the File

Reading a file back in is just as easy:

string fileContents = File.ReadAllText("C:/Users/RB/Desktop/test1.txt");
 
string[] fileContentsByLine = File.ReadAllLines("C:/Users/RB/Desktop/test2.txt");

This does the same thing but in reverse. In the first part, the file's entire contents are "slurped" into the single fileContents variable. In the second part, the whole thing is pulled in as an array of strings, where each line in the file comes in as a different index in the string array.

But Wait, There's More…

This technique is extremely simple and easy to work with, but it's often not quite as simple as this. You'll often work with data besides a single long string or a set of strings that you want to write to a file.

In these cases, turning your data into strings should be easy enough. For instance, let's say you have a high score board, where you have multiple scores with the name of the player who got it, along with their score. We can create a file format where we write out each player on a single line, with the player's name and score separated by a comma. (This is a CSV file, by the way.)

So we'd want our file to look like this:

Kelly,1000
Steve,950
Angela,925
Oscar,900
Jim,840

Let's say we have a HighScore object that we created that has Name and Score properties.

To create the strings we want to write out to a file, we can simply do something like this:

List<HighScore> scores = new List<HighScore>();
 
// populate and maintain the list of high scores here as needed...
 
string allHighScoresText = "";
 
foreach (HighScore score in scores)
{
    allHighScoresText += score.Name + "," + score.Score + "\n";
}
 
File.WriteAllText("highscores.csv", allHighScoresText);

By the way, in case you haven't seen it before, when you are creating strings, there are special characters that you can place in your string. To make a special character, you start off with the \ character, saying that the next character is to be interpreted in a special way, rather than in the normal way. For example, \n indicates a newline character (or "move to the next line") instead of a literal n.

In reverse, to read the file back in, you can do similar stuff.

You may start off by reading the entire text of the file, then parse (the process of breaking up a block of text into smaller, more meaningful components) the file and turn it back into our list of high scores.

The following code reads in the high scores file we just created and turns it back into a high scores list:

string[] highScoresText = File.ReadAllLines("highscores.csv");
 
HighScore[] highScores = new HighScore[highScoresText.Length];
 
for (int index = 0; index < highScoresText.Length; index++)
{
    string[] tokens = highScoresText[index].Split(',');
 
    string name = tokens[0];
    int score = Convert.ToInt32(tokens[1]);
 
    highScores[index] = new HighScore(name, score);
}

You can see here that the Split method is going to be our good friend when we're reading stuff from a file. This breaks one string into smaller strings, splitting it where it runs into a particular character (the commas in this case).

Other Ways

If File's ReadAllLines/ReadAllText and WriteAllLines/WriteAllText isn't enough for you, there are a couple of other options. We won't get into too many details on either here, but I just want to make you aware of them.

The first alternative is to use streams. A stream allows you to read and write chunks of data a little at a time. You can get a stream from a file with File.OpenWrite or File.OpenRead, which returns a FileStream.

FileStream fileStream = File.OpenWrite("C:/Users/RB/Desktop/test3.txt");

The methods that a stream like FileStream provides are… extremely low-level. Rather than working with this directly, the typical thing to do is to use a StreamReader or StreamWriter, which gives you more advanced tools. These readers and writers convert higher-level commands to the low-level methods that the raw stream supports.

I won't get into it too much, other than to show a simple example and say that it works a fair bit like working with the console window and the Console class.

FileStream fileStream = File.OpenWrite("C:/Users/RB/Desktop/test3.txt");
TextWriter textWriter = new StreamWriter(fileStream);
 
textWriter.Write(3);
textWriter.Write("Hello");
 
textWriter.Flush();
textWriter.Close();

A bunch of overloads of Write allows you to write all sorts of different things, including integers and strings, as shown above. Flush pushes data out to a file instead of holding onto it in memory. Writing stuff to the file system is slow, so a stream writer will default attempt to hang on to chunks of stuff and do it in batches. You don't need to call Flush, but can do so strategically. Close will close out both the writer and the stream; you should call it when you are done.

The StreamReader class can be used to do the inverse, but the catch is that it doesn't have simple options for reading an int. You can call ReadLine to get a full line or ReadToEnd to get everything. This complicates things a bit because there just aren't a ton of tools available. You need to think carefully about how you want to write stuff out in order to be able to easily bring it back in and parse it all.

On the other hand, instead of writing text, you could also write to a binary file using a BinaryWriter and BinaryReader. A binary file cannot just be opened in a text editor (well… it can, but you won't be able to quickly make sense of it). But when you write out an int in binary, you know it will use four bytes and be able to just read the next four bytes back in as an int, making round-tripping data much easier.

I don't want to spend too much time on streams because, while there's a place for them, what you're doing is usually so simple that File.ReadAllLines and other simple methods are enough or so complicated that you don't want to do it all by hand using streams anyway.

Instead, you find a library that will do the hard parts for you. For example, storing data in JSON and XML is common. Rather than reading and writing all of that yourself, you would simply find a library that reads and writes JSON or XML and just use the classes that are defined in that library.

I don't want to dig into the details of reading and writing JSON or XML right now other than to point out that there is support for both of these in the Base Class Library, and you can look up their documentation and read up on them. (The JSON stuff can be found here: https://docs.microsoft.com/en-us/dotnet/api/system.text.json. The XML stuff can be found here: https://docs.microsoft.com/en-us/dotnet/api/system.xml.)

For most common formats, there is already some library out there to handle it in C#. You just have to search for it to find it and then learn how to use it. That last part isn't always easy, but it is usually much easier than writing all of the parsing code yourself, which can be a huge undertaking.

What's Next?

We've covered the basics of file I/O here, but there's still a lot more you could do with it. Still, it is probably enough of a start to begin imagining how you could move forward and give you some basic tools.

As we continue in the C# Crash Course, we'll continue to look at some of the other more advanced programming topics in C#. The next thing we'll look at is how to handle errors using exceptions.