Skip to content

Commit a4c7122

Browse files
Migrate settings storage from XML to JSON with automatic migration (#3282)
* Initial plan * Migrate SettingsManager to use JSON format with automatic XML migration Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add JSON exception handling in App.xaml.cs Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Address code review comments: refactor JSON options and add null check Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix remaining code review issues: extract common error handling, add null checks, use timestamped backups Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix backup file copy to allow overwrite for same-timestamp scenarios Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Feature: Migrate settings to json * Docs: #3282 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
1 parent d6b9086 commit a4c7122

File tree

4 files changed

+155
-24
lines changed

4 files changed

+155
-24
lines changed

Source/NETworkManager.Settings/SettingsInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
using System.Collections.Specialized;
1414
using System.ComponentModel;
1515
using System.Runtime.CompilerServices;
16-
using System.Xml.Serialization;
16+
using System.Text.Json.Serialization;
1717

1818
// ReSharper disable InconsistentNaming
1919

@@ -42,7 +42,7 @@ private void OnPropertyChanged([CallerMemberName] string propertyName = null)
4242

4343
#region Variables
4444

45-
[XmlIgnore] public bool SettingsChanged { get; set; }
45+
[JsonIgnore] public bool SettingsChanged { get; set; }
4646

4747
/// <summary>
4848
/// Private field for the <see cref="WelcomeDialog_Show" /> property.

Source/NETworkManager.Settings/SettingsManager.cs

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using log4net;
22
using NETworkManager.Models;
33
using NETworkManager.Models.Network;
4+
using NETworkManager.Utilities;
45
using System;
56
using System.IO;
67
using System.Linq;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
710
using System.Xml.Serialization;
811

912
namespace NETworkManager.Settings;
@@ -22,6 +25,11 @@ public static class SettingsManager
2225
/// </summary>
2326
private static string SettingsFolderName => "Settings";
2427

28+
/// <summary>
29+
/// Settings backups directory name.
30+
/// </summary>
31+
private static string BackupFolderName => "Backups";
32+
2533
/// <summary>
2634
/// Settings file name.
2735
/// </summary>
@@ -30,7 +38,13 @@ public static class SettingsManager
3038
/// <summary>
3139
/// Settings file extension.
3240
/// </summary>
33-
private static string SettingsFileExtension => ".xml";
41+
private static string SettingsFileExtension => ".json";
42+
43+
/// <summary>
44+
/// Legacy XML settings file extension.
45+
/// </summary>
46+
[Obsolete("Legacy XML settings are no longer used, but the extension is kept for migration purposes.")]
47+
private static string LegacySettingsFileExtension => ".xml";
3448

3549
/// <summary>
3650
/// Settings that are currently loaded.
@@ -42,6 +56,17 @@ public static class SettingsManager
4256
/// </summary>
4357
public static bool HotKeysChanged { get; set; }
4458

59+
/// <summary>
60+
/// JSON serializer options for consistent serialization/deserialization.
61+
/// </summary>
62+
private static readonly JsonSerializerOptions JsonOptions = new()
63+
{
64+
WriteIndented = true,
65+
PropertyNameCaseInsensitive = true,
66+
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
67+
Converters = { new JsonStringEnumConverter() }
68+
};
69+
4570
#endregion
4671

4772
#region Settings location, default paths and file names
@@ -58,6 +83,15 @@ public static string GetSettingsFolderLocation()
5883
AssemblyManager.Current.Name, SettingsFolderName);
5984
}
6085

86+
/// <summary>
87+
/// Method to get the path of the settings backup folder.
88+
/// </summary>
89+
/// <returns>Path to the settings backup folder.</returns>
90+
public static string GetSettingsBackupFolderLocation()
91+
{
92+
return Path.Combine(GetSettingsFolderLocation(), BackupFolderName);
93+
}
94+
6195
/// <summary>
6296
/// Method to get the settings file name.
6397
/// </summary>
@@ -67,6 +101,16 @@ public static string GetSettingsFileName()
67101
return $"{SettingsFileName}{SettingsFileExtension}";
68102
}
69103

104+
/// <summary>
105+
/// Method to get the legacy settings file name.
106+
/// </summary>
107+
/// <returns>Legacy settings file name.</returns>
108+
[Obsolete("Legacy XML settings are no longer used, but the method is kept for migration purposes.")]
109+
public static string GetLegacySettingsFileName()
110+
{
111+
return $"{SettingsFileName}{LegacySettingsFileExtension}";
112+
}
113+
70114
/// <summary>
71115
/// Method to get the settings file path.
72116
/// </summary>
@@ -76,6 +120,16 @@ public static string GetSettingsFilePath()
76120
return Path.Combine(GetSettingsFolderLocation(), GetSettingsFileName());
77121
}
78122

123+
/// <summary>
124+
/// Method to get the legacy XML settings file path.
125+
/// </summary>
126+
/// <returns>Legacy XML settings file path.</returns>
127+
[Obsolete("Legacy XML settings are no longer used, but the method is kept for migration purposes.")]
128+
private static string GetLegacySettingsFilePath()
129+
{
130+
return Path.Combine(GetSettingsFolderLocation(), GetLegacySettingsFileName());
131+
}
132+
79133
#endregion
80134

81135
#region Initialize, load and save
@@ -99,7 +153,9 @@ public static void Initialize()
99153
public static void Load()
100154
{
101155
var filePath = GetSettingsFilePath();
156+
var legacyFilePath = GetLegacySettingsFilePath();
102157

158+
// Check if JSON file exists
103159
if (File.Exists(filePath))
104160
{
105161
Current = DeserializeFromFile(filePath);
@@ -109,22 +165,66 @@ public static void Load()
109165
return;
110166
}
111167

168+
// Check if legacy XML file exists and migrate it
169+
if (File.Exists(legacyFilePath))
170+
{
171+
Log.Info("Legacy XML settings file found. Migrating to JSON format...");
172+
173+
Current = DeserializeFromXmlFile(legacyFilePath);
174+
175+
Current.SettingsChanged = false;
176+
177+
// Save in new JSON format
178+
Save();
179+
180+
// Create a backup of the legacy XML file and delete the original
181+
Directory.CreateDirectory(GetSettingsBackupFolderLocation());
182+
183+
var backupFilePath = Path.Combine(GetSettingsBackupFolderLocation(),
184+
$"{TimestampHelper.GetTimestamp()}_{GetLegacySettingsFileName()}");
185+
186+
File.Copy(legacyFilePath, backupFilePath, true);
187+
188+
File.Delete(legacyFilePath);
189+
190+
Log.Info($"Legacy XML settings file backed up to: {backupFilePath}");
191+
192+
Log.Info("Settings migration from XML to JSON completed successfully.");
193+
194+
return;
195+
}
196+
112197
// Initialize the default settings if there is no settings file.
113198
Initialize();
114199
}
115200

116201
/// <summary>
117-
/// Method to deserialize the settings from a file.
202+
/// Method to deserialize the settings from a JSON file.
118203
/// </summary>
119204
/// <param name="filePath">Path to the settings file.</param>
120205
/// <returns>Settings as <see cref="SettingsInfo" />.</returns>
121206
private static SettingsInfo DeserializeFromFile(string filePath)
207+
{
208+
var jsonString = File.ReadAllText(filePath);
209+
210+
var settingsInfo = JsonSerializer.Deserialize<SettingsInfo>(jsonString, JsonOptions);
211+
212+
return settingsInfo;
213+
}
214+
215+
/// <summary>
216+
/// Method to deserialize the settings from a legacy XML file.
217+
/// </summary>
218+
/// <param name="filePath">Path to the XML settings file.</param>
219+
/// <returns>Settings as <see cref="SettingsInfo" />.</returns>
220+
[Obsolete("Legacy XML settings are no longer used, but the method is kept for migration purposes.")]
221+
private static SettingsInfo DeserializeFromXmlFile(string filePath)
122222
{
123223
var xmlSerializer = new XmlSerializer(typeof(SettingsInfo));
124224

125225
using var fileStream = new FileStream(filePath, FileMode.Open);
126226

127-
var settingsInfo = (SettingsInfo)xmlSerializer.Deserialize(fileStream);
227+
var settingsInfo = xmlSerializer.Deserialize(fileStream) as SettingsInfo;
128228

129229
return settingsInfo;
130230
}
@@ -145,17 +245,28 @@ public static void Save()
145245
}
146246

147247
/// <summary>
148-
/// Method to serialize the settings to a file.
248+
/// Method to serialize the settings to a JSON file.
149249
/// </summary>
150250
/// <param name="filePath">Path to the settings file.</param>
151251
private static void SerializeToFile(string filePath)
152252
{
153-
var xmlSerializer = new XmlSerializer(typeof(SettingsInfo));
253+
var jsonString = JsonSerializer.Serialize(Current, JsonOptions);
254+
255+
File.WriteAllText(filePath, jsonString);
256+
}
154257

155-
using var fileStream = new FileStream(filePath, FileMode.Create);
258+
#endregion
259+
260+
#region Backup
261+
/*
262+
private static void Backup()
263+
{
264+
Log.Info("Creating settings backup...");
156265
157-
xmlSerializer.Serialize(fileStream, Current);
266+
// Create the backup directory if it does not exist
267+
Directory.CreateDirectory(GetSettingsBackupFolderLocation());
158268
}
269+
*/
159270

160271
#endregion
161272

Source/NETworkManager/App.xaml.cs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.IO;
44
using System.Linq;
5+
using System.Text.Json;
56
using System.Threading;
67
using System.Windows;
78
using System.Windows.Threading;
@@ -92,21 +93,15 @@ by BornToBeRoot
9293
}
9394
catch (InvalidOperationException ex)
9495
{
95-
Log.Error("Could not load application settings!");
96-
Log.Error(ex.Message + "-" + ex.StackTrace);
97-
98-
// Create backup of corrupted file
99-
var destinationFile =
100-
$"{TimestampHelper.GetTimestamp()}_corrupted_" + SettingsManager.GetSettingsFileName();
101-
File.Copy(SettingsManager.GetSettingsFilePath(),
102-
Path.Combine(SettingsManager.GetSettingsFolderLocation(), destinationFile));
103-
Log.Info($"A backup of the corrupted settings file has been saved under {destinationFile}");
104-
105-
// Initialize default application settings
106-
Log.Info("Initialize default application settings...");
96+
Log.Error("Could not load application settings!", ex);
97+
98+
HandleCorruptedSettingsFile();
99+
}
100+
catch (JsonException ex)
101+
{
102+
Log.Error("Could not load application settings! JSON file is corrupted or invalid.", ex);
107103

108-
SettingsManager.Initialize();
109-
ConfigurationManager.Current.ShowSettingsResetNoteOnStartup = true;
104+
HandleCorruptedSettingsFile();
110105
}
111106

112107
// Upgrade settings if necessary
@@ -220,6 +215,27 @@ by BornToBeRoot
220215
}
221216
}
222217

218+
/// <summary>
219+
/// Handles a corrupted settings file by creating a backup and initializing default settings.
220+
/// </summary>
221+
private void HandleCorruptedSettingsFile()
222+
{
223+
// Create backup of corrupted file
224+
var destinationFile =
225+
$"{TimestampHelper.GetTimestamp()}_corrupted_" + SettingsManager.GetSettingsFileName();
226+
227+
File.Copy(SettingsManager.GetSettingsFilePath(),
228+
Path.Combine(SettingsManager.GetSettingsFolderLocation(), destinationFile));
229+
230+
Log.Info($"A backup of the corrupted settings file has been saved under {destinationFile}");
231+
232+
// Initialize default application settings
233+
Log.Info("Initialize default application settings...");
234+
235+
SettingsManager.Initialize();
236+
ConfigurationManager.Current.ShowSettingsResetNoteOnStartup = true;
237+
}
238+
223239
private void DispatcherTimer_Tick(object sender, EventArgs e)
224240
{
225241
Log.Info("Run background job...");
@@ -267,4 +283,4 @@ private void Save()
267283
ProfileManager.Save();
268284
}
269285
}
270-
}
286+
}

Website/docs/changelog/next-release.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ Release date: **xx.xx.2025**
5151
- Profile file dialog migrated to a child window to improve usability. [#3227](https://github.com/BornToBeRoot/NETworkManager/pull/3227)
5252
- Credential dialogs migrated to child windows to improve usability. [#3231](https://github.com/BornToBeRoot/NETworkManager/pull/3231)
5353

54+
**Settings**
55+
56+
- Settings format migrated from `XML` to `JSON`. The settings file will be automatically converted on first start after the update. [#3282](https://github.com/BornToBeRoot/NETworkManager/pull/3282)
57+
5458
**DNS Lookup**
5559

5660
- Allow hostname as server address in addition to IP address in the add/edit server dialog. [#3261](https://github.com/BornToBeRoot/NETworkManager/pull/3261)

0 commit comments

Comments
 (0)