Versioning Application Data in Universal Applications

March 5th 2016 Universal Apps

New versions of applications introduce new features. Sooner or later this inevitably causes the format of locally stored application data and settings to evolve, as well. To avoid loss of data or even application crashes, newer application versions must be able to at least read older data formats or convert them to its own native format. The option of having data roamed between different devices with potentially different versions of application, can make handling data format changes even more complex.

Universal Windows applications run in a sandbox, therefore they can only store their data locally in a dedicated folder provided by the OS. Programmatic access to it is allowed only through the ApplicationData class, which has some built-in support for handling versioning of the data, albeit not documented very well. Apart from the reference documentation there's only one very minimalistic example demonstrating how to use it.

Any code using ApplicationData without paying attention to data versioning will automatically use version 0. This will work fine, as long as data format doesn't have to change. Once that happens, it's time to start explicitly versioning the data, i.e. start using a higher version. There's no need for the application data version to match application version, therefore you'll probably want to start at 1. At application startup you can now check the application data version and convert it if it doesn't match the current version:

if (ApplicationData.Current.Version != applicationDataVersion)
{
    // convert old data format to new one
    await ApplicationData.Current.SetVersionAsync(applicationDataVersion, 
        ConvertAppData)
}

The actual conversion will be implemented in the ConvertAppData delegate passed to the SetVersionAsync method call. Its structure will typically be as follows:

async void ConvertAppData(SetVersionRequest setVersionRequest)
{
    var deferral = setVersionRequest.GetDeferral();

    var settings = await LoadSettingsAsync(setVersionRequest.CurrentVersion);
    await SaveSettingsAsync(setVersionRequest.DesiredVersion, settings);

    deferral.Complete();
}

Since most data access is asynchronous, you will need to obtain the deferral to notify SetVersionAsync when you're done. Your delegate will also receive information about the current version of data that needs to be read and the target version of data required by the application, allowing your load and save methods to behave correctly.

Unfortunately, error handling in the conversion process leaves a lot to be desired. There's no transactional support: if the method doesn't successfully execute to the end, any partial changes to the data will be saved, making your data inconsistent. It's best to use new names for any files and settings that can't be read by the old code any more after the conversion. This way, if the conversion process restarts, all the old data will still be preserved.

There's also no way to notify SetVersionAsync that the conversion process went awry. Any uncaught exceptions thrown in the delegate will crash the application. Not calling Complete on the deferral will cause any future calls to SetVersionAsync to timeout until the application is restarted. The application data version won't be changed in this case, though.

Taking all this into account, your data conversion delegate should always succeed and call Complete on the deferral at the end. If for some reason the conversion fails, you should catch any exceptions and reset the data in such a way that the application will still function correctly afterwards. The only remaining case when the conversion might not execute completely is when the process is terminated unexpectedly, e.g. because the user terminated it or the device was shut down. In this edge case the conversion will be performed again from the beginning when the application is restarted, because the application data version is only chnaged at the very end of the process.

Since you don't know how old your application data might be, the latest version of the application should be able to read and convert all older versions of data. Never versions of the application should never decrease the expected application data version number, for the process to work. As long as applications are only installed from the store and not sideloaded, this should prevent encountering a newer version of the application data than expected, unless data is roamed between devices.

According to the documentation, even in roaming scenarios when not all devices are updated to the latest version of the application, they shouldn't get access to newer version of application data:

App data only roams between installed apps with the same version number. For example, devices on version 2 will transition data between each other and devices on version 3 will do the same, but no roaming will occur between a device running version 2 and a device running version 3. If you install a new app that utilized various version numbers on other devices, the newly installed app will sync the app data associated with the highest version number.

However, I would still recommend, you handle this possibility, just to be sure. Resetting the application data if its version is not recognized is a reasonable strategy for most cases. The user shouldn't encounter it ever. If it does happen nevertheless, its still better than the application crashing or behaving unexpectedly.

Copyright
Creative Commons License