Versioning Serialized Files with Managed C++

Welcome to this week’s installment of .NET Tips & Techniques! Each week, award-winning Architect and Lead Programmer Tom Archer demonstrates how to perform a practical .NET programming task using either C# or Managed C++ Extensions.

In the previous article on .NET serialization using Managed C++, I illustrated how to uniquely identify serialized files using a GUID as a class member. In this installment, I’ll present a step-by-step technique for versioning serialized files so that a file’s application can properly handle multiple versions of its file formats.

A Versioning Example

The following step-by-step instructions use an example class called YourClass that has gone through three (3) versions, with a data member being added with each version. In this example, you’ll make sure that any of the three file versions can be successfully opened—and if necessary, converted—with your test application.

  1. Mark the class as serializable and implement the ISerializable interface. As I covered in previous columns, this gives you complete control over the serialization process. You need it here to enable the class to verify the file’s format version when the file is being read. Therefore, the first step is to apply the Serializable attribute to the class and derive from (or implement) the ISerializable interface:
  2. using namespace System::IO;
    using namespace System::Runtime::Serialization;
    using namespace System::Runtime::Serialization::Formatters
                          ::Binary;
    ...
    [Serializable]
    __gc class YourClass : public ISerializable
    {
    };
    
  3. Call an initialization method (implemented and explained shortly) from the class’s constructor:
  4. public:
      YourClass()
      {
        Init();
      }
    
  5. Define a class member that will contain the current version supported by the application. In this example, the YourClass class supports three versions (1.00, 1.10, and the most current, 1.20) for testing purposes:
  6. protected:
      static String* fileVersion = S"1.20";    // updated each time
                                               // file format is
                                               // changed
    
  7. Add the class’s data members. For maintenance purposes, I like to block off each version’s members with comments, so that anyone maintaining my code can easily see which members were added in which version:
  8. protected:
      // version 1.00
      String* fieldA;
    
      // version 1.10
      String* fieldB;
    
      // version 1.20
      String* fieldC;
    
  9. Implement the initialization method that is called from the class’s constructor and initialize the class’s members:
  10. protected:
      void Init()
      {
        this->fieldA = S"Version 1.00 field";
        this->fieldB = S"Version 1.10 field";
        this->fieldC = S"Version 1.20 field";
      }
    
  11. Implement the ISerializable::GetObjectData method to save the desired data when the object is serialized to disk:
  12. public:
      void GetObjectData(SerializationInfo *si, StreamingContext sc)
      {
        si->AddValue(S"fileVersion", this->fileVersion);
        si->AddValue(S"fieldA", this->fieldA);
        si->AddValue(S"fieldB", this->fieldB);
        si->AddValue(S"fieldC", this->fieldC);
      }
    
  13. Now, to the main part of checking version information. The first thing to do is call the initialization routine to set any member variables. This is necessary because if the code is reading in a “1.10” formatted file, members added in subsequent versions won’t be read and therefore won’t have values.

    The “fileVersion” field is then read from the serialized file into a temporary variable (tempFileVersion). Then, this temporary value is compared to the class’ static fileVersion value. If the file format is newer than the class’s value, an exception is thrown. This is because you would never want to convert a file that was created with a newer version of the application and have the user lose data.

    From there, each version is represented. Note how the comparisons work. So that code is not duplicated, the first comparison checks for the lowest supported version and higher. Therefore, the first comparison is stating that if the file’s version value is 1.00 or anything greater, read the fieldA value. The code then continues through each version until it reaches the current version, where the version number must match exactly.

    In summary, this method ensures that any current or older versions of the file format will be read successfully (and written as the current version), whereas any files with a format newer than the application will be rejected. Obviously, you might need to insert application-specific logic to determine which versions you can automatically convert based on the application’s ability to initialize members introduced in later file format versions:

    protected:
      YourClass(SerializationInfo *si, StreamingContext sc)
      {
        Init();
    
        // Check version
        String* tempFileVersion = si->GetString(S"fileVersion");
    
        // Make sure that the file format is not newer than the
        // application
        if (String::Compare(this->fileVersion, tempFileVersion) < 0)
          throw new Exception(S"Invalid version!");
    
        if (String::Compare(tempFileVersion, S"1.00") >= 0)
           // 1.00+
        {
          this->fieldA = si->GetString(S"fieldA");
        }
    
        if (String::Compare(tempFileVersion, S"1.10") >= 0)
           // 1.10+
        {
          this->fieldB = si->GetString(S"fieldB");
        }
    
        if (String::Compare(tempFileVersion, S"1.20") == 0)
           // 1.20
        {
          this->fieldC = si->GetString(S"fieldC");
        }
      }
    

Testing the Example

  1. At this point, the class is complete. Now, you need only write two very simple functions to serialize this class’s members. As I mentioned in previous articles, I personally implement these methods as static methods—called Open and Save—in a helper class. For those of you accustomed to MFC serialization and having the class serialize itself, that unfortunately won’t work with .NET serialization because the Formatter object does the actual serialization and instantiates a new object during the read. This is different from MFC, where the class can simply open the CArchive object and perform its own insertion and extraction. Hence, the need for another class—or the client code—to make the calls to perform the serialization:
  2. __gc class YourClassSerializer
    {
    public:
      static YourClass* Open(String* fileName)
      {
        YourClass* yc = NULL;
    
        FileStream* stream = new FileStream(fileName,
                                            FileMode::Open,
                                            FileAccess::Read);
        BinaryFormatter* formatter = new BinaryFormatter();
        yc = static_cast<YourClass*>
             (formatter->Deserialize(stream));
        stream->Close();
    
        return yc;
      }
    
      static void Save(YourClass* yc, String* fileName)
      {
        FileStream* stream = new FileStream(fileName,
                                            FileMode::Create,
                                            FileAccess::Write);
        BinaryFormatter* formatter = new BinaryFormatter();
        formatter->Serialize(stream, yc);
        stream->Close();
      }
    };
    
  3. Finally, you need only call the two static helper class functions to serialize your files. Note the reference to the Exception::InnerException object. This is due to the fact that when an object being serialized throws an exception, the Formatter object will catch that exception and wrap it in its own exception that is then thrown.

    One way to test this code is to create and save a file with a given version (fileVersion member) such as 1.10, change the fileVersion member to 1.20, and then open the “1.10” file. You’ll see that your code automatically converts the “1.10” file to a “1.20” file when you save. Conversely, if you save the file as a “1.20” version, modify the fileVersion to “1.10”, and then try to open the file, the code will throw an exception as designed:

    try
    {
      YourClass* yc;
    
      // create and save test file
      yc = new YourClass();
      YourClassSerializer::Save(yc, S"test.yc");
    
      // open existing file
      // yc = static_cast<YourClass*>
      //      (YourClassSerializer::Open(S"test.yc"));
    
      // save current file
      // YourClassSerializer::Save(yc, S"test.yc");
    }
    catch(Exception* e)
    {
      Console::WriteLine(e->Message);
      if (e->InnerException)
        Console::WriteLine(e->InnerException->Message);
    }
    

Additional Notes

In the YourClass example (and as I mentioned in the description of the GetObjectData method), the code automatically converts older file formats to the current format. You’ve probably seen many applications that—upon opening an older-formatted file—ask you whether or not you want to convert the file. Here’s an easy way to implement that:

  1. Define a property in the class called something like AutoConvertEarlierFormats, initializing it to false.
  2. In situations where the file format is older than the latest version, check to see whether the AutoConvertEarlierFormats property is true. If it is, initialize the members not previously saved according to your application’s needs. If the value is false, throw an exception that the client code knows how to handle that indicates that the file format is older but can be converted.
  3. The client code then asks the user whether or not he or she wants to convert the file.
  4. If the user does, simply set the AutoConvertEarlierFormats property to true and retry the open operation by which the file should be successfully opened and automatically saved to the current file-format version when the file is serialized.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read