Skip to content

Instantly share code, notes, and snippets.

@DamianSuess
Last active October 23, 2025 18:25
Show Gist options
  • Select an option

  • Save DamianSuess/98911c062b31f78a2ab7adff18160327 to your computer and use it in GitHub Desktop.

Select an option

Save DamianSuess/98911c062b31f78a2ab7adff18160327 to your computer and use it in GitHub Desktop.
Cross-Platform INI File

Cross-Platform INI File for Preferences Service

This is helpful when you need a bare-bones configuration file in an INI file format. The PreferenceService can be used in Dependency Injection (DI) or a regular class instance. The current setup always saves after setting a parameter

This has been tested on Windows, Linux, and Mac.

Usecases include AOT complation and minimilistic configuration system.

ViewModel Sample

The follow example makes use of Prism Library's SetProperty(...) to automatically save to the settings file using the property's name as the key. This is a generic example and should be taylored for your own use cases.

public class SettingsViewModel
{
  private string _networkHost1 = string.Empty;
  private ZoneType _zoneTypeSelected;

  public SettingsViewModel(IPreferenceService prefs)
  {
    // NOTE! The 'key' value MUST be the same as the property name
    // so that [CallerMemberName] can work when saving.
    _networkHost1 = _prefs.NetworkHost1;

    // Loading an enum sample
    _zoneTypeSelected = (ZoneType)_prefs.GetValue(Section.Settings, "ZoneTypeSelected", (int)ZoneType.Unknown);
  }

  public string SlotType { get => _slotType; set => SaveProperty(ref _slotType, value); }

  // NOTE: When saving, the INI's Key is pulled from the Property's name
  //  And we must save the value as INT
  public ZoneType ZoneTypeSelected { get => _zoneTypeSelected; set => SavePropertyInt(ref _zoneTypeSelected, value); }

  public IEnumerable<ZoneType> ZoneTypes => Enum.GetValues<ZoneType>().Cast<ZoneType>();
 
  private void SaveProperty<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
  {
    // WARNING:
    //  The property names above are STRONGLY typed to the PreferenceService's property names
    //  We're cheating below assuming "propertyName" == the service's property name.
    //  SO, be sure to name them the exact same so .NET's reflection can do it's magic.
    SetProperty(ref storage, value);

    // The '!' denotes that WE KNOW it's not NULL
    Debug.WriteLine($"Saving: [{propertyName}] '{storage}' = '{value}'");
    _prefs.SetValue(Section.Settings, propertyName!, value?.ToString()!);
  }

