Skip to content

Instantly share code, notes, and snippets.

@DamianSuess
Last active October 17, 2025 14:27
Show Gist options
  • Save DamianSuess/98911c062b31f78a2ab7adff18160327 to your computer and use it in GitHub Desktop.
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.

  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);

    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);

    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