You can use different Stream classes to read and write files, including Stream classes for text files and binary files, and specialized classes to allow you to compress data or store data in memory. You can also use streams to store files in isolated storage, a private file system managed by the .NET Framework.
At the end of this tutorial, you will be able to:
- Read and write text files, binary files, and strings
- Write to a stream stored in memory
- Compress a file when writing it
- Use isolated storage to improve the privacy of data you store
The .NET Framework includes classes that make it easy to read and write text files. For example, the TextWriter and TextReader classes are stream classes that can be used only with text files.
To write a text file, follow these steps:
Create a new instance of TextWriter. TextWriter does not have its own constructor, but you can use the StringWriter constructor, or call File.CreateText().
Call TextWriter.Write and TextWriter.WriteLine to save text to the file.
Call TextWriter.Close to close the file.
The following code sample demonstrates this:
TextWriter tw = File.CreateText("output.txt tw.WriteLine("Hello, world!"); tw.Close();
Reading Text Files
The quickest way to read an entire text file is to create an instance of the TextReader class and then call the TextReader.ReadToEnd method. As with most file operations, be sure to call TextReader.Close afterwards.
The following code sample demonstrates how to use TextReader:
TextReader tr = File.OpenText(@"C:\file.txt"); Console.Write(tr.ReadToEnd()); tr.Close();
You can save non-text data to a file using two different techniques: binary streams and serialization. Serialization, discussed in Module 5, is typically the more efficient choice. However, you should also be familiar with binary streams.
Use the BinaryReader and BinaryWriter classes in much the same way you use TextReader and TextWriter. BinaryReader and BinaryWriter are different in a couple of important ways:
When you create either class, you must provide an instance of the FileStream class to specify which file to read or write.
You can write non-text types, including character array, byte arrays, a variety of number types, and Boolean values.
When reading from the binary file, you must use a method specific the type that you are reading. For example, if you write a 32-bit integer, you must read a 32-bit integer using the BinaryReader.ReadInt32 method. Similarly, you must use the BinaryReader.ReadBoolean method to read a Boolean value.
// Create a file FileStream fs = new FileStream("data.bin", FileMode.Create); BinaryWriter w = new BinaryWriter(fs); // Write data to the file w.Write(32.15d); // Write an double w.Write(16); // Write an Int32 w.Write(false); // Write a Boolean value w.Write("Hello, world!"); // Close the file w.Close(); fs.Close(); // Open the file for reading fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // Read the data from the file Console.WriteLine(r.ReadDouble()); Console.WriteLine(r.ReadInt32()); Console.WriteLine(r.ReadBoolean()); Console.WriteLine(r.ReadString()); // Close the file r.Close(); fs.Close();
Using Streams (Presentation)
Watch my presentation about using streams and storing files with the .NET Framework:
Here’s the full transcript:
The .NET Framework uses streams to send data to a variety of different medias. For example, the most common use is to send data to a file on the file system. You can do this using several different Stream variants, such as TextWriter or BinaryWriter. The most basic of those is the StreamWriter class. Using StreamWriter, you can specify a filename in the constructor, and then use the StreamWriter.Write method to save data to that file. If you need to read data from that file later, you would use the StreamReader class.
You can also layer other classes on top of StreamWriter. If you want to write a compressed file, you would create an instance of the GZipStream class. Then, when you create your StreamWriter class, you provide that GZipStream instance as part of the StreamWriter constructor. Then, anytime you call StreamWriter.Write, that data is funneled through the GZipStream instance. GZipStream compresses the data using a type of compression called GZip compression. When you need to read the data back later, you can’t just use an ordinary StreamWriter, or the data will come back as gibberish. So, you need to do exactly the same thing in reverse—you create a GZipStream instance set to decompress the data, and then create a StreamReader instance using your GZipStream instance. Then, you’ll be able to read directly from the compressed file.
The .NET Framework actually provides two different compression methods for writing compressed files. The one most people use is GZip, that I just discussed. It’s a more effective compression method than the Deflate compression method. They’re just different compression methods; they accomplish the same thing. GZip is more common; it’s even built into the file system. Deflate is a little less common and a little less efficient. So, either one will compress a file, but the compressed file will probably be a little smaller if you use GZip.
Now, the beauty of using streams is you can use a variety of different media, not just the file system. In the .NET Framework, you probably won’t write to the file system unless you need to access the file in a different application. Instead, you should use Isolated Storage. Isolated Storage is stored on the file system, so it is permanent, and you can close your application and re-launch your application and the data will still be there. Isolated storage is, in fact, a file on the file system. But, the .NET Framework treats it as its own entity. So, an application can write to Isolated Storage and read it back almost as if it were part of the file system, but it has some benefits that writing directly to the file system does not provide. First, Isolated Storage is protected by the .NET Framework. Different users cannot access Isolated Storage. Different assemblies cannot access another assembly’s isolated storage. Perhaps most importantly, Isolated Storage works with the security principal of least privilege because it takes fewer .NET Framework privileges to Isolated Storage than it does to write directly to the file system.. So, if you are writing a file that will only be read back by your assembly and from the same user, you should always write to Isolated Storage instead of the file system.
When you use Isolated Storage you can protect it in two different ways. You can specify the user domain, which means only that assembly run by that user can access files in Isolated Storage. Or, you can specify protection for the application domain. If you specify application domain, only other assemblies running in the same application domain can access the isolated storage. Application domains really only become important when you’re writing multi-threaded applications or applications that spawn other assemblies. So, at this point, you probably won’t use it. But, we will discuss application domains in later modules.
Another way to use StreamWriter is to write through the MemoryStream class. Just as we discussed when using GZipStream or DeflateStream, you will create an instance of MemoryStream, and then specify the instance when you create your StreamWriter instance. Then, when you call StreamWriter.Write to save data, it will be processed by the MemoryStream class. MemoryStream completely bypasses the file system and stores the data in the computer’s RAM, in the memory itself. So, it’s not persistent. If you save data to a MemoryStream instance and the user closes the application, the data’s gone. If the computer shuts down, the data’s gone. It’s stored in the computer’s memory, so it’s only persistent while the application and the computer are running. For that reason, you wouldn’t normally use MemoryStream unless you need to prepare a file to be written to the file system later. For example, if you need to gradually create a file over the course of five minutes—perhaps you’re querying a database server and processing data and then outputting it to a file, you might write that data to a MemoryStream, and then once the data had been processed, you might copy the data to the file system using another instance of StreamWriter. That might be useful when multiple users might be accessing the same file, or when you need to minimize the number of writes that you make to a particular file. So, think of a MemoryStream as a way to pre-write data to a file. But, remember that it’s not backed by the file system, so if your application is interrupted, it’s gone forever.
Reading and Writing Binary Files (Video Demonstration)
Watch me demonstrate using binary files–be sure to watch it full screen in HD. Here is the source code: Using BinaryReader and BinaryWriter in C#.
Here’s the full transcript:
Now we’re going to create a console application that rights the current time to a binary file, and later reads it back and displays it to the console. I’m going to create two methods: the SaveCurrentTime method and the ShowPreviousTime method. I’m going to call each of these methods from the main routine. Then, in the main routine, I’m also going to call Console.ReadKey just to pause the display.
So, in SaveCurrentTime, as you might recall from the course, the first thing I need to do is to create a FileStream object. This FileStream object is required to create the BinaryWriter object that I’ll need to write it with. Of course, FileStream requires the System.IO namespace.
So, when you create the FileStream object you need to specify the filename. I’m just specifying a name here that will drop it in the same directory, the default directory, which is the same directory the assembly is running from. You could, of course, specify a full path.
Notice that the BinaryWriter constructor takes the FileStream object as a parameter.
Now, I can make write calls to the BinaryWriter instance, but notice that there’s no Write override for writing a full DateTime object. So, I can’t write an entire DateTime.Now object. I can however write integers, so what I’ve done is write DateTime.Now.Hour, which is an integer, and DateTime.Now.Minute, which is also an integer.
When I go to read it, I need to read it in the exact same order. I would get an error if I were to, say, read a double instead of an integer. So, again, I need to create a FileStream object. This time, instead of specifying the FileMode.Create parameter, I just open it for reading. Instead of creating a BinaryWriter object, I create a BinaryReader object. Basically, reading is exactly the same as writing.
Now I’m creating a string called previousTime. I’m creating it by reading in the hours and minutes from the binary file. Now, when I wrote, I could use the overridden write method and add in any of the supported types. But, when I read, I need to specify the type. Here, I’m calling ReadInt32 for both the hours and minutes. There are also read methods for doubles, longs, character arrays, and a variety of other types—but not DateTime.
Now, I can run this, and you can see it writes the time, 4:33pm, and reads it back.
Using Compressed Files (Video Demonstration)
Now, watch this video demonstrating how to use compressed files in C#. Here’s the source code: Using Compressed Files in C#.
Here’s the full transcript:
Now I’m going to create a C# application that writes a lot of data to a compressed file. First, we’re going to write it to a standard file so we can compare the size before and after.
We know we’re going to be working with the file system, so let’s add the System.IO namespace.
First, let’s demonstrate the use of the StreamWriter class. Obviously, there are many different ways to create a file, but StreamWriter is one you should be familiar with. I will simply specify the name of the file here.
Now, let’s create a For loop to write “hello, world” 10,000 times. We’re going to call StreamWriter.Write to write it out.
Now, we just have to close the StreamWriter.
Reading it in is just as straightforward. We’re going to create a StreamReader instance, I’ll call it sr, and specify the filename. We can simply call Console.WriteLine and specify the StreamReader.ReadToEnd method. That just reads all of the data in from the file as one big string.
In practice, StreamWriter and StreamReader can handle binary data and buffer arrays, but it’s a little complex to actually read it. It’s easier to write than it is to read, so you might end up using the BinaryWriter and BinaryReader classes instead.
So, once again, we need to close it once we’ve read in all the data.
Just for fun, so we can see how effective the compression is, I’m going to create a FileInfo instance and use it to display the size of the file, both compressed and uncompressed.
FileInfo.Length is the actual size of the file in bytes.
One last call to Console.ReadKey to prevent the console from disappearing when we’re done. Now, I’ll run it.
That’s a lot of “Hello, worlds!” You can see that the entire file amounts to 129,987 bytes.
We’ll do that one more time. This time, we’re going to use the GZipStream class, which requires the System.IO.Compression namespace. To write to a compressed stream, you need to create the GZipStream instance, and then use it when creating your StreamWriter constructor. We’re going to specify File.Create to specify the filename, and I’ll use the same filename. And, you specify the compression mode, which is either Compress or Decompress. Because we’re writing here, we’re going to be doing compression.
Now, in the StreamWriter, instead of specifying the file name, I’m going to specify the GZipStream instance that we just created. That is sufficient to write the file—I just need to be sure to close the GZipStream instance.
I need to do the same thing to read the file—I create a GZipStream instance and specify that instance when I create the StreamReader. Notice this time the CompressionMode is going to be Decompress. I’m reading it in—reading a compressed file and decompressing it.
Once again, I’m specifying the GZipStream instance instead of the filename in the StreamReader. I don’t need to change the code to read or write within the StreamReader, because that’s abstracted.
That’s going to do it—I’ll run it and look at the bugs. I forgot a parenthesis. Happens to everybody! We’ll try this one more time.
Last time the file size was 129,987 bytes. This time it is 1,576 bytes. You can see that was a ridiculous compression—we got down to about 1.4% of the file size. You won’t normally see those kind of results, unfortunately, unless you’re compressing a text file that has the same pattern repeated over and over. If you were to try to compress a picture file, you would get about 0% compression. Most data files will give you 20-30%. Your results will vary, but for text, it’s definitely worthwhile.
Return to the .NET Framework Fundamentals Tutorials Table of Contents.