开发者

How to Read a Configuration Section from XML in a Database?

I have a Config class like this:

public class MyConfig : ConfigurationSection
{
        [ConfigurationProperty("MyProperty", IsRequired = true)]
        public string MyProperty
        {
            get { return (string)this["MyProperty"]; }
            set { this["MyProperty"] = value; }
        }
}

And it is being instantiated by another class like this

(MyConfig)ConfigurationManager.GetSection("myConfig")

We are making some changes and are now storing the configuration file in the DB as an xml, exactly like it is currently in the config file.

I开发者_如何学Python would like to maintain the MyConfig as a ConfigurationSection for backwards compatibility but still be able to instantiate it by using the XML string retrieved from the DB.

Is it possible? If so, how? (Keep in mind it should still work as instantiated above)


Here is how I usually do it: just add these members to the MyConfig class:

    public class MyConfig : ConfigurationSection
    {
        private static MyConfig _current;
        public static MyConfig Current
        {
            get
            {
                if (_current == null)
                {
                    switch(ConfigurationStorageType) // where do you want read config from?
                    {
                        case ConfigFile: // from .config file
                            _current = ConfigurationManager.GetSection("MySectionName") as MyConfig;
                            break;

                        case ConfigDb: // from database
                        default:
                            using (Stream stream = GetMyStreamFromDb())
                            {
                                using (XmlTextReader reader = new XmlTextReader(stream))
                                {
                                    _current = Get(reader);
                                }
                            }
                            break;


                    }
                }
                return _current;
            }
        }

        public static MyConfig Get(XmlReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            MyConfig section = new MyConfig();
            section.DeserializeSection(reader);
            return section;
        }
    }

This way, you have nothing to change in the MyConfig class, but you still need to change the way your customers access it with this kind of code:

string myProp = MyConfig.Current.MyProperty;


If you need to get any System.Configuration.ConfigurationSection stored in database, you may consider writing generic section reader like this:

    
public class ConfigurationSectionReader where T : ConfigurationSection, new()
{
    public T GetSection( string sectionXml ) 
    {
        T section = new T();
        using ( StringReader stringReader = new StringReader( sectionXml ) )
        using ( XmlReader reader = XmlReader.Create( stringReader, new XmlReaderSettings() { CloseInput = true } ) )
        {
            reader.Read();
            section.GetType().GetMethod( "DeserializeElement", BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( section, new object[] { reader, true } );
        }
        return section;
    }
}
    

This will work for all classes that override DeserializeElement method. e.g.

    
protected override void DeserializeElement( XmlReader reader, bool serializeCollectionKey )
{
    XmlDocument document = new XmlDocument();
    document.LoadXml( reader.ReadOuterXml() );
    MyProperty = document.DocumentElement.HasAttribute( "MyProperty" )
        ? document.DocumentElement.Attributes[ "MyProperty" ].Value
        : string.Empty;
}
    

Than you could get a section like this:

    
var reader = new ConfigurationSectionReader();
var section = reader.GetSection( sectionXml ); // where sectionXml is the XML string retrieved from the DB
    


My suggestion would be to keep your current MyConfig class but load your XML from your database in the constructor, then in each property of your MyConfig, you can put in logic to determine where you get the value from (either database or .config file) if you need to pull config from either location, or have it fall back if the value is empty.

public class MyConfig : ConfigurationSection
{
    public MyConfig()
    {
        // throw some code in here to retrieve your XML from your database
        // deserialize your XML and store it 
        _myProperty = "<deserialized value from db>";
    }

    private string _myProperty = string.Empty;

    [ConfigurationProperty("MyProperty", IsRequired = true)]
    public string MyProperty
    {
        get
        {
            if (_myProperty != null && _myProperty.Length > 0)
                return _myProperty;
            else
                return (string)this["MyProperty"];
        }
        set { this["MyProperty"] = value; }
    }
}


This is a hard problem. You could have a config file with some deliberately incorrect XML, override OnDeserializeUnrecognizedElement in your ConfigurationSection and then effectively bypass the file to ConfigurationSection mapping (essentially set your properties manually) - some refactoring would be needed, but you can still expose the same properties etc. It is a bit WTF, but possibly workable.

I essentially describe how to do this with LINQ to XML in this blog post. In all of my code now I don't have classes that rely on ConfigurationSection, I use the technique described in my blog post to bypass that and return POCOs through an interface. This has made my code more unit testable, as I can easily use a stub for the interface.

I can also easily move my configuration into a DB should I wish to do so- I just create a new class that implements my configuration interface and switch it in my IoC configuration. Microsoft didn't design the configuration system to be flexible, so you have to take that into consideration when using it in your own code.

The only other way I can think of is to write the DB config out to a file and then read it in, but that is also weird!


Rather old question, but just playing around with a solution to this. It's similar to Simon Mourier's approach (which I like a better in some ways - less hacky) but does mean any code that calls System.Configuration.ConfigurationManager.GetSection() will continue to work without having to change them to use the static method, so might result in less code change overall.

The first basic caveat is that I have no idea if this works with nested sections, but I'm almost certain it won't. The nearly-main caveat is that it requires changes to the config section class, so you can only use it with custom sections that you have the source to (and are allowed to change!)

The second and MAIN CAVEAT is that I'm just playing around with this, I'm not using it in either development and definitely not production, and simply smearing my own code over the base functionality like this may well have knock-on effects that do not show up in my example. Use at your own risk.

(Having said that, I'm testing it in an Umbraco site so with loads of other config sections going on, and they still all work, so I think it has no immediately awful effects)

EDIT: This is .NET 4, not 3.5 as per the original question. No idea if that wil make a difference.

So, here's the code, pretty simple, just override DeserializeSection to use an XML reader that loads from a database.

public class TestSettings : ConfigurationSection
{
    protected override void DeserializeSection(System.Xml.XmlReader reader)
    {
        using (DbConnection conn = /* Get an open database connection from whatever provider you are using */)
        {
            DbCommand cmd = conn.CreateCommand();

            cmd.CommandText = "select ConfigFileContent from Configuration where ConfigFileName = @ConfigFileName";

            DbParameter p = cmd.CreateParameter();
            p.ParameterName = "@ConfigFileName";
            p.Value = "TestSettings.config";

            cmd.Parameters.Add(p);

            String xml = (String)cmd.ExecuteScalar();

            using(System.IO.StringReader sr = new System.IO.StringReader(xml))
            using (System.Xml.XmlReader xr = System.Xml.XmlReader.Create(sr))
            {
                base.DeserializeSection(xr);
            }                
        }            
    }

    // Below is all your normal existing section code

    [ConfigurationProperty("General")]
    public GeneralElement General { get { return (GeneralElement)base["General"]; } }

    [ConfigurationProperty("UI")]
    public UIElement UI { get { return (UIElement)base["UI"]; } }

    ...

    ...
}

I'm using ASP.Net, so to make it work, you do need a web.config, but then hey, I need somewhere for connection strings anyway or I'm not going to connect to a database at all.

Your custom section should be defined as normal in <configSections/>; the key to making this work is to then put an empty element in place of your normal settings; i.e. in place of <TestSettings configSource="..."/> or you inline settings, simply put <TestSettings/>

The configuration manager will then load all sections, see the existing <TestSettings/> element, and deserialize it, at which point it hits your override and loads the XML from the database instead.

NOTE: The deserialize expects a document fragment (it expects to be called when the reader is already located at a node), not a whole document, so if your sections are stored in separate files, you must remove the <?xml ?> declaration first, or you get Expected to find an element.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