Config Files with IronPython: Overriding the ConfigurationManager
Posted: February 2nd, 2011 | Filed under: .NET, IronPython, PythonAuthor: Tom
2 Comments →
Clarification: This is a need I had in the IronPython console. If you have an IronPython project and run it from a running .NET App I doubt you would need this as the ConfigurationManager knows what your correct AppDomain is.
If you’re like me and have worked in .NET extensively for years, you’re no stranger to config files. You may have even built you’re own set of section handlers to support extra configuration by environment or similar. Since I’ve existed in the neatly wrapped little Microsoft box about how .NET apps should be developed I hadn’t really had issue with the config files overall until today.
I’ve been meaning to dive into Python for a long time but I’ve never made it passed a small script here or there. Recently though I made a decision to invest a little of my time at work in discovering what powers I may unleash. Being a .NET shop it only made sense for me to bring in IronPython so I could continue to leverage the enormous libraries we already had. I quickly made sense of pulling in CLR assemblies and so I started bringing in my own. That was satisfying. It was simple and easy access to manipulating our business objects. Then I wanted to test connecting to the database and so I pulled over an App.config that would (I hoped) load up all the data I needed into the ConfigurationManager.
The design of the ConfigurationManager appears to be making some assumptions that the config file for an executable should be [executable].config (in this case: ipy.exe.config). But for the scope of Python that doesn’t really work. Since any scripts I write will have be running in the ipy context but I may have a number of differently configured scripts. I’ll spare you further details. The point is, since I didn’t want to use an ipy.exe.config file, it wouldn’t work. I tried a number of options for setting the configuration file but nothing worked as needed. Now, I could have written a python module that would have loaded up the needed connections and app settings, etc. But it really didn’t meet my needs as I have extensive custom section handlers and I’m not aware of a way to put that type of config into ConfigurationManager. After some fairly extensive searching, I found a couple of relatively simple solutions not great, but they were effective enough in a .NET app, but not in IronPython. Then I came across this suggestion on StackOverflow that had promise, it didn’t do everything I needed, but the idea was right. So I started exploring that solution and after a lot of hacking around with that I got to a solution that works, at least for my current needs.
The solution is relatively simple, the problem was finding it. I’m am hacking around the framework here and Microsoft doesn’t give up a lot of details about how all of this is working. And there are a lot of internal classes at work that I simply couldn’t get access to without even more hacking around the framework. It required a lot of trial and error and a whole lot of reflection on private variables. That work was really made possible by IronPython. It would have taken me dramatically longer to work through all of these details without the benefit of the interactive shell.
Essentially, I just wrote a new ConfigSystem which gets injected into ConfigurationManager where it is used for all requests from the config file. This isn’t a complete solution. For instance, it doesn’t support sectionGroups in the configSections but it does support the custom config handlers I’m currently using. I have a feeling as soon as I post this, I’m going to find or hear from someone of some really simple way to accomplish this, but oh well.
Here is the C# ConfigurationProxy class:
public sealed class ConfigurationProxy : IInternalConfigSystem
{
Configuration config;
Dictionary customSections;
// this is called filename but really it's the path as needed...
// it defaults to checking the directory you're running in.
public ConfigurationProxy(string fileName)
{
customSections = new Dictionary();
if (!Load(filename))
throw new ConfigurationErrorsException(string.Format(
"File: {0} could not be found or was not a valid cofiguration file.",
config.FilePath));
}
private bool Load()
{
var map = new ExeConfigurationFileMap { ExeConfigFilename = file };
config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);
var xml = new XmlDocument();
using(var stream = new FileStream(file, FileMode.Open, FileAccess.Read))
xml.Load(stream);
var cfgSections = xml.GetElementsByTagName("configSections");
if (cfgSections.Count > 0)
{
foreach (XmlNode node in cfgSections[0].ChildNodes)
{
var type = System.Activator.CreateInstance(
Type.GetType(node.Attributes["type"].Value))
as IConfigurationSectionHandler;
if (type == null) continue;
customSections.Add(node.Attributes["name"].Value, type);
}
}
return config.HasFile;
}
public Configuration Configuration
{
get { return config; }
}
#region IInternalConfigSystem Members
public object GetSection(string configKey)
{
if (configKey == "appSettings")
return BuildAppSettings();
object sect = config.GetSection(configKey);
if (customSections.ContainsKey(configKey) && sect != null)
{
var xml = new XmlDocument();
xml.LoadXml(((ConfigurationSection)sect).SectionInformation.GetRawXml());
// I have no idea what I should normally be passing through in the first
// two params, but I never use them in my confighandlers so I opted not to
// worry about it and just pass through something...
sect = customSections[configKey].Create(config,
config.EvaluationContext,
xml.FirstChild);
}
return sect;
}
public void RefreshConfig(string sectionName)
{
// I suppose this will work. Reload the whole file?
Load(config.FilePath);
}
public bool SupportsUserConfig
{
get { return false; }
}
#endregion
private NameValueCollection BuildAppSettings()
{
var coll = new NameValueCollection();
foreach (var key in config.AppSettings.Settings.AllKeys)
coll.Add(key, config.AppSettings.Settings[key].Value);
return coll;
}
public bool InjectToConfigurationManager()
{
// inject self into ConfigurationManager
var configSystem = typeof(ConfigurationManager).GetField("s_configSystem",
BindingFlags.Static | BindingFlags.NonPublic);
configSystem.SetValue(null, this);
// lame check, but it's something
if (ConfigurationManager.AppSettings.Count == config.AppSettings.Settings.Count)
return true;
return false;
}
}
I’m especially fond of the GetSection implementation. /sarcasm
I grab the section from the config object, get the raw xml, reload up that xml and then reconfigure it all, but… that was the simplest solution I could come up with at the time. Or maybe BuildAppSettings is the best. I tried setting it up to use the NameValueSectionHandler but it requires the use of an internal class I couldn’t get to without extensive reflection which would have meant writing my own version of the NameValueSectionHandler anyway so it seemed better to just make that one exception.
The python module that goes with this is very simple. This is my configproxy.py.
import clr
clr.AddReferenceToFile("ConfigurationProxy.dll")
import ConfigurationProxy
def override(filename):
proxy = ConfigurationProxy(filename)
return proxy.InjectToConfigurationManager()
There are a large collection of reasons that I ended up settling on this implementation, but the easy answer is that nothing else worked. I have a few ideas on other optional implementations for this that might work, however I’m not likely to chase them too far since I have a working solution now.
My real beef here is why didn’t the .NET team leave a way to tell the ConfigurationManager to use a different config file. I imagine it’s some sort of security concern, but if I was able to workout a functional though imperfect solution in a few hours then I can’t imagine this would stop anyone truly determined. Not to mention that a rogue program could probably just as easily drop a new config in place with the same name, then make a call to RefreshSection as needed which would accomplish the same thing. So really all they’ve done is make the lives of developers unnecessarily complicated.
I’m still a little green in terms of using Python to feel comfortable diving in with this right away, which is why I instead made this a .NET assembly. However I plan to make this a proper python module which would be cleaner and a little simpler to use.
I haven’t even attempted to build this in any versions of .NET 4. I don’t see anything that should cause problems, but …
Tags: .NET 4, c#, hack, IronPython, Python, Reflection
Could you elaborate as to how someone could use your proxy class within an IronPython with a custom file?
Well like I mentioned at the top this solution is for my use of the IronPython shell. if you’re writing an app you shouldn’t need it. But if you are using the shell then it will work like the example I have.
import clr
clr.AddReferenceToFile("ConfigurationProxy.dll")
clr.AddReference("System.Configuration")
from System.Configuration import ConfigurationManager
import ConfigurationProxy
p = ConfigurationProxy('myAlternateAppSettings.config')
p.InjectToConfigurationManager()
conn = ConfigurationManager.ConnectionStrings['myConnectionString']
You might be interested in my other post where I use this a little more.