Open AArnott opened 2 years ago
This has been a longstanding bug because AppSettings (despite being entirely appropriate to store in roaming app profiles) gets put in LocalAppData. The current recommendation is to not use AppSettings 😅
Thanks for looking so quickly. Where is "local app settings"? Is that something that squirrel can copy from an old location to a new one?
Or heck... given Squirrel offers callbacks around upgrade time, maybe I could do something to preserve them. If I didn't use AppSettings, what would I use? Anything I store in the app dir itself I suppose would be wiped out in the upgrade as well, wouldn't it?
I typically store settings and other stuff one directory up from my EXE's directory, this doesn't get wiped across upgrades, but does get wiped on uninstall
Thanks for looking so quickly. Where is "local app settings"?
This was me getting confused 😓 I'm talking about %LocalAppData%
And somehow the app upgrade makes .NET read/write app settings in a different path under LocalAppData?
It took Process Monitor to figure it out since I couldn't find any documentation for it, but in my case the settings are saved in C:\Users\andarno\AppData\Local\Andrew_Arnott\MoneyMan.WPF_Url_zor3ou4qwd14tf0bxfj3xami0oyfzk5t\0.2.0.0\user.config
I wonder what goes into that zor... character sequence. Whatever it is, I guess it must change with each update. Maybe the path of the app itself.
Yup. When I rename the directory the app is in, the AppSettings writes to a new path, and all prior settings are unreachable.
As AppSettings has a Providers property, I may be able to replace the default provider with another that does as you say and writes the settings to the parent directory.
I think it's theoretically possible, but the .NET BCL doesn't expose the types required to make it remotely easy. I'm going to go with your original suggestion and serialize my settings another way.
It looks like the logic for generating this path is here https://referencesource.microsoft.com/#System.Configuration/System/Configuration/ClientConfigPaths.cs,343 - and it will use a SHA1 hash of the strong name if your assembly has one, and fall back to the exe path if not. If it's simple enough to sn sign your assembly, it may fix this for you.
Depending on your framework version, the following may also be useful as it forces a custom application path. If set before you use the configuration manager for the first time, it will be used instead of your app path.
AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path);
(from the dotnet runtime), or;
AppDomain.CurrentDomain.SetupInformation.ConfigurationFile
(from reference source)
@caesay: I wasn't able to set APP_CONFIG_FILE
and detect any difference in where the file was saved. But strong name signing did seem to give it a new and stable path for settings. Thank you.
This isn't the most elegant solution, but strong naming produced too many other build errors. I dug a bit on that Providers
property in Reference Source and settled on this workaround:
public Settings()
{
LocalFileSettingsProvider localProvider = Providers
.OfType<LocalFileSettingsProvider>()
.FirstOrDefault();
if (localProvider is not null)
{
Configuration userConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
/* User config is landing in: %LocalAppData%\Publisher\<exe-name><hash>\<version>\user.config
* This "hash" includes the file path of the *executable* meaning it changes with each Squirrel version.
* For example:
* |****** THIS PART CHANGES *******|
* %LocalAppData%\<company-name>\<exe-filename>_Url_fvqct42v5v155z0imoat5bi2twry5min\1.2.3.4\user.config
*
* The framework looks for other version folders (e.g. 1.2.2.0) in that same location and will upgrade from them.
* We can override that behavior by forcing its search one level higher into the <company-name> folder.
*/
DirectoryInfo? parent = Directory.GetParent(userConfig.FilePath) // e.g. 1.2.3.4
?.Parent // e.g. <exe-filename>_Url_fvqct42v5v155z0imoat5bi2twry5min
?.Parent; // e.g. <company-name>
FileInfo? previous = parent?.EnumerateFiles("*user.config", SearchOption.AllDirectories)
.Where(f => f.FullName != userConfig.FilePath)
.OrderByDescending(f =>
{
try
{
return new Version(f.Directory.Name);
}
catch
{
return new Version();
}
}, new VersionComparison())
.FirstOrDefault();
// .NET Framework resolves this field to previous folders, we can bypass that behavior by
// pre-calculating it ourselves before calling Upgrade()
FieldInfo fileNameField = typeof(LocalFileSettingsProvider).GetField("_prevLocalConfigFileName",
bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic);
fileNameField.SetValue(localProvider, previous?.FullName);
}
}
Thanks for sharing an alternative. Strong naming was definitely the simplest solution for me but I can see where some folks wouldn't have that option.
Squirrel version(s) 2.0.2-netcore.3 (a private build with minimal changes based on the official 2.0.1 version).
Description
Updating a squirrel .NET app causes it to lose all its AppSettings.
Steps to recreate
Expected behavior
The app setting's value should be what was set in the prior version.
Actual behavior
The app setting's value has been cleared.