Saving Objects With C#
Sections
- Background
- Saving Our Object
- Restoring Our Object
- Building Our Settings Class
- Putting It All Together
- Final Thoughts
Background
When I first started programming, I would often attempt to create configuration files to save various information that I wanted to persist across instances of the application I was writing. I remember the first configuration I ever wrote and thinking about it now just makes me cringe. I manually imported the file and iterated over it line by line. I would split the line by the = symbol and I had a series of if statements that checked the string and assigned it to whatever variable.
The function grew quite large and it soon became far too complicated to add new settings. Much to my dismay, I did not learn from this mistake at first and I continued to use this same method until my co-worker introduced me to the built-in application settings that you can utilize in .Net.
At first, I thought this was a terrific solution. On the surface is it easy to use and there don’t seem to be many downsides. However, after a while, I began to see things that I didn’t like about it. One of the first things I had a problem with was the fact that the settings can only be saved globally if they never change. That means you must commit to the value of those settings at design time rather than being able to change them at will.
If you choose to save them on a per-user basis, you can change the value of the settings whenever you want, however, when a different user logs in and runs your application, the settings will start out at their defaults until they are changed. This means they have to be changed for every single user who runs your application.
The next problem that I encountered is that when I saved the settings I began to see the user.config file become corrupt. At first, I didn’t think anything of it because from my point of view, it was simply a fluke. However, in the past few years, I have seen the user.config file become corrupt on a lot of different machines and I have never been able to figure out why.
This is when I decided it was time for another solution. This time I went searching and I came across a thread on Stack Overflow. In this thread, there was a nice snippet of code that showed a method of saving an object to the disk. I immediately typed it up and tried it out and it is the solution I have been using ever since. I fell in love with this method, and so this is the method that I want to present here today.
Saving Our Object
The original code snippet had a hardcoded implementation, so I altered it to make it use generics so that it was multi-purpose. The method here is relatively simple and versatile. There are two methods and both will be static. The first method is the Save method and this will be used to store an object to the disk.
*** Please note that I have stripped out error checking for the sake of simplicity ***
public static void Save(T obj, string filename)
{
BinaryFormatter bf = new BinaryFormatter();
// Create a file with the specified filename
FileStream fs = new FileStream(filename, FileMode.Create);
// Serialize the provided object and output to filestream
bf.Serialize(fs, obj);
// Flush the filestream buffer and close
fs.Flush();
fs.Close();
}
The Save method takes two arguments. The first is the object that you are going to save. The second argument is the filename that you are going to use to store it on disk. There are a couple of different methods we can use, but for this example, I have chosen to store it on disk in binary format. You can use XML or even JSON if you want. There are links at the top of the page for various versions.
The first line of the Save method in declaring a BinaryFormatter. This is what we will use to convert the object into the form that can be saved to disk. Next, we declare our FileStream. The FileStream takes our filename argument and a mode. In this case we are creating a new file so we will use FileMode.Create. In the third line, we are feeding the BinaryFormatter, the filestream and the object to be saved. The BinaryFormatter will convert the object to binary format and the output will be stored in our FileStream.
The last two lines are writing the changes to disk and closing out the FileStream. This is a simple and quick solution to saving our settings to disk. However, saving the settings to disk is only half the battle. We also need to be able to read the file back into our program.
Restoring Our Object
Now that wasn’t so bad, was it? We have the code to save our object to disk, but this is only half the battle. This code is no good without the ability to restore it to a usable object in your application. To so that, we need to create a Restore method like the one below.
public static T Restore(string filename)
{
// Create a default for value types or null for reference types
T e = default(T);
// if specified file doesn't exist, return
if (!File.Exists(filename))
return e;
BinaryFormatter bf = new BinaryFormatter();
// Open file
FileStream fs = new FileStream(filename, FileMode.Open);
// Deserialize file contents and cast to specified type
e = (T)bf.Deserialize(fs);
// Close the filestream
fs.Close();
return e;
}
There are a few more lines in our Restore method than we had in the Save method, but the bulk of it should look familiar. This method only takes a single argument. We only need to supply a filename to this method. If you notice, this method returns a generic object. This is key to making it useful for more than one type of object.
In the very first line, we are simply going to create a default of whatever type of object has been specified. We will see an example of how this works later on. We then need to do a quick check to see if the filename that was passed in is a valid file. If not, we just return the default object. You could also choose to throw an exception here, but I did not choose to do that.
The next two lines are very similar to what we see in the Save method. The only difference is that we use the file mode Open to open the specified file instead of Create to create a new one. We will then use a try catch block in case the contents of the file are not what we expect them to be.
We are going to take the contents of the file that we opened and we are going to pass it to the BinaryFormatter to deserialize it back into an object. The (T) is casting it back into the type that we want. If the contents of the file were unable to be deserialized. Finally, we are closing the file stream and returning the object.
Build Our Settings Class
Now that we see how these methods are built, let’s take a look at an example. Below, I have created a very simple class that represents the application’s settings. We will use this to save and restore.
// Must have this attribute
[Serializable]
public class MyAppSettings
{
public string ApplicationName { get; set; }
public string LogPath { get; set; }
public DateTime LastRunTime { get; set; }
public int NumTimesAppOpened { get; set; }
}
You will notice a few things. First, the MyAppSettings class contains only properties. This just makes things simple for the demonstration, however, you can add fields if you wish. Just keep in mind that anything marked private will not be saved.
The next thing that should be noted is the Serializable class attribute. This attribute is essential. If it is not included you will get an exception when our code tries to serialize the class. If you have properties in the settings that are object you created, you need to ensure that those are also marked Serializable.
Putting It All Together
Whew! I know this post has been long. But we are nearing the end. There is a light at the end of the tunnel. We have our code to save our object, restore our object, and a sample settings class. We just need to put the code together so you can see how it should work. For this, I am just using a simple console application. Generally you are going to want to have the code to save the settings associated with a button click or a console command, and the restore either on boot up or command.
static void Main(string[] args)
{
// Create and instance of the settings class
MyAppSettings settings = new MyAppSettings();
settings.ApplicationName = "My Application";
settings.LogPath = ".\\MyAppLog.txt";
settings.LastRunTime = DateTime.Now;
settings.NumTimesAppOpened = 1;
// Save the settings to disk with the filename "settings"
BinaryFormattedObject<MyAppSettings>.Save(settings, ".\\settings");
// Read the file and store it in settings
settings = BinaryFormattedObject<MyAppSettings>.Restore(".\\settings");
}
In the above code, you can see the MyAppSettings class just represents a class used to store my application settings. The one thing to note about this class is that it must have the serializable attribute or it will throw an exception when you try to serialize it.
In the second snippet, I am creating an instance of the class and setting some values. I then call the Save method. I pass it the settings class that I just created and the filename to use to save it to the disk. This will save a file in the current working directory called settings.
The last line is demonstrating how to read the file and restore the settings. Because the restore method returns a generic, it must be provided with a type. You do this by putting the type you expect to read from the file in between the <> symbols. If the file does not contain this type of object an exception will be thrown at run time. You must also pass the Restore method the filename to read from the disk.
Final Thoughts
I am sure there are many other methods out there to accomplish the same thing, but this is the method that I found that has worked very well for me. It is simple, it is quick, and I haven’t had any issues with it. These methods can also be extended which makes this my preferred method of saving settings.
If you made it this far and have any comments or questions, feel free to leave me a comment below. All code is linked at the top of this post.