如何从数据库中的XML中读取配置节?

28
我有一个像这样的Config类:
public class MyConfig : ConfigurationSection
{
        [ConfigurationProperty("MyProperty", IsRequired = true)]
        public string MyProperty
        {
            get { return (string)this["MyProperty"]; }
            set { this["MyProperty"] = value; }
        }
}

它是通过另一个类进行实例化的,就像这样

(MyConfig)ConfigurationManager.GetSection("myConfig")
我们进行了一些更改,现在将配置文件作为XML存储在数据库中,与配置文件中的格式完全相同。
我希望保持MyConfig作为ConfigurationSection以确保向后兼容性,但仍能够使用从数据库检索的XML字符串来实例化它。
这是否可能?如果可能,如何实现?(请记住,它应该仍然像上面实例化时那样工作)

5
我非常希望能够看到这个问题的解决方案——不幸的是,我已经进行了一段时间的研究,但并没有取得很大的成功。 - marc_s
1
很高兴看到我不是一个人。 - Jonas Stawski
我不知道这个问题的答案,因为似乎应用程序代码无法更改配置文件的位置。但是,在这种情况下,我不会过载配置文件,而是创建自己的配置读取器,从数据库中获取数据。 - akonsu
@akonsu,我认真考虑了你的建议。在朝那个方向前进之前,我想问过了。 - Jonas Stawski
@akonsu:主要问题在于我找不到一种方法来插入自己基于数据库的配置读取器。整个.NET配置系统很遗憾并不是基于提供程序的(就像.NET中的许多其他东西一样),而且许多相关类都是密封的,使用内部和私有方法 :-( - marc_s
5个回答

14

这是我通常的做法:只需将这些成员添加到MyConfig类中:

    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;
        }
    }

通过这种方式,您无需更改MyConfig类中的任何内容,但仍需要更改客户端访问它的方式,例如:

string myProp = MyConfig.Current.MyProperty;

6
如果您需要从数据库中获取任何 System.Configuration.ConfigurationSection,您可以考虑编写通用的部分读取器,如下所示:
    
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;
    }
}
    
这将适用于所有重写 DeserializeElement 方法的类。例如:
    
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;
}
    
然后,您可以像这样获取一个部分:
    
var reader = new ConfigurationSectionReader();
var section = reader.GetSection( sectionXml ); // 其中 sectionXml 是从数据库检索到的 XML 字符串
    

1
这是不好的,因为它依赖于私有成员,因此可能无法在Mono或.NET的下一个版本中工作,会被downvote。 - knocte
如果它被用作永久解决方案,这可能不是最理想的选择,但如果它被用作重构远离使用System.Configuration的手段,那么在我看来它完全足够了。我曾在一些非常恶劣的遗留代码上使用过类似的技术,以此来重构需要物理文件和环境转换的部分。当然,下一步将是进一步重构并消除对System.Configuration的依赖。鉴于这个条件,我给出+1的评价。 - killthrush
优秀的解决方案。虽然该解决方案依赖于私有成员,但它实现了整体目标,即能够将完全依赖于 app.config 的组件解耦并进行重构。在我的情况下,这是 Unity 容器。现在我能够仅使用 XML 实例化一个完整的类层次结构。在 Azure Function 中表现良好。谢谢。 - Sau001

3
这是一个难题。你可以使用一些故意不正确的XML配置文件,在ConfigurationSection中覆盖OnDeserializeUnrecognizedElement方法,然后有效地绕过文件到ConfigurationSection的映射(基本上手动设置属性)-需要进行一些重构,但仍然可以暴露相同的属性等。这有点奇怪,但可能可行。
我在这篇博客文章中详细介绍了如何使用LINQ to XML解决此问题。现在我的所有代码中都没有依赖于ConfigurationSection的类,我使用博客文章中描述的技术来绕过它并通过接口返回POCOs。这使得我的代码更易于单元测试,因为我可以轻松地为接口使用存根。
我还可以轻松地将我的配置移动到数据库中-我只需创建一个实现我的配置接口的新类,并在我的IoC配置中切换它。Microsoft没有设计灵活的配置系统,因此在使用自己的代码时必须考虑到这一点。
我能想到的另一种方法是将DB配置写入文件,然后读取该文件,但那也很奇怪!

3

我的建议是保留你当前的MyConfig类,但在构造函数中从数据库加载XML,然后在每个MyConfig属性中,您可以放入逻辑来确定从哪里获取值(数据库或.config文件),如果需要从任一位置拉取配置,或者如果该值为空,则让它回退。

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; }
    }
}

这是我最终采取的路线。 - Jonas Stawski

3
这是一个比较老的问题,但我正在尝试解决它。这个解决方案与Simon Mourier的方法类似(在某些方面上我更喜欢他的方法-不那么hacky),但意味着调用System.Configuration.ConfigurationManager.GetSection()的任何代码将继续工作,而无需将它们改为使用静态方法,因此可能导致整体代码变动较少。
首先要注意的是,我不知道这是否适用于嵌套部分,但我几乎可以肯定它不会。第二个要注意的是,它需要更改配置部分类,因此您只能将其与您拥有源代码(并且被允许更改!)的自定义部分一起使用。
第二个也是主要要注意的是,我只是在测试中使用它,既没有在开发中使用,也绝对没有在生产中使用,简单地将我的代码覆盖到基本功能上可能会产生未显示在我的示例中的连锁反应。请自行承担风险
(话虽如此,我正在一个Umbraco网站中测试它,有很多其他的配置部分,它们仍然都有效,所以我认为它没有立即可怕的影响)
编辑:这是.NET 4,而不是原始问题的3.5。不知道是否会有所不同。
因此,这是相当简单的代码,只需覆盖DeserializeSection以使用从数据库加载的XML读取器即可。
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"]; } }

    ...

    ...
}

我正在使用ASP.Net,所以为使其工作,您需要一个web.config文件,但是嘿,无论如何我都需要连接字符串的地方,否则我将无法连接到数据库。

您的自定义部分应按正常方式在<configSections/>中定义;使其起作用的关键是将一个空元素放置在您的常规设置位置上。 即在<TestSettings configSource="..."/>或内联设置的位置上,仅需放置<TestSettings/>

配置管理器将加载所有部分,查看现有的<TestSettings/>元素,并反序列化它,此时它会遇到您的覆盖并从数据库中加载XML。

注意:反序列化期望文档片段(它期望在读取器已定位到节点时调用),而不是整个文档,因此如果您的部分存储在单独的文件中,则必须首先删除<?xml ?>声明,否则您会得到Expected to find an element


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接