Application Configuration in C#

5/12/2010

I can't stand configuration files for the most part. I mean I have a number of options, but they all have some flaw. For example INI files and the registry both require a decent amount of work. With an app.config or web.config file, you may not have access (yay security constraints) and it's a pain if you're dealing with anything except strings as you have to parse it out. The last real option is XML, which is actually not that bad. My biggest gripe with it though is the fact that it's a pain to write by hand (which I avoid at all costs).For me, I would much rather have a system that allows most if not any type, is mostly code based, but also will allow me to modify an XML representation if need be (basically after it's been deployed), and lastly I want to be able to keep my configuration files/classes modular. Basically one file/class per system, but having a central place to get them all.

So while I was bored this weekend I came up with my own system that works for my needs. The best part about it is it was actually incredibly simple:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Collections.Generic;
  25: using System.Reflection;
  26: using Gestalt.Attributes;
  27: using Gestalt.Interfaces;
  28: using Utilities.DataTypes.Patterns.BaseClasses;
  29: using System.Text;
  30: #endregion
  31:  
  32: namespace Gestalt
  33: {
  34:     /// <summary>
  35:     /// Config manager
  36:     /// </summary>
  37:     public class Manager : Singleton<Manager>
  38:     {
  39:         #region Constructor
  40:  
  41:         /// <summary>
  42:         /// Constructor
  43:         /// </summary>
  44:         protected Manager()
  45:             : base()
  46:         {
  47:             ConfigFiles = new Dictionary<string, IConfig>();
  48:         }
  49:  
  50:         #endregion
  51:  
  52:         #region Public Functions
  53:  
  54:         /// <summary>
  55:         /// Registers a config file
  56:         /// </summary>
  57:         /// <param name="Name">Name to reference by</param>
  58:         /// <param name="ConfigObject">Config object to register</param>
  59:         public void RegisterConfigFile(string Name, IConfig ConfigObject)
  60:         {
  61:             ConfigObject.Load();
  62:             ConfigFiles.Add(Name, ConfigObject);
  63:         }
  64:  
  65:         /// <summary>
  66:         /// Registers all config files in an assembly
  67:         /// </summary>
  68:         /// <param name="AssemblyContainingConfig">Assembly to search</param>
  69:         public void RegisterConfigFile(Assembly AssemblyContainingConfig)
  70:         {
  71:             List<Type> Types = Utilities.Reflection.Reflection.GetTypes(AssemblyContainingConfig, "IConfig");
  72:             foreach (Type Temp in Types)
  73:             {
  74:                 if (!Temp.ContainsGenericParameters)
  75:                 {
  76:                     string Name = "";
  77:                     object[] Attributes = Temp.GetCustomAttributes(typeof(ConfigAttribute), true);
  78:                     if (Attributes.Length > 0)
  79:                     {
  80:                         Name = ((ConfigAttribute)Attributes[0]).Name;
  81:                     }
  82:                     IConfig TempConfig = (IConfig)Temp.Assembly.CreateInstance(Temp.FullName);
  83:                     RegisterConfigFile(Name, TempConfig);
  84:                 }
  85:             }
  86:         }
  87:  
  88:         /// <summary>
  89:         /// Registers all config files in an assembly that is not currently loaded
  90:         /// </summary>
  91:         /// <param name="AssemblyLocation">Location of the assembly</param>
  92:         public void RegisterConfigFile(string AssemblyLocation)
  93:         {
  94:             RegisterConfigFile(Assembly.LoadFile(AssemblyLocation));
  95:         }
  96:  
  97:         /// <summary>
  98:         /// Gets a specified config file
  99:         /// </summary>
 100:         /// <typeparam name="T">Type of the config object</typeparam>
 101:         /// <param name="Name">Name of the config object</param>
 102:         /// <returns>The config object specified</returns>
 103:         public T GetConfigFile<T>(string Name)
 104:         {
 105:             if (!ConfigFiles.ContainsKey(Name))
 106:                 throw new Exception("The config object " + Name + " was not found.");
 107:             if (!(ConfigFiles[Name] is T))
 108:                 throw new Exception("The config object " + Name + " is not the specified type.");
 109:             return (T)ConfigFiles[Name];
 110:         }
 111:  
 112:         public override string ToString()
 113:         {
 114:             StringBuilder Builder = new StringBuilder();
 115:             Builder.Append("<ul>").Append("<li>").Append(ConfigFiles.Count).Append("</li>");
 116:             foreach (string Name in ConfigFiles.Keys)
 117:             {
 118:                 Builder.Append("<li>").Append(Name).Append(":").Append(ConfigFiles[Name].GetType().FullName).Append("</li>");
 119:             }
 120:             Builder.Append("</ul>");
 121:             return Builder.ToString();
 122:         }
 123:  
 124:         #endregion
 125:  
 126:         #region Private properties
 127:  
 128:         private static Dictionary<string, IConfig> ConfigFiles { get; set; }
 129:  
 130:         #endregion
 131:     }
 132: }

The above is the manager class. Basically what I wanted was a central location where I could register the configuration classes and pull them out again. And since I needed to be able to access it anywhere I chose to go the singleton route using a class that I created and added to my utility library here. Anyway, it only has two functions. The first is to load/register config files and the second is to get it back out of the system. In the load functions I can either load all configs in an assembly or individual config objects. In the case of the mass loading, it uses a reflection function from my utility library which can be found here. There are two things of note. The first is the ConfigAttribute:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24:  
  25: #endregion
  26:  
  27: namespace Gestalt.Attributes
  28: {
  29:     /// <summary>
  30:     /// Attribute for naming a config file
  31:     /// </summary>
  32:     [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
  33:     public class ConfigAttribute:Attribute
  34:     {
  35:         #region Properties
  36:         
  37:         /// <summary>
  38:         /// Config's name
  39:         /// </summary>
  40:         public string Name { get; set; }
  41:  
  42:         #endregion
  43:     }
  44: }

The only other thing is the IConfig interface:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23:  
  24: #endregion
  25:  
  26: namespace Gestalt.Interfaces
  27: {
  28:     /// <summary>
  29:     /// IConfig interface
  30:     /// </summary>
  31:     public interface IConfig
  32:     {
  33:         #region Functions
  34:  
  35:         /// <summary>
  36:         /// Loads the config file
  37:         /// </summary>
  38:         void Load();
  39:  
  40:         /// <summary>
  41:         /// Saves the config file
  42:         /// </summary>
  43:         void Save();
  44:  
  45:         #endregion
  46:     }
  47: }

The attribute allows me to name a config object in a simple fashion so I can pull it out of the system later. The interface really only sets up two functions, a load and save. But that's it. The manager calls Load when registering the class, but save is only called by the end user. Now whether or not they actually do anything is up to the implementation. In my case I also wanted a base class that handled a couple things for me, serialization to/from an XML file and encryption. So I wrote the following class:

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Reflection;
  25: using Gestalt.Interfaces;
  26: using Utilities.IO;
  27: #endregion
  28:  
  29: namespace Gestalt
  30: {
  31:     /// <summary>
  32:     /// Config object
  33:     /// </summary>
  34:     [Serializable()]
  35:     public class Config<T> : IConfig
  36:     {
  37:         #region Constructor
  38:  
  39:         public Config()
  40:         {
  41:         }
  42:  
  43:         #endregion
  44:  
  45:         #region Protected Virtual Properties
  46:  
  47:         /// <summary>
  48:         /// Location to save/load the config file from.
  49:         /// If blank, it does not save/load but uses any defaults specified.
  50:         /// </summary>
  51:         protected virtual string ConfigFileLocation { get { return ""; } }
  52:  
  53:         /// <summary>
  54:         /// Encryption password for fields. Used only if set.
  55:         /// </summary>
  56:         protected virtual string EncryptionPassword { get { return ""; } }
  57:  
  58:         #endregion
  59:  
  60:         #region IConfig Members
  61:  
  62:         public void Load()
  63:         {
  64:             if (string.IsNullOrEmpty(ConfigFileLocation))
  65:                 return;
  66:             T Temp = default(T);
  67:             try
  68:             {
  69:                 string FileContent = FileManager.GetFileContents(ConfigFileLocation);
  70:                 if (string.IsNullOrEmpty(FileContent))
  71:                 {
  72:                     Save();
  73:                     return;
  74:                 }
  75:                 Temp = (T)Utilities.IO.Serialization.XMLToObject(FileContent, this.GetType());
  76:             }
  77:             catch { throw; }
  78:             LoadProperties(Temp);
  79:             Decrypt();
  80:         }
  81:  
  82:         public void Save()
  83:         {
  84:             if (string.IsNullOrEmpty(ConfigFileLocation))
  85:                 return;
  86:             Encrypt();
  87:             Utilities.IO.Serialization.ObjectToXML(this, ConfigFileLocation);
  88:             Decrypt();
  89:         }
  90:  
  91:         #endregion
  92:  
  93:         #region Private Functions
  94:  
  95:         private void LoadProperties(T Temp)
  96:         {
  97:             if (Temp == null)
  98:                 return;
  99:             Type ObjectType = Temp.GetType();
 100:             PropertyInfo[] Properties = ObjectType.GetProperties();
 101:             foreach (PropertyInfo Property in Properties)
 102:             {
 103:                 if (Property.CanWrite && Property.CanRead)
 104:                 {
 105:                     Property.SetValue(this, Property.GetValue(Temp, null), null);
 106:                 }
 107:             }
 108:         }
 109:  
 110:         private void Encrypt()
 111:         {
 112:             if (string.IsNullOrEmpty(EncryptionPassword))
 113:                 return;
 114:             Type ObjectType = this.GetType();
 115:             PropertyInfo[] Properties = ObjectType.GetProperties();
 116:             foreach (PropertyInfo Property in Properties)
 117:             {
 118:                 if (Property.CanWrite && Property.CanRead && Property.PropertyType == typeof(string))
 119:                 {
 120:                     Property.SetValue(this,
 121:                         Utilities.Encryption.AESEncryption.Encrypt((string)Property.GetValue(this, null),
 122:                             EncryptionPassword),
 123:                         null);
 124:                 }
 125:             }
 126:         }
 127:  
 128:         private void Decrypt()
 129:         {
 130:             if (string.IsNullOrEmpty(EncryptionPassword))
 131:                 return;
 132:             Type ObjectType = this.GetType();
 133:             PropertyInfo[] Properties = ObjectType.GetProperties();
 134:             foreach (PropertyInfo Property in Properties)
 135:             {
 136:                 if (Property.CanWrite && Property.CanRead && Property.PropertyType == typeof(string))
 137:                 {
 138:                     string Value = (string)Property.GetValue(this, null);
 139:                     if (!string.IsNullOrEmpty(Value))
 140:                     {
 141:                         Property.SetValue(this,
 142:                             Utilities.Encryption.AESEncryption.Decrypt(Value,
 143:                                 EncryptionPassword),
 144:                             null);
 145:                     }
 146:                 }
 147:             }
 148:         }
 149:  
 150:         #endregion
 151:     }
 152: }

This, like most of my files in this, uses a couple of functions that I've built and added to my utility library. AESEncryption and Serialization classes are found here and here. I've since added default values (one of the things that I've really enjoyed in 4.0, even if some people complain about using them in public functions). Anyway, the class is set up in such a way that it's not designed to be a fully functional config object. It's meant to be inherited from, allowing for something like this:

   1: namespace MyConfig
   2: {
   3:     [ConfigAttribute(Name="LogFile")]
   4:     public class Config:Gestalt.Config<Config>
   5:     {
   6:         #region Properties
   7:  
   8:         protected override string ConfigFileLocation { get { return "FILELOCATION.config"; } }
   9:         public virtual string LogFileLocation { get; set; }
  10:  
  11:         #endregion
  12:     }
  13: }

Doing something this simple allows me to set up a config file, load it, save to it, and pull it anywhere in my code asking the manager for "LogFile". I can even inherit from this class. Doing that would allow me to have one system, like HaterAide, with a default config object and allow the application using it to change the options, store the config file in a specific place, etc. without having to do much other than override a couple properties and the manager and base class would handle everything else. So there you go. It's incredibly simple and gives me more options than anything else that I've used in the past. So give it a try, leave feedback, and happy coding.



Comments