  private void SavePropertyInt<T>(ref T storage, T value, [CallerMemberName] string? propertyName = null)
  {
    // WARNING:
    //  The property names above are STRONGLY typed to the PreferenceService's property names
    //  We're cheating below assuming "propertyName" == the service's property name.
    //  SO, be sure to name them the exact same so .NET's reflection can do it's magic.
    SetProperty(ref storage, value);

    // The '!' denotes that WE KNOW it's not NULL
    Debug.WriteLine($"Saving: [{propertyName}] '{storage}' = '{value}'");
    int valueInt = Convert.ToInt32(value);
    _prefs.SetValue(Section.Settings, propertyName!, valueInt);
  }
}
namespace TestApp.Models.Settings;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "This is small utility.")]
public static class Key
{
public const string AutoStartApp = "AutoStartApp";
public const string Ping1Enabled = "ping1-enabled";
public const string Ping1Host = "ping1-host";
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is small utility.")]
public static class Section
{
public const string Settings = "Settings";
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is small utility.")]
public static class Value
{
public const string False = "0";
public const string True = "1";
}
using System;
using System.Collections.Generic;
using System.IO;
using Aws.Diagnostics.Models.Settings;
namespace TestApp.Services;
public class PreferenceService : IPreferenceService
{
public const string SettingsFile = "settings.ini";
private const char BracketClose = ']';
private const char BracketOpen = '[';
private const char Equal = '=';
private const char Hash = '#';
private const char Semi = ';';
private readonly Dictionary<string, Dictionary<string, string>> _iniData =
new(StringComparer.OrdinalIgnoreCase);
private string _filePath = string.Empty;
public PreferenceService()
{
_filePath = SettingsFile;
Load();
}
public bool AutoStartApp
{
get => GetBool(Section.Settings, Key.AutoStartApp, false);
set => SetBool(Section.Settings, Key.AutoStartApp, value);
}
public string FilePath { get => _filePath; set => _filePath = value; }
public string NetworkHost1
{
get => GetValue(Section.Settings, Key.Ping1Host, DefaultLoopbackHost) ?? DefaultLoopbackHost;
set => SetValue(Section.Settings, Key.Ping1Host, value);
}
public bool NetworkIsEnabled1
{
get => GetBool(Section.Settings, Key.Ping1Enabled, false);
set => SetBool(Section.Settings, Key.Ping1Enabled, value);
}
public bool GetBool(string section, string key, bool defaultValue)
{
var value = GetValue(section, key, defaultValue ? Value.True : Value.False);
return value == Value.True;
}
public string? GetValue(string section, string key, string? defaultValue = null)
{
if (_iniData.ContainsKey(section) && _iniData[section].ContainsKey(key))
return _iniData[section][key];
return defaultValue;
}
public int GetValue(string section, string key, int defaultValue = 0)
{
// if (_iniData.ContainsKey(section) && _iniData[section].ContainsKey(key))
if (_iniData.TryGetValue(section, out Dictionary<string, string>? value) && value.ContainsKey(key))
{
string? item = _iniData[section][key];
if (int.TryParse(item, out int result))
return result;
else
return defaultValue;
}
return defaultValue;
}
public bool Load()
{
_iniData.Clear();
if (!File.Exists(_filePath))
return false;
string currentSection = string.Empty;
foreach (string line in File.ReadAllLines(_filePath))
{
string trimmedLine = line.Trim();
// Skip empty lines and comments
if (string.IsNullOrWhiteSpace(trimmedLine) || trimmedLine.StartsWith(Semi) || trimmedLine.StartsWith(Hash))
continue;
if (trimmedLine.StartsWith(BracketOpen) && trimmedLine.EndsWith(BracketClose))
{
////currentSection = trimmedLine.Substring(1, trimmedLine.Length - 2);
currentSection = trimmedLine[1..^1];
if (!_iniData.ContainsKey(currentSection))
_iniData[currentSection] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
else if (trimmedLine.Contains(Equal))
{
int equalsIndex = trimmedLine.IndexOf(Equal);
////string key = trimmedLine.Substring(0, equalsIndex).Trim();
////string value = trimmedLine.Substring(equalsIndex + 1).Trim();
string key = trimmedLine[..equalsIndex].Trim();
string value = trimmedLine[(equalsIndex + 1)..].Trim();
if (!string.IsNullOrEmpty(currentSection) && _iniData.ContainsKey(currentSection))
_iniData[currentSection][key] = value;
}
}
return true;
}
public bool Save()
{
try
{
using StreamWriter writer = new(_filePath);
foreach (var sectionEntry in _iniData)
{
writer.WriteLine($"{BracketOpen}{sectionEntry.Key}{BracketClose}");
foreach (var keyValuePair in sectionEntry.Value)
writer.WriteLine($"{keyValuePair.Key}{Equal}{keyValuePair.Value}");
// Add a blank line between sections for readability
writer.WriteLine();
}
}
catch
{
return false;
}
return true;
}
public void SetBool(string section, string key, bool value) =>
SetValue(section, key, value ? Value.True : Value.False);
public void SetValue(string section, string key, string value)
{
if (!_iniData.ContainsKey(section))
_iniData[section] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_iniData[section][key] = value;
Save();
}
public void SetValue(string section, string key, int value) =>
SetValue(section, key, value.ToString());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment