How to save and load objects to and from file in C#
Persistency is almost always a requirement for applications that are meant for serious usage. Perhaps we want the data that our C# program had harvested or generated to be available everytime it runs. Or for that load of data that we are unable to send to a server to be remembered, so that we can try sending at a later time.
Because most of the data that is held by a C# application at runtime is in the form of objects, it is convenient to be able to save and load objects to file directly. Such capability is dubbed object serialization, and like many other programming languages, C# has the facilities to perform object serialization for developers.
Remembering our friends
Suppose we want to write a C# program that can remember our friends' email addresses. Instead of using System.Data.SQLite, let's use object serialization to achieve the data persistence aspect of our program.
The serializable Friend class
In order to be able to save and load objects, the class has to be indicated as being serializable. We mark a class with the Serializable
attribute to indicate that we want to be able to save its objects:
using System; using System.Text; [Serializable] public class Friend { private string name; private string email; public Friend (string name, string email) { this.name = name; this.email = email; } public string Name { get { return this.name; } set { this.name = value; } } // end public string Name public string Email { get { return this.email; } set { this.email = value; } } // end public string Email } // end public class Friend
Here, the Friend
class is a data wrapper class that we will use to hold the name and email address of a particular friend. As we add more friends to our program, more Friend
instances will be created. Such instances will be maintained by the FriendsInformation
class.
The FriendsInformation class
Let's create a singleton class, FriendInformation
, that will be responsible for the saving and loading of information of our friends.
using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; using System.Text; public class FriendsInformation { private static FriendsInformation friendsInformation; private Dictionary<string, Friend> friendsDictionary; private BinaryFormatter formatter; private const string DATA_FILENAME = "friendsinformation.dat"; public static FriendsInformation Instance() { if (friendsInformation == null) { friendsInformation = new FriendsInformation(); } // end if return friendsInformation; } // end public static FriendsInformation Instance() private FriendsInformation() { // Create a Dictionary to store friends at runtime this.friendsDictionary = new Dictionary<string, Friend>(); this.formatter = new BinaryFormatter(); } // end private FriendsInformation() public void AddFriend(string name, string email) { // If we already had added a friend with this name if (this.friendsDictionary.ContainsKey(name)) { Console.WriteLine("You had already added " + name + " before."); } // Else if we do not have this friend details // in our dictionary else { // Add him in the dictionary this.friendsDictionary.Add(name, new Friend(name, email)); Console.WriteLine("Friend added successfully."); } // end if } // end public bool AddFriend(string name, string email) public void RemoveFriend(string name) { // If we do not have a friend with this name if (!this.friendsDictionary.ContainsKey(name)) { Console.WriteLine(name + " had not been added before."); } // Else if we have a friend with this name else { if (this.friendsDictionary.Remove(name)) { Console.WriteLine(name + " had been removed successfully."); } else { Console.WriteLine("Unable to remove " + name); } // end if } // end if } // end public bool RemoveFriend(string name) public void Save() { // Gain code access to the file that we are going // to write to try { // Create a FileStream that will write data to file. FileStream writerFileStream = new FileStream(DATA_FILENAME, FileMode.Create, FileAccess.Write); // Save our dictionary of friends to file this.formatter.Serialize(writerFileStream, this.friendsDictionary); // Close the writerFileStream when we are done. writerFileStream.Close(); } catch (Exception) { Console.WriteLine("Unable to save our friends' information"); } // end try-catch } // end public bool Load() public void Load() { // Check if we had previously Save information of our friends // previously if (File.Exists(DATA_FILENAME)) { try { // Create a FileStream will gain read access to the // data file. FileStream readerFileStream = new FileStream(DATA_FILENAME, FileMode.Open, FileAccess.Read); // Reconstruct information of our friends from file. this.friendsDictionary = (Dictionary<String, Friend>) this.formatter.Deserialize(readerFileStream); // Close the readerFileStream when we are done readerFileStream.Close(); } catch (Exception) { Console.WriteLine("There seems to be a file that contains " + "friends information but somehow there is a problem " + "with reading it."); } // end try-catch } // end if } // end public bool Load() public void Print() { // If we have saved information about friends if (this.friendsDictionary.Count > 0) { Console.WriteLine("Name, Email"); foreach (Friend friend in this.friendsDictionary.Values) { Console.WriteLine(friend.Name + ", " + friend.Email); } // end foreach } else { Console.WriteLine("There are no saved information about your friends"); } // end if } // end public void Print() } // end public class FriendsInformation
Encapsulating some facilitators
To facilitate the representation, addition and removal of friends information, we maintain an instance of the System.Collections.Generic.Dictionary
class as an instance variable.
On the other hand, to be able to serialize (save) and deserialize (load), we maintain an instance of the System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
class.
Adding new friend
The AddFriend
method allows callers to add the name and email of a friend to the underlying Dictionary
instance, via its Add
method. If we had already added a friend with the same name, the friend will not be added.
Removing a friend
The RemoveFriend
method allows callers to remove a friend, based on his name, from our underlying Dictionary
instance, via its Remove
method. If there is no friend with the supplied name, no removal will take place.
Saving Friend objects to file
The Save
method is all about writing to file. We first obtain an instance of System.IO.FileStream
which has write access to friendsinformation.dat
.
If we are successful in doing so, we use the Serialize method of our BinaryFormatter
instance to save the entire friendsDictionary
to friendsinformation.dat
.
Loading Friend objects from file
On the contrary, the Load method is all about reading from file. We first check whether there are previous calls to the Save
method by checking the existence of friendsinformation.dat
.
If friendsinformation.dat
exists, we first obtain an instance of FileStream
that has read access to friendsInformation.dat
. We then use the Deserialize to read from the FileStream
instance.
When the Deserialize
method is called successfully, a Dictionary
instance, which contains Friend
objects that were previously saved to file, will be created. This Dictionary
instance is then set to the friendsDictionary
variable.
Printing of friends' information
The Print
method allows us to inspect the Friend
objects that are maintained by our program at runtime. It iterates the list of Friend
objects and outputs the name and email.
A simple command line application
To sum up this post, let's create a command line application that we can use to remember the names and email addresses of our friends.
using System; using System.Collections.Generic; using System.IO; using System.Text; public class Program { private static string programName = Path.GetFileNameWithoutExtension(System.AppDomain.CurrentDomain.FriendlyName); public static void Main(string[] args) { if (args.Length < 1) { PrintUsage(); } else { FriendsInformation fi = FriendsInformation.Instance(); fi.Load(); String command = args[0].ToLower(); if (command == "list") { fi.Print(); } else if (command == "add") { if (args.Length >= 3) { String name = args[1]; String email = args[2]; fi.AddFriend(name, email); fi.Save(); } else { PrintUsage(); return; } // end if } else if (command == "remove") { if (args.Length >= 2) { String name = args[1]; fi.RemoveFriend(name); fi.Save(); } else { PrintUsage(); return; } // end if } else { Console.WriteLine("Command not found: " + command); PrintUsage(); } // end if } // end if } // end public static void Main(string[] args) public static void PrintUsage() { Console.WriteLine("Usage: "); Console.WriteLine(programName + " list"); Console.WriteLine(programName + " add <name> <email>"); Console.WriteLine(programName + " remove <name>"); } // end public static void Main(string[] args) } // end public class Program
The application accepts three commands via the command line:
- list displays the names and email addresses of all our friends which we have added to our program.
- add allows us to add a new friend to our program.
- remove allows us to remove a friend from our program.
For each of the 3 commands, the FriendsInformation
singleton is created and called upon to attempt to load previously saved Friend objects into runtime.
Except for the list command, the Save
method of the FriendsInformation
is called to save any changes made to the collection of Friend objects back into friendsInformation.dat
.
After several execution of the add and remove commands, friendsInformation.dat
should appear in the same folder as our command line application. To check whether our command line application can remember (or forget) our friends, we can execute the list
command.