C# Lists and Files
The course is part of this learning path
This course uses file reading and writing as a vehicle to illustrate important data validation concepts when mapping text data to objects and receiving user input. It demonstrates many of the essential elements present in business applications, that is: ingesting data from an external source, enabling users to modify that data, and saving the data back to permanent storage.
Robust and well-designed apps, especially concerning the user interface, need to validate input in terms of data format and applicability and provide feedback when entered data is not appropriate. We round off the course by implementing the data access layer design pattern that will enable the code to be easily adapted to other data sources.
- Read planet data from a text file into a list of planet objects within a solar system object
- Create a basic command-line user interface with data validation and feedback that writes updated planet data back to file
- Take the file reading and writing functionality and split it out into a class that returns planet objects so you can easily change data sources in the future
This course is ideal for anyone who already has some basic knowledge of C# and is looking to expand on that by learning about lists and files within the programming language.
The ideal prerequisite for this course is our C# Loops Deep Dive course, but if you know what the .NET List class is and how to create one, then you’ll be able to follow along with these lectures and demonstrations.
Demo Source Code
When it comes to manipulating list data, an application usually populates a list from an external data source like a file. In this demonstration, we will read a list of planets and their distances to the sun from a text file into a list of planet objects. I’ll start by creating a boilerplate console application and paste my planet class code into planet.cs. Not forgetting to add the supporting C# and .NET libraries. This is a simple class with two properties. The name of the planet is a string, and its distance to the sun is an int that relates to millions of kilometers. Please take note of the ToString method that planet overrides to print out the name of the planet plus its distance to the sun.
Next, I’ll create a solar system class that internally stores the planets in a list of planet type. I’ll give this class two constructors using ctor, followed by the tab key shortcut, one with no parameters and one with a string parameter that will be the name of the text file containing the planet information. I’ll add a using statement for the system.collections.generic namespace, which contains the list class. Before I go any further, I’ll create the text file to store the planet information with a new folder called data. It’s very simple, the planet’s name followed by a comma, then the millions of kilometers to the sun.
In the solar system constructor with the file name parameter, I will read the file using the .NET file class. File comes from the system.IO namespace, I/O standing for input-output. The file class is static, so we don’t create an instance of the file; we use the static methods of the File class. In this case, it will be File.ReadAllLines, taking the file name variable as its parameter. Read all lines returns an array of string, where each string corresponds to a line of text in the file. I’ll create a local string array variable called planets without an underscore and assign the file lines directly to it. We’ve already declared our planets list, so I’ll create a new instance of list planet, specifying the number of elements it should contain with the planets array length. This is more efficient than using the default list constructor, where the list has to grow as we add items to it. The next job is to turn our planet string into a planet object and add it to the planets list. We can do that by looping through the planet’s array, splitting each string at the comma into an array of two elements using the string class split method. The first being the name and the second being the distance. We then pass that information as parameters to the planet constructor, creating a new planet object and adding it to the planets list. There are two things to note here. I’m converting the second planet string to an integer with the Convert ToInt32 method as I pass the value to the planet constructor. Secondly, the planet constructor is being called within the list Add method. We end up having several methods nested within each other instead of performing each action separately, assigning it to its own variable, and then using the variable. This is common practice, but if the data we’re reading is incorrect, bits of it are missing, or the distance from the sun text can’t be converted into a number, we have a problem resulting in a runtime error. Let’s create an add planet method to add a planet after the solar system object has been created.
AddPlanet will take a planet object as its parameter and add it to the _planets list.
We want to print our solar system to the console, so like the planet class, I will override the base object ToString method. ToString, as the name implies, returns a string of text, so we can’t return our list of planets. I’m going to use the string builder class that can be found in the system.text namespace for creating my solar system string. String builder is designed specifically for this scenario and is much more efficient than adding strings together in a for loop, which used to be the way of joining strings. Once again, I know how many elements will be needed because I know the length of the list so I can create the string builder object with the correct capacity. For each planet in the planet list, I’ll use the append method of string builder and the planet class ToString method along with the Environment.newline constant to add each planet to the planets string builder. Environment.newline adds the appropriate carriage return character for whichever operating system you are targeting. Windows uses different characters to Linux and macOS to represent a new line. Finally, we call the string builder to string method to return the list as one piece of text.
I haven’t quite finished yet. I want to take all the read from file functionality and put it in a public method so we can also create the solar system with the base no parameters constructor. Having done that, I now need to call ReadFromFile from the second solar system constructor. I don’t want to have two places where I’m adding planets to the internal planet list, so I will replace _planets.add with the public add planet method.
Now that we have created our solar system class, let’s instantiate an instance of it and print the solar system to the console. Of course, we need to tell the solar system where it can find its planet data, so I’ll pass the planets.txt file to the constructor. There are a few operating system issues I want to address here regarding paths. I’m currently working in Linux, so the path separator is a forward slash; if this were Windows, it would be a backward slash. In C#, the backward slash is a special escape character, so a string literal would need two backward slashes. Another issue is the directory location. In VS Code, in Linux, the base path for the application is the project folder, so I could just give the relative path data/planets.txt. This may work in this scenario, but it may not when the app is run on another OS. .NET has an Environment class that helps you deal with cross-platform differences. I’ll use Environment.CurrentDirectoy to get the absolute path and assign it to a string variable currentDir to inspect the value within VS Code.
Clicking the mouse in the left-hand margin of the code file sets a breakpoint. A breakpoint tells VS Code to stop executing the program at this line when I run the app in debug mode. I’ll select Start Debugging from the run menu. Sure enough, execution has stopped at the line with the breakpoint. Placing the cursor over the currentDir variable displays its value, which happens to be the current directory path. On the left, VS Code is now displaying the debug panel. The local variables of the method currently executing are displayed at the top. You can watch particular variables of interest in the watch panel. At the top of the screen is a widget for controlling code execution while debugging. Step into takes us to the statement currently highlighted. In this case, we would switch to the SolarSystem constructor code. I don’t want to do that right now, so I’ll click step over, which will execute the current line and move to the following line within the current method. VS Code lets me look inside objects while debugging. If I watch solar system, I can see the planets’ data and even drill down into each one. This is a very helpful feature for understanding what is happening inside your code while it's executing. We don’t need the currentDir variable as I can use string interpolation to build the planets.txt file path as I pass it to the solar system constructor.
When I run that, we get the expected output. It turns out that reading the file was a one-line operation taken care of by .NET. The parsing of the file, extracting the data from it, and turning it into class objects, is the bulk of the coding.
Before I finish, I want to show you the differences I talked about with Windows and Visual Studio. In Visual Studio, I’ve got the same code and set the breakpoint in the same place. When I run the app, not only do we get the double backward slashes for the windows style file path, but the current directory is the output directory, not the project folder. Continuing to execute the code results in a file not found error, because, well the file isn't located where I'm telling it, it is. I’ll concatenate the current directory with the planets.txt path and run again. Still, the same file is not found error, which makes send because the data folder is in the project directory, not where the executable is output. There are several solutions to this problem, but to keep consistent with the Linux code version, I’ll select planets.txt in the solution explorer and change the Copy to output directory property to Copy if newer. The Data directory doesn’t exist in the output folder until I rerun or rebuild the app. Now when I debug with Visual Studio, it works.
Hallam is a software architect with over 20 years experience across a wide range of industries. He began his software career as a Delphi/Interbase disciple but changed his allegiance to Microsoft with its deep and broad ecosystem. While Hallam has designed and crafted custom software utilizing web, mobile and desktop technologies, good quality reliable data is the key to a successful solution. The challenge of quickly turning data into useful information for digestion by humans and machines has led Hallam to specialize in database design and process automation. Showing customers how leverage new technology to change and improve their business processes is one of the key drivers keeping Hallam coming back to the keyboard.