app.config in IronPython without additional assemblies

29 October 2012 - Iron Python

Problem and Motivation

One of the most important features of IronPython is its interoperability with existing .net assemblies. They can be easily referenced, imported and used like in the following sample:

import clr
clr.AddReference("SomeLibrary")
from SomeLibrary import Helper

someValue = Helper.GetSomeValueUsingConfig()
print someValue

In this sample I add a reference to a CLR-library called SomeLibrary, import one of its classes called Helper, call a static member named GetSomeValueUsingConfig and print its result using python. The implementation of the Helper class is very simple and just creates some string values which use the ConfigurationManager to access the application configuration file and the contained AppSettings.

namespace SomeLibrary
{
    using System.Configuration;

    /// <summary>
    /// Example helper class.
    /// </summary>
    public static class Helper
    {
        /// <summary>
        /// Gets a string which contains some data from appSettings.
        /// </summary>
        /// <returns>Some string including config data.</returns>
        public static string GetSomeValueUsingConfig()
        {
            var someSetting = ConfigurationManager.AppSettings["SomeSetting"];
            return string.Format("The config contains: {0}", someSetting);
        }
    }
}

The App.config contains the following xml data:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="SomeSetting" value="this is config data" />
    </appSettings>
</configuration>

During building or deploying of a .net application the App.config file is typically copied to the location of the resulting library (e.g. SomeLibrary.dll) or executable (e.g. SomeApp.exe) and has to conform to a strict naming convention to be discovered by the runtime (e.g. SomeApp.exe.config). In case of a IronPython script the .net runtime is looking for a config-file called like the process executable (e.g. ipy64.exe) which would have to be called ipy64.exe.config and reside in the IronPython installation directory. This is not very useful and maintainable when trying to run different scripts. If the referenced DLL is developed in-house the configuration mechanism could be adapted to allow for alternative/external configuration mechanisms but in many cases third-party assemblies are as-is and can't be changed.

If the example python script is executed the Helper class won't be able to find the required app settings and the result looks like:

The config contains:

Previous Solutions

An answer on Stack Overflow and the related blog post (which is based on another blog post) pointed me in the right direction by explaining how to implement IInternalConfigSystem in order to redirect all configuration requests to an arbitrary file (which can reside at a more convenient location like next to our IronPython script).

These solutions require to reference an additional .net assembly containing a class implementing IInternalConfigurationSystem. In my situation I wanted to avoid having to add another assembly due to some deployment constraints. Therefore a pure IronPython solution was the best approach.

Implementing IInternalConfigurationSystem in IronPython

The following snippet shows how to implement a configuration proxy in IronPython (based on the previous solutions):

import clr
clr.AddReference("System.Configuration")
from System.Configuration.Internal import IInternalConfigSystem
class ConfigurationProxy(IInternalConfigSystem):
    def __init__(self, fileName):
        from System import String
        from System.Collections.Generic import Dictionary
        from System.Configuration import IConfigurationSectionHandler, ConfigurationErrorsException
        self.__customSections = Dictionary[String, IConfigurationSectionHandler]()
        loaded = self.Load(fileName)
        if not loaded:
            raise ConfigurationErrorsException(String.Format("File: {0} could not be found or was not a valid cofiguration file.", fileName))

    def Load(self, fileName):
        from System.Configuration import ExeConfigurationFileMap, ConfigurationManager, ConfigurationUserLevel
        exeMap = ExeConfigurationFileMap()
        exeMap.ExeConfigFilename = fileName
        self.__config = ConfigurationManager.OpenMappedExeConfiguration(exeMap, ConfigurationUserLevel.None);
        return self.__config.HasFile;
    
    def GetSection(self, configKey):
        if configKey == "appSettings":
            return self.__BuildAppSettings()
        return self.__config.GetSection(configKey);
    
    def __BuildAppSettings(self):
        from System.Collections.Specialized import NameValueCollection
        coll = NameValueCollection()
        for key in self.__config.AppSettings.Settings.AllKeys:
            coll.Add(key, self.__config.AppSettings.Settings[key].Value);
        return coll

    def RefreshConfig(self, sectionName):
        self.Load(self.__config.FilePath)
        
    def SupportsUserConfig(self):
        return False
    
    def InjectToConfigurationManager(self):
        from System.Reflection import BindingFlags
        from System.Configuration import ConfigurationManager
        configSystem = clr.GetClrType(ConfigurationManager).GetField("s_configSystem", BindingFlags.Static | BindingFlags.NonPublic)
        configSystem.SetValue(None, self);

When added to our initial IronPython script the ConfigurationProxy class can be used to redirect all configuration requests to a file called my.config located next to the IronPython script.

proxy = ConfigurationProxy("my.config")
proxy.InjectToConfigurationManager()

someValue = Helper.GetSomeValueUsingConfig()
print someValue

This will result in the correct output:

The config contains: this is config data

Full sample (VS2012 + pytools solution, tested with IronPython 2.7.3), possible updates can be found at our github repository.