Using Ini Files In C#

An Initialization, or Ini file, is a common text based file format commonly used on the Windows platform. Today its mostly been succeeded in favor of XML files for application configuration and persisting user data. Never the less, these files still exist and are in use by many applications. In this post, we will explore how to work with Ini files via C#.

The structure of an Ini file is very simple and straight forward. You have three primary types of information: sections, parameters, and values. They look like this:

[SectionA]
Parameter1=value1
Parameter2=value2

[SectionB]
Parameter1=value1

Below we will create a class to read this data from a file. Keep in mind as you look at this C# Ini file implementation that this is just one way to do it. You could for example, create a much more concise solution using regular expressions, etc.

Parsing the Data

The first thing we should do if figure out how to get the data from the Ini file. We could read in all the text and manually parse the file; however there is an easier approach. The Win32 API contains a function called GetPrivateProfileString that we can use specifically for this task. We can use it to directly obtain the sections, parameters, and values. For a more detailed description of the GetPrivateProfileString function, check out this link on MSDN.

To use GetPrivateProfileString in C#, we need only reference it using the DLLImport attribute with the correct function signature.

        [DllImport("Kernel32.dll", EntryPoint = "GetPrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        public static extern int GetPrivateProfileString(string lpAppName,
                                                         string lpKeyName,
                                                         string lpDefault,
                                                         [In, Out] char[] lpReturnString,
                                                         int nSize,
                                                         string lpFilename);

Reading from an Ini File

First, to make our job a little easier we will wrap the call to GetPrivateProfileString with our own method so that we can handle all of the details. Here is the wrapper:

        private string[] GetPrivateProfileString(string appName, string keyName, string defaultName, string inifile)
        {
            // the file inifile should exist as it's validated in the class constructor, however GetPrivateProfileString does not lock the file hence it could be locked or deleted by
            // another process between calls to this method
 
            // used to tell if we successfully read all requested data from the file 
            // from MSDN:
            //  If neither lpAppName nor lpKeyName is NULL and the supplied destination buffer is too small to hold the requested string, the string is truncated and followed by a null character, and the return value is equal to nSize minus one.
            //  If either lpAppName or lpKeyName is NULL and the supplied destination buffer is too small to hold all the strings, the last string is truncated and followed by two null characters. In this case, the return value is equal to nSize minus two.
            int sizeReadOffset = (appName != null && keyName != null) ? 1 : 2;
 
            // try and read the entire amount of data from the ini file
            int sizeRead = Kernel32.GetPrivateProfileString(appName, keyName, defaultName, _readBuffer, _readBufferSize, inifile);
 
            // if unable to read all of the data because the buffer is to small, increase the size and try again, but only allow memory to increase up to _readBufferMaxSize
            while ((sizeRead >= _readBufferSize - sizeReadOffset) && (_readBufferSize < _readBufferMaxSize))
            {
                _readBufferSize += 512;
                _readBuffer = new char[_readBufferSize];
 
                sizeRead = Kernel32.GetPrivateProfileString(appName, keyName, defaultName, _readBuffer, _readBufferSize, inifile);
            }
 
            // if memory consumption exceeded _readBufferMaxSize then we have a problem
            if (_readBufferSize >= _readBufferMaxSize)
                throw new FileLoadException("ErrorBadIniLoad");
 
            // the buffer returned from GetPrivateProfileString will be null terminated C-strings followed by a double null at the end - so split the strings on the nulls
            char[] sep = new char[1];
            sep[0] = '\0';
 
            string s = new string(_readBuffer, 0, sizeRead);
 
            string[] result = s.Split(sep, StringSplitOptions.RemoveEmptyEntries);
 
            return result;
        }

Here, the MSDN terminology refers to what I call a section as an appName and what I refer to as a paramter as a keyName.

This method has a lot going on but is very easy to use. To get an array of strings containing all of the section names you simply say:

string[] raw = GetPrivateProfileString(null, null, null, filename);

And, to get an array of all parameter names for a particular section you would say:

string[] raw = GetPrivateProfileString(sectionName, null, null, inifileName);

And finally, to get a parameter value you could say:

string[] raw = GetPrivateProfileString(sectionName, parameterName, null, inifile);

And the value is stored in the first element of the returned array.

Putting it All Together

So now we can put together a simple class encapsulating the above functionality. You can use this class as follows:

Given an Ini file:

[SectionA]
Parameter1=value1
Parameter2=value2

[SectionB]
Parameter1=value1

You could,

            INIFile file =  new INIFile(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "MyInifile.ini");
 
            string a1 = file["SectionA","Parameter1"]; // returns "value1"
            string a2 = file["SectionA","Parameter2"]; // returns "value2"
 
            string b1 = file["SectionB", "Parameter1"]; // returns "value1"

Have fun playing with it. You could easily add the ability to write new sections and parameters as well. If you make some changes, find some bugs, or just plain find it useful let me know. I am always interested.

Here is the complete code listing for the class. You can download it here or just copy and paste it.

    /// <summary>
    /// Contains native Win32 API methods found in Kernel32. 
    /// </summary>
    public static class Kernel32
    {
        /// <summary>
        /// Retrieves a string from the specified section in an initialization file.
        /// </summary>
        /// <param name="lpAppName">The name of the section containing the key name. If this parameter is NULL, the GetPrivateProfileString function copies all section names in the file to the supplied buffer.</param>
        /// <param name="lpKeyName">The name of the key whose associated string is to be retrieved. If this parameter is NULL, all key names in the section specified by the lpAppName parameter are copied to the buffer specified by the lpReturnedString parameter.</param>
        /// <param name="lpDefault">A default string. If the lpKeyName key cannot be found in the initialization file, GetPrivateProfileString copies the default string to the lpReturnedString buffer. If this parameter is NULL, the default is an empty string, "".
        ///                         <para>Avoid specifying a default string with trailing blank characters. The function inserts a null character in the lpReturnedString buffer to strip any trailing blanks.</para></param>
        /// <param name="lpReturnString">A pointer to the buffer that receives the retrieved string.</param>
        /// <param name="nSize">The size of the buffer pointed to by the lpReturnedString parameter, in characters.</param>
        /// <param name="lpFilename">The name of the initialization file. If this parameter does not contain a full path to the file, the system searches for the file in the Windows directory.</param>
        /// <returns>The return value is the number of characters copied to the buffer, not including the terminating null character.</returns>
        [DllImport("Kernel32.dll", EntryPoint = "GetPrivateProfileStringW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
        public static extern int GetPrivateProfileString(string lpAppName,
                                                         string lpKeyName,
                                                         string lpDefault,
                                                         [In, Out] char[] lpReturnString,
                                                         int nSize,
                                                         string lpFilename);
 
    }
 
 
    /// <summary>
    /// Represents the <i>common</i> INI file format consisting of parameter-value pairs grouped by sections.
    /// </summary>
    public class INIFile
    {
        #region Nested Types
        public enum CaseSensitiviy
        {
            CaseInsensitive,
            CaseSensitive
        }
        #endregion
 
        #region Properties
        /// <summary>
        /// Provides the string value of parameter belonging to the specified section. The section and parameter must exist.
        /// </summary>
        /// <param name="sectionName">The name of the section the parameter belongs to.</param>
        /// <param name="parameterName">The name of the parameter containing the value.</param>
        /// <exception cref="System.ArgumentException">Thrown if either the section or parameter name do not exist.</exception>
        /// <returns>The value of the parameter.</returns>
        public string this[string sectionName, string parameterName]
        {
            get
            {
                if (_sections.ContainsKey(sectionName))
                {
                    if (_sections[sectionName].ContainsKey(parameterName))
                        return _sections[sectionName][parameterName];
                    else
                        throw new ArgumentException("ErrorBadIniFile");
                }
                else
                    throw new ArgumentException("ErrorBadIniFile");
            }
        }
 
        /// <summary>
        /// Specifies what case sensitivity mode is being used when looking up section and parameter names.
        /// </summary>
        public CaseSensitiviy CaseSensitivity
        {
            get { return _caseSensitivity; }
        }
        #endregion
 
        #region Events
        #endregion
 
        #region Constructors
        /// <summary>
        /// Loads the specified INI file. By default, all searches for section and parameter names will be case sensitive.
        /// </summary>
        /// <param name="filename">The file name and path to the INI file. If this parameter does not contain a full path to the file, the method will search the Windows directory.</param>
        /// <exception cref="System.IO.FileNotFoundException">Thrown if filename could not be found and doesn't exist in the Window directory.</exception>
        public INIFile(string filename) :
            this(filename, CaseSensitiviy.CaseSensitive)
        {
 
        }
 
        /// <summary>
        /// Loads the specified INI file.
        /// </summary>
        /// <param name="filename">The file name and path to the INI file. If this parameter does not contain a full path to the file, the method will search the Windows directory.</param>
        /// <param name="sensitivity">Specifies if letter case should be accounted for when looking up section and parameter names.</param>
        /// <exception cref="System.IO.FileNotFoundException">Thrown if filename could not be found and doesn't exist in the Window directory.</exception>
        public INIFile(string filename, CaseSensitiviy sensitivity)
        {
            // make sure the file exists, directly, or in the Windows directory
            if (!File.Exists(filename))
                if (!File.Exists(Path.Combine(Environment.ExpandEnvironmentVariables("%WinDir%"), filename)))
                    throw new FileNotFoundException();
 
            // save local state
            _caseSensitivity = sensitivity;
            _readBufferSize = _readBufferDefaultSize;
            _readBuffer = new char[_readBufferSize];
 
            // read all section names
            string[] raw = GetPrivateProfileString(null, null, null, filename);
 
            // the data is stored in a dictionary, we must tell it if case sensitivity is important
            if (_caseSensitivity == CaseSensitiviy.CaseInsensitive)
                _sections = new Dictionary<string, IDictionary<string, string>>(StringComparer.CurrentCultureIgnoreCase);
            else
                _sections = new Dictionary<string, IDictionary<string, string>>();
 
            // now, read in all parameters and values for each section name we located
            foreach (string s in raw)
                ReadSection(filename, s, _sections);
        }
        #endregion
 
        #region Public Methods
        /// <summary>
        /// Checks to see if a given section exists in the Ini file.
        /// </summary>
        /// <param name="sectionName">The section name to check for.</param>
        /// <returns>True if the section exists, false if not.</returns>
        public bool DoesSectionExist(string sectionName)
        {
            return _sections.ContainsKey(sectionName);
        }
 
        /// <summary>
        /// Checks to see if a given parameter exists for a section.
        /// </summary>
        /// <param name="sectionName">The name of the section containing the parameter.</param>
        /// <param name="parameterName">The name of the parameter to check for.</param>
        /// <returns>True if the parameter exists, false if not.</returns>
        public bool DoesParameterExist(string sectionName, string parameterName)
        {
            if (DoesSectionExist(sectionName))
                return _sections[sectionName].ContainsKey(parameterName);
            else
                return false;
        }
        #endregion
 
        #region Private Methods
        /// <summary>
        /// Reads all parameters and values for a given section within the Ini file.
        /// </summary>
        /// <param name="inifile">The ini file to read from.</param>
        /// <param name="sectionName">The name of the section to read.</param>
        /// <param name="storage">A dictionary containing an entry for the section that will be populated with the resulting parameters/values.</param>
        private void ReadSection(string inifile, string sectionName, IDictionary<string, IDictionary<string, string>> storage)
        {
            // create the parameter/value dictionary for the section entry
            // it is assumed that the callers logic created/validated storage[sectionName] -- we will not check again here
            if (_caseSensitivity == CaseSensitiviy.CaseInsensitive)
                storage[sectionName] = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
            else
                storage[sectionName] = new Dictionary<string, string>();
 
            string[] raw = GetPrivateProfileString(sectionName, null, null, inifile);
 
            // now read in the values for the parameters that we found
            foreach (string s in raw)
                ReadParameter(inifile, sectionName, s, storage[sectionName]);
        }
 
        /// <summary>
        /// Reads the value of a parameter in a given section within the Ini file.
        /// </summary>
        /// <param name="inifile">The ini file to read from.</param>
        /// <param name="sectionName">The name of the section containing the parameter.</param>
        /// <param name="parameterName">The name of the parameter to read.</param>
        /// <param name="storage">The dictionary that the parameter value will be written to.</param>
        /// <exception cref="System.ArgumentException">Thrown if more than 1 value is associated with a single parameter.</exception>
        private void ReadParameter(string inifile, string sectionName, string parameterName, IDictionary<string, string> storage)
        {
            // get the param value
            string[] raw = GetPrivateProfileString(sectionName, parameterName, null, inifile);
 
            // store the resulting value, if more than 1 value comes back for the parameter throw an exception, not sure how to parse that scenario
            // if no values come back just use an empty string by default
            if (raw.Length > 1)
                throw new ArgumentException("ErrorBadIniFile");
            else if (raw.Length == 0)
                storage[parameterName] = "";
            else
                storage[parameterName] = raw[0];
        }
 
        /// <summary>
        /// Wrapper around the Win32 function GetPrivateProfileString to make it nicer and more robust.
        /// </summary>
        /// <param name="appName">The name of the section containing the key name. If this parameter is null, all section names will be returned.</param>
        /// <param name="keyName">The name of the key whose associated string is to be retrieved. If this parameter is null, all key names in the section will be returned.</param>
        /// <param name="defaultName">A default string. If the keyName key cannot be found in the initialization file, GetPrivateProfileString copies the default string to the 
        ///                           returned value. If this parameter is null, the default is an empty string, "".</param>
        /// <param name="inifile">The path and file name of the INI file. If this parameter does not contain a full path to the file, the system searches for the file in the Windows directory.</param>
        /// <returns>An array of string containing either section names, parameter names, or values corresponding to appName and keyName.</returns>
        /// <exception cref="FileLoadException"></exception>
        private string[] GetPrivateProfileString(string appName, string keyName, string defaultName, string inifile)
        {
            // the file inifile should exist as it's validated in the class constructor, however GetPrivateProfileString does not lock the file hence it could be locked or deleted by
            // another process between calls to this method
 
            // used to tell if we successfully read all requested data from the file 
            // from MSDN:
            //  If neither lpAppName nor lpKeyName is NULL and the supplied destination buffer is too small to hold the requested string, the string is truncated and followed by a null character, and the return value is equal to nSize minus one.
            //  If either lpAppName or lpKeyName is NULL and the supplied destination buffer is too small to hold all the strings, the last string is truncated and followed by two null characters. In this case, the return value is equal to nSize minus two.
            int sizeReadOffset = (appName != null && keyName != null) ? 1 : 2;
 
            // try and read the entire amount of data from the ini file
            int sizeRead = Kernel32.GetPrivateProfileString(appName, keyName, defaultName, _readBuffer, _readBufferSize, inifile);
 
            // if unable to read all of the data because the buffer is to small, increase the size and try again, but only allow memory to increase up to _readBufferMaxSize
            while ((sizeRead >= _readBufferSize - sizeReadOffset) && (_readBufferSize < _readBufferMaxSize))
            {
                _readBufferSize += 512;
                _readBuffer = new char[_readBufferSize];
 
                sizeRead = Kernel32.GetPrivateProfileString(appName, keyName, defaultName, _readBuffer, _readBufferSize, inifile);
            }
 
            // if memory consumption exceeded _readBufferMaxSize then we have a problem
            if (_readBufferSize >= _readBufferMaxSize)
                throw new FileLoadException("ErrorBadIniLoad");
 
            // the buffer returned from GetPrivateProfileString will be null terminated C-strings followed by a double null at the end - so split the strings on the nulls
            char[] sep = new char[1];
            sep[0] = '\0';
 
            string s = new string(_readBuffer, 0, sizeRead);
 
            string[] result = s.Split(sep, StringSplitOptions.RemoveEmptyEntries);
 
            return result;
        }
        #endregion
 
        #region Fields
        /// <summary>
        /// Stores the sections from the Ini file. The section name is the key. The value is another dictionary containing the parameter values, keyed on the parameter name.
        /// </summary>
        private IDictionary<string, IDictionary<string, string>> _sections;
 
        /// <summary>
        /// Determines if case is ignored or not when referencing section and parameter names.
        /// </summary>
        private CaseSensitiviy _caseSensitivity;
 
        /// <summary>
        /// Holds the information from the last read of the ini file. The buffer is overwritten with sections, parameters, or values on each call to GetPrivateProfileString.
        /// </summary>
        private char[] _readBuffer;
 
        /// <summary>
        /// The size, in bytes, of _readBuffer.
        /// </summary>
        private int _readBufferSize;
 
        /// <summary>
        /// The starting size of _readBuffer, in bytes.
        /// </summary>
        private const int _readBufferDefaultSize = 1024;
 
        /// <summary>
        /// The maximum size _readBuffer is allowed to grow to in order to hold all information obtained from GetPrivateProfileString.
        /// </summary>
        private const int _readBufferMaxSize = 4096;
        #endregion
    }

July 16, 2009 В· crow В· 2 Comments
Posted in: C#

2 Responses

  1. Scott Knake - July 18, 2009

    Perfect, thanks. I ran across an instance where I needed to read an old Delphi application’s .ini file and I had no experience working with them in C#

  2. Jason S. - June 24, 2010

    Excellent, thank you so much.
    This will save me a lot of time.
    Nice work!
    BTW, there are a couple of places where “doesn’t exist in the Window directory” should be changed to “Windows” directory.

Leave a Reply

Connect with Facebook

*