using Common.Logging; using System.Xml.Serialization; namespace Common.AppSettings; public class XmlAppSettingsProvider : IAppSettingsProvider { private readonly string settingsFilePath; private readonly XmlSerializer serializer; private List settings; public event EventHandler? LogWorthyEvent; public XmlAppSettingsProvider(string settingsFilePath) { // check for file path validity if (String.IsNullOrEmpty(settingsFilePath)) { throw new ArgumentNullException(nameof(settingsFilePath)); } this.settingsFilePath = settingsFilePath; this.settings = new List(); this.serializer = new XmlSerializer(typeof(List)); // create file if it does not exist yet if (File.Exists(settingsFilePath) == false) { this.Save(); } } public void Load() { // read file by creating a stream. this will throw an exception when the file does not exist. using var fileStream = new FileStream(this.settingsFilePath, FileMode.Open); // deserialize and cast to List if not null var deserializedObj = this.serializer.Deserialize(fileStream); if(deserializedObj != null) { // create log entry if there were already some settings loaded if(this.settings.Count > 0) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(Load)}()' deserialization overwrote previously loaded settings.")); } this.settings = (List)deserializedObj; } else { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(Load)}()' deserialization resulted in null object. Did not change loaded settings.")); } } public void Save() { // if saving fails, just log the exception. user is happier if the app is useable but does not remember settings try { // create file and serialize data TextWriter writer = new StreamWriter(this.settingsFilePath); this.serializer.Serialize(writer, this.settings); writer.Close(); } catch (Exception ex) { this.LogWorthyEvent?.Invoke(this, new LogEntry(){ LogLevel = LogLevel.Error, Category = nameof(XmlAppSettingsProvider), Message = $"'{nameof(Save)}()' failed to create file or serialize data.", Exception = ex }); } } public bool WriteSetting(string key, string value) { // guard empty key or value if(String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); if (String.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); // get all settings with this key var foundSettings = this.settings.FindAll(x => x.Key == key); // check if the key already exists multiple times if(foundSettings.Count > 1) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(WriteSetting)}()' cancelled because {foundSettings.Count} settings with the key '{key}' were found. Expected none or exactly one.")); return false; // cancel } // if exactly one is found => remove existing app setting else if(foundSettings.Count == 1) { this.settings.Remove(this.settings.Find(x => x.Key == key)!); } // none existing yet => inform via event else if(foundSettings.Count == 0) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(WriteSetting)}()' initialized new setting with key '{key}' and value '{value}'.")); } // add new setting this.settings.Add(new AppSetting(key, value)); return true; } public bool TryReadSetting(string key, out string value) { // guard empty key if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); // get all settings with this key var foundSettings = this.settings.FindAll(x => x.Key == key); // too many values found if (foundSettings.Count > 1) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSetting)}()' did find {foundSettings.Count} settings with the key '{key}'. Expected only one.")); value = string.Empty; return false; } // no value found if (foundSettings.Count == 0) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSetting)}()' did not find a setting with the key '{key}'.")); value = string.Empty; return false; } // extract correct setting when found exactly one var setting = foundSettings[0]; if(setting.Value != null) { value = setting.Value; } // if value is null it was probably an empty string or similar => set it to empty string else { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSetting)}()' value of retrieved setting with key '{key}' was null. Returned empty string instead.")); value = string.Empty; } return true; } public bool TryReadSettingOrAddDefault(string key, out string value, string defaultValue) { // guard empty key or defaultValue if (String.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); if (String.IsNullOrEmpty(defaultValue)) throw new ArgumentNullException(nameof(defaultValue)); // get all settings with this key var foundSettings = this.settings.FindAll(x => x.Key == key); // too many values found if (foundSettings.Count > 1) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Error, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSettingOrAddDefault)}()' did find {foundSettings.Count} settings with the key '{key}'. Expected only one.")); value = string.Empty; return false; } // no value found => initialize with the default value if (foundSettings.Count == 0) { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Info, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSettingOrAddDefault)}()' did not find a setting with the key '{key}'. Adding setting with value '{defaultValue}'.")); value = defaultValue; return this.WriteSetting(key, defaultValue); } // extract correct setting when found exactly one var setting = foundSettings[0]; if (setting.Value != null) { value = setting.Value; } // if value is null it was probably an empty string or similar => set it to empty string else { this.LogWorthyEvent?.Invoke(this, new LogEntry(level: LogLevel.Warn, category: nameof(XmlAppSettingsProvider), message: $"'{nameof(TryReadSetting)}()' value of retrieved setting with key '{key}' was null. Returned empty string instead.")); value = string.Empty; } return true; } }