TechHui

Hawaiʻi's Technology Community

This post will touch on several topics about app.config files.  The primary point of the posting is to provide sample code for a winforms application that will run in multiple environments.  In the case of this example: dev, test, uat, and production.  The code provided has been useful for a specific class of problems however there is a very great amount of capability in the configuration system and it is important to at least be aware of it.  In the quest to do things right, knowing what tools that are available go a long way.

Writing configuration code

The configuration system that Microsoft has provided encourages programmers to create objects that map to the configuration file.  It then loads the assembly, create instances of the objects and populate the fields.  There is a lot of power in this approach, if it is needed.

http://msdn.microsoft.com/en-us/library/2a1tyt9s

http://msdn.microsoft.com/en-us/library/8eyb2ct1(v=vs.110).aspx

http://www.codeproject.com/Articles/16466/Unraveling-the-Mysteries-...

http://nf2p.com/dot-net/configuring-a-net-2-0-application-using-the...

 

Transforming configuration files at build time

Visual Studio 2010 projects have the ability to create transforms for the web.config file.  This allows programmers to have custom settings for each build configuration.  Unfortunately, this functionality is not available by default to desktop application programmers.  The good news is that there is a plugin written for it.

 http://visualstudiogallery.msdn.microsoft.com/69023d00-a4f9-4a34-a6...

 

A solution for a problem

On a recent project, a set of WCF webservices were created and deployed to multiple environments.  The environments were: development (the local instance of the service), test, uat, and production.  The desktop application needed to be able to talk to any of these environments based on the needs of the time.  For example, it was useful to know that the changes that were currently being implemented in the desktop application worked with the services currently in the production environment.  Conversely it was useful to know that the web services that were recently deployed to the test environment did not break any existing desktop applications.

 

This approach became extremely valuable when debugging customer issues.  With the desktop application running in the debugger, the application used the production configuration settings.  This allowed programmers to see exactly what was happening in the specific environment that the customer was using.

 

It is also worth noting that this same approach to the app.config file was used when running automated tests.  This allowed for the same set of tests to be run against any environment, and provided a high degree of confidence when validating the services were deployed correctly.

 

Here is an example of the app.config file that was used for one of the desktop applications

  <MyApplication>

    <Machines>

      <Machine name="2Q4BKQ1" setting="uat" />

      <Machine name="6W7NFK1" setting="test" />

      <Machine name="5NHVGM1" setting="test" />

      <Machine name="8LW1MS1" setting="dev" />

      <Machine name="6hqyjs1" setting="dev" />

    </Machines>

 

    <Settings default="production">

      <Setting name="production"

               uitheme="DevExpress Style"

               logger="Null"

               logginglevel="Fatal"

               serviceurl="http://production:4568/webservices.svc"/>

 

      <Setting name="uat"

               uitheme="DevExpress Style"

               logger="Null"

               logginglevel="Fatal | Info"

               serviceurl ="http://uat:4568/webservices.svc"/>

 

      <Setting name="test"

               uitheme="DevExpress Dark Style"

               logger="Null"

               logginglevel="Fatal | Info | Trace"

               serviceurl ="http://test:4568/webservices.svc"/>

 

      <Setting name="dev"

               uitheme="VS2010"

               logger="debugger"

               logginglevel="All"

               serviceurl="http://localhost:8732/Design_Time_Addresses/server/services"/>

    </Settings>

  </MyApplication>

 

In this specific case, there was not the need to have all the power of Microsoft’s configuration system.  The desire to create all the classes was also lacking.  What was needed was a single class that would turn the settings into properties that the application could use.

 

The properties are straightforward enough:

 /// <summary>

        /// Gets or sets the location to the Services

        /// </summary>

        public string Service

        {

            get;

            protected set;

        }

 

        /// <summary>

        /// Gets or sets the logger to use

        /// </summary>

        public ILogger Logger

        {

            get;

            protected set;

        }

 

        /// <summary>

        /// Gets or sets the name of the setting being used

        /// </summary>

        public string Name

        {

            get;

            protected set;

        }

 

        /// <summary>

        /// Gets or sets the UI theme to use

        /// </summary>

        public string Theme

        {

            get;

            protected set;

        }

 

The code to load the settings is worth talking about a little bit.  There are a couple of approaches that could be used here.  In this specific case, the properties are in the same class as the Load method.  It is reasonable to suggest that a better design would be to declare the load method as static and have it return an object with the settings parsed out.  Other design considerations guided the implementation in a different direction.

        /// <summary>

        /// Gets the configuration and parses it.  This needs to be called before using any of the settings.

        /// </summary>

        public void Load()

        {

            XDocument config = this.ReadSettings();

            this.ParseSettings(config);

        }

 

Notice that the methods being called by the Load method are protected virtual.  This was done for testing purposes.  A test class can override the ReadSettings method and return a variety of app.config configurations to ensure that the parsers handle them correctly.  The ParseSettings method is also virtual for similar reasons.

 /// <summary>

        /// Get the settings as an XDocument

        /// </summary>

        /// <returns>The configuration settings for the application</returns>

        protected virtual XDocument ReadSettings()

        {

            return XDocument.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);

        }

 

 /// <summary>

        /// Parses the XDocument of configuration settings to the properties for the xml

        /// </summary>

        /// <param name="config">An XDocument representation of the application settings</param>

        protected virtual void ParseSettings(XDocument config)

        {

            XElement settings = this.GetSettingsSection(config);

            XElement machines = this.GetMachinesSection(config);

            string machineName = Environment.MachineName.ToLower();

            string defaultSetting = settings.Attribute("default").Value;

            string settingName = this.DetermineSettingName(machines, machineName, defaultSetting);

            XElement machineSettings = this.SelectMachineSettings(settings, settingName);

            this.ParseSettings(machineSettings);

        }

 

 

This method pulls the settings section shown above.  Remember that the configuration shown is a snippet of a larger app.config file, with the first element being “configuration”.  There is an attribute “default” on the root element of this section.  This will tell the parser which environment to use if there is not an entry in the <Machines> section.

        /// <summary>

        /// Gets the settings section from the app.config file

        /// </summary>

        /// <param name="config">The app.config file</param>

        /// <returns>The settings section</returns>

        protected virtual XElement GetSettingsSection(XDocument config)

        {

            XElement result = config.Element("configuration").Element("MyApplication").Element("Settings");

            return result;

        }

 

 

The machines section allows the user to indicate which environment to use for a given machine.  The name attribute is the name of the computer.

        /// <summary>

        /// Load the machines section

        /// </summary>

        /// <param name="config">The full app.config file</param>

        /// <returns>The Machines section of the app.config file</returns>

        protected virtual XElement GetMachinesSection(XDocument config)

        {

            XElement result = config.Element("configuration").Element("MyApplication").Element("Machines");

            return result;

        }

 

 

This method will determine the name of the setting to use.  This is also a protected method so that in testing test cases can force back specific setting names to be used and recreate very specific scenarios.

        /// <summary>

        /// Figure out which configuration we want to load

        /// </summary>

        /// <param name="machines">The list of machines listed in the app.cofig file</param>

        /// <param name="machineName">The current machine name</param>

        /// <param name="defaultSetting">The name of the config to use if the machinename is not found in machines</param>

        /// <returns>The name of the config to use</returns>

        protected virtual string DetermineSettingName(XElement machines, string machineName, string defaultSetting)

        {

            XElement machine = (from m in machines.Descendants("Machine")

                                where m.Attribute("name").Value.ToLower().Equals(machineName)

                                select m).FirstOrDefault();

            if (null == machine)

            {

                return defaultSetting;

            }

 

            return machine.Attribute("setting").Value;

        }

 

 

This is the code that will get the specific settings for the machine based the result of calling the method above.

/// <summary>

        /// From the collection of settings, find the settings for the provided name

        /// </summary>

        /// <param name="settings">The collection of settings</param>

        /// <param name="settingName">The name of the settings to find</param>

        /// <returns>The specific settings</returns>

        protected virtual XElement SelectMachineSettings(XElement settings, string settingName)

        {

            XElement machineSettings = (from s in settings.Descendants("Setting")

                                        where s.Attribute("name").Value.ToLower().Equals(settingName)

                                        select s).FirstOrDefault();

            return machineSettings;

        }

 

The meat of the whole class, parsing the settings into property values.

/// <summary>

        /// From the provided machine settings, parse out the attributes and set this objects properties

        /// </summary>

        /// <param name="machineSettings">The machine settings to parse</param>

        protected virtual void ParseSettings(XElement machineSettings)

        {

            this.Service = machineSettings.Attribute("serviceurl").Value;

            string flagString = this.ReadAttribute(machineSettings, "logginglevel");

            this.Logger = LoggerFactory.CreateLogger(machineSettings.Attribute("logger").Value, this.ParseLogFlags(flagString));

            this.Name = machineSettings.Attribute("name").Value;

            this.Theme = machineSettings.Attribute("uitheme").Value;

        }

 

The LogFlag enum is from the previous post on logging, here is the method that parses out the flags.

        /// <summary>

        /// Convert the string of flag settings in the app.config file to LogFlag values

        /// </summary>

        /// <param name="flagString">The flag values as a pipe delimited string</param>

        /// <returns>The LogFlag value</returns>

        protected virtual LogFlag ParseLogFlags(string flagString)

        {

            LogFlag result = LogFlag.Fatal;

            LogFlag parsed = LogFlag.Fatal;

 

            string[] flags = flagString.Split('|');

            foreach (string flag in flags)

            {

                if (Enum.TryParse<LogFlag>(flag.Trim(), out parsed))

                {

                    result |= parsed;

                    parsed = LogFlag.Fatal;

                }

            }

 

            return result;

        }

 

This code has worked very well over several projects, but it did have one very big flaw.  If something was not right in the app.config file, the error sent back to the user was very cryptic and made debugging very tiresome.

 

        /// <summary>

        /// Attempts to get the attribute value, throws a helpful exception if the attribute cannot be found

        /// </summary>

        /// <param name="element">The machine element to pull from</param>

        /// <param name="attribute">The attribute to get</param>

        /// <returns>The value of the attribute</returns>

        private string ReadAttribute(XElement element, string attribute)

        {

            XAttribute attrib = element.Attribute(attribute);

            if (null == attrib)

            {

                throw new MyApplicationException(string.Format("Could not find the setting attribute '{0}' - If it exists, check the case", attribute));

            }

 

            return attrib.Value;

        }

 

 

The last bit of magic

The configuration system will complain loudly if a configuration section entry is not provided for the section that was added.  It does not need to map to anything, but the entry needs to be there.

<configSections>

    <section name="MyApplication" type="MyCompany, MyApplication" />

  </configSections>

 

Visual studio 2010 will also generate several warnings if the app.config file is open when building the project because the schema does not validate.

To fix this, open the app.config file.  Then click on "XML" on the menu bar and select “Create Schema”

Save the schema with the project. 

With the app.config file the active file in the editor, go back to XML and select “Schemas…”. 

Add the schema file that was created and click “use”.

What is missing

The LogFlags enum is provided in the logging post I did previously, and the exception MyApplicationException will need to be written, otherwise this code can be put into a class file and will compile.

The conclusion

This solution provided a testable class that made it very easy to switch between multiple environments.  The decision was made to not use the ConfigurationManager classes because it was a lot more work to provide functionality that was not needed.  The decision was made against transforms because there was a desire to have settings for all environments available.  For problem customers, it is helpful to create a custom configuration with advanced logging to make it easier to determine how the customer is using the system.

Views: 304

Comment

You need to be a member of TechHui to add comments!

Join TechHui

Sponsors

web design, web development, localization

© 2018   Created by Daniel Leuck.   Powered by

Badges  |  Report an Issue  |  Terms of Service