I currently do this with a combination of custom action and XmlConfig.
The custom action is run after CostFinalize and reads current values from the config file(s) and saves them in public properties.
string configFile = Path.Combine(session["INSTALLLOCATION"], "app.exe.config");
ExeConfigurationFileMap map = new ExeConfigurationFileMap();
map.ExeConfigFilename = configFile;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
session["OLD_PRESERVEDVALUE"] = config.AppSettings.Settings["PreservedValue"].Value;
then I have an XmlConfig entry like below which sets the preserved values from the public properties:
<Component Id="RestoreOldPreservedValue" Guid="<GUID>" >
<Condition>OLD_PRESERVEDVALUE</Condition>
<CreateFolder/>
<util:XmlConfig
Id='RestoreOldPreservedValue'
Action='create'
On='install'
Node='value'
ElementPath='/configuration/applicationSettings/app.Properties.Settings/setting[\[]@name="PreservedValue"[\]]/value'
File='[#app.exe.config]'
Value='[OLD_PRESERVEDVALUE]'>
</util:XmlConfig>
</Component>
My next iteration will be to have the custom action create entries in the XmlConfig table directly.
The ultimate solution would be a WiX extension that populates a custom table and schedules a custom action which saves the values to be preserved after CostFinalize, and then another custom action that restores the values after the new config file(s) have been copied by the installer.