在运行时更改默认的app.config

139

我有以下问题:
我们有一个加载模块(插件)的应用程序。这些模块可能需要在 app.config 中有条目(例如 WCF 配置)。由于这些模块是动态加载的,我不想在我的应用程序的 app.config 文件中添加这些条目。
我想做的是:

  • 在内存中创建一个新的 app.config,其中包括来自模块的配置部分
  • 告诉我的应用程序使用该新的 app.config

注意:我不想覆盖默认的 app.config!

它应该透明地工作,例如 ConfigurationManager.AppSettings 使用那个新文件。

在解决这个问题时,我提出了与此处提供的解决方案相同的解决方案:Reload app.config with nunit.
不幸的是,它似乎没有任何作用,因为我仍然从正常的 app.config 获取数据。

我使用了以下代码进行测试:

Console.WriteLine(ConfigurationManager.AppSettings["SettingA"]);
Console.WriteLine(Settings.Default.Setting);

var combinedConfig = string.Format(CONFIG2, CONFIG);
var tempFileName = Path.GetTempFileName();
using (var writer = new StreamWriter(tempFileName))
{
    writer.Write(combinedConfig);
}

using(AppConfig.Change(tempFileName))
{
    Console.WriteLine(ConfigurationManager.AppSettings["SettingA"]);
    Console.WriteLine(Settings.Default.Setting);
}

它会打印出相同的值两次,尽管combinedConfig包含除了正常的app.config之外的其他值。


将模块托管在具有适当配置文件的单独的“AppDomain”中不是一个选项吗? - João Angelo
不是很好,因为这会导致大量的跨应用程序域调用,因为该应用程序与模块之间的交互非常频繁。 - Daniel Hilgarth
当需要加载新模块时,重新启动应用程序怎么样? - João Angelo
这与业务需求不兼容。此外,我无法覆盖app.config文件,因为用户没有权限这样做。 - Daniel Hilgarth
@João:我找到了一个解决方案。如果你感兴趣的话,请看我的答案。 - Daniel Hilgarth
显示剩余4条评论
8个回答

297
链接中提到的 hack 只适用于在配置系统第一次使用之前使用。之后,它就不再起作用了。 原因是: 存在一个名为 ClientConfigPaths 的类缓存了路径。因此,即使使用 SetData 更改了路径,也不会重新读取它,因为已经存在缓存值。解决方法是也要移除这些缓存值:
using System;
using System.Configuration;
using System.Linq;
using System.Reflection;

public abstract class AppConfig : IDisposable
{
    public static AppConfig Change(string path)
    {
        return new ChangeAppConfig(path);
    }

    public abstract void Dispose();

    private class ChangeAppConfig : AppConfig
    {
        private readonly string oldConfig =
            AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE").ToString();

        private bool disposedValue;

        public ChangeAppConfig(string path)
        {
            AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path);
            ResetConfigMechanism();
        }

        public override void Dispose()
        {
            if (!disposedValue)
            {
                AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", oldConfig);
                ResetConfigMechanism();


                disposedValue = true;
            }
            GC.SuppressFinalize(this);
        }

        private static void ResetConfigMechanism()
        {
            typeof(ConfigurationManager)
                .GetField("s_initState", BindingFlags.NonPublic | 
                                         BindingFlags.Static)
                .SetValue(null, 0);

            typeof(ConfigurationManager)
                .GetField("s_configSystem", BindingFlags.NonPublic | 
                                            BindingFlags.Static)
                .SetValue(null, null);

            typeof(ConfigurationManager)
                .Assembly.GetTypes()
                .Where(x => x.FullName == 
                            "System.Configuration.ClientConfigPaths")
                .First()
                .GetField("s_current", BindingFlags.NonPublic | 
                                       BindingFlags.Static)
                .SetValue(null, null);
        }
    }
}

使用方法如下:

// the default app.config is used.
using(AppConfig.Change(tempFileName))
{
    // the app.config in tempFileName is used
}
// the default app.config is used.

如果您想在整个应用程序运行时更改所使用的 app.config,只需在应用程序启动时的某个地方放置AppConfig.Change(tempFileName),而不使用任何内容。


3
@Daniel,太棒了!我将它应用为ApplicationSettingsBase的扩展方法,这样我就可以调用Settings.Default.RedirectAppConfig(path)了。如果可以的话我会给你+2的! - JMarsch
2
@PhilWhittington:是的,这就是我所说的。 - Daniel Hilgarth
2
有趣的是,如果没有声明终结器,有抑制终结器的原因吗? - Gusdor
2
@DanielHilgarth 标准的实现Dispose调用GC.SuppressFinalize模式涉及到一个Dispose()非虚方法和一个Dispose(bool)虚方法。你并没有真正使用标准的Dispose模式。无论如何,即使基类没有终结器,GC.SuppressFinalize的目的是因为派生类可能有,但除非你真的想支持其他从AppConfig派生的类,否则没有任何好处。事实上,我会给AppConfig一个私有构造函数,以便其他类不能将其用作基类。 - user743382
3
除此之外,使用反射访问私有字段可能现在可行,但需要警告它不被支持,并且在未来的 .NET Framework 版本中可能会出现问题。 - user743382
显示剩余25条评论

11

您可以尝试在运行时使用Configuration并添加ConfigurationSection

Configuration applicationConfiguration = ConfigurationManager.OpenMappedExeConfiguration(
                        new ExeConfigurationFileMap(){ExeConfigFilename = path_to_your_config,
                        ConfigurationUserLevel.None
                        );

applicationConfiguration.Sections.Add("section",new YourSection())
applicationConfiguration.Save(ConfigurationSaveMode.Full,true);

编辑:这里有一个基于反射的解决方案(虽然不是很优雅)

创建从IInternalConfigSystem派生的类。

public class ConfigeSystem: IInternalConfigSystem
{
    public NameValueCollection Settings = new NameValueCollection();
    #region Implementation of IInternalConfigSystem

    public object GetSection(string configKey)
    {
        return Settings;
    }

    public void RefreshConfig(string sectionName)
    {
        //throw new NotImplementedException();
    }

    public bool SupportsUserConfig { get; private set; }

    #endregion
}

然后通过反射将其设置为ConfigurationManager中的私有字段。

        ConfigeSystem configSystem = new ConfigeSystem();
        configSystem.Settings.Add("s1","S");

        Type type = typeof(ConfigurationManager);
        FieldInfo info = type.GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static);
        info.SetValue(null, configSystem);

        bool res = ConfigurationManager.AppSettings["s1"] == "S"; // return true

我不明白这如何对我有帮助。这将在指定file_path文件中添加一个部分。但是,它并不会使ConfigurationManager.GetSection的用户可用,因为GetSection使用默认的app.config。 - Daniel Hilgarth
你可以向现有的 app.config 添加节(Sections)。我刚试过了,对我有效。 - Stecya
请注意:我不想覆盖默认的app.config! - Daniel Hilgarth
覆盖 app.config 有什么问题吗?当你完成使用插件后,你总是可以从 app.config 中删除那些不必要的部分。 - Stecya
5
什么出了问题?很简单:用户没有权力覆盖它,因为该程序被安装在%ProgramFiles%目录下,而该用户不是管理员。 - Daniel Hilgarth
2
@Stecya:谢谢你的努力。但请看一下我的答案,那才是真正的问题解决方案。 - Daniel Hilgarth

6

@Daniel的解决方案可以正常工作。 一个类似的解决方案并附有更多解释在c-sharp corner上。 为了完整性,我想分享我的版本:使用using语句,并且位标志被缩写。

using System;//AppDomain
using System.Linq;//Where
using System.Configuration;//app.config
using System.Reflection;//BindingFlags

    /// <summary>
    /// Use your own App.Config file instead of the default.
    /// </summary>
    /// <param name="NewAppConfigFullPathName"></param>
    public static void ChangeAppConfig(string NewAppConfigFullPathName)
    {
        AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", NewAppConfigFullPathName);
        ResetConfigMechanism();
        return;
    }

    /// <summary>
    /// Remove cached values from ClientConfigPaths.
    /// Call this after changing path to App.Config.
    /// </summary>
    private static void ResetConfigMechanism()
    {
        BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static;
        typeof(ConfigurationManager)
            .GetField("s_initState", Flags)
            .SetValue(null, 0);

        typeof(ConfigurationManager)
            .GetField("s_configSystem", Flags)
            .SetValue(null, null);

        typeof(ConfigurationManager)
            .Assembly.GetTypes()
            .Where(x => x.FullName == "System.Configuration.ClientConfigPaths")
            .First()
            .GetField("s_current", Flags)
            .SetValue(null, null);
        return;
    }

5

如果有人感兴趣,这里有一种在Mono上有效的方法。

string configFilePath = ".../App";
System.Configuration.Configuration newConfiguration = ConfigurationManager.OpenExeConfiguration(configFilePath);
FieldInfo configSystemField = typeof(ConfigurationManager).GetField("configSystem", BindingFlags.NonPublic | BindingFlags.Static);
object configSystem = configSystemField.GetValue(null);
FieldInfo cfgField = configSystem.GetType().GetField("cfg", BindingFlags.Instance | BindingFlags.NonPublic);
cfgField.SetValue(configSystem, newConfiguration);

4

非常好的讨论,我已经添加了更多的注释到ResetConfigMechanism方法中,以理解该方法中声明/调用的神奇操作。同时也添加了文件路径存在性检查。

using System;//AppDomain
using System.Linq;//Where
using System.Configuration;//app.config
using System.Reflection;//BindingFlags
using System.Io;

/// <summary>
/// Use your own App.Config file instead of the default.
/// </summary>
/// <param name="NewAppConfigFullPathName"></param>
public static void ChangeAppConfig(string NewAppConfigFullPathName)
{
    if(File.Exists(NewAppConfigFullPathName)
    {
      AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", 
      NewAppConfigFullPathName);
      ResetConfigMechanism();
      return;
    }
}

/// <summary>
/// Remove cached values from ClientConfigPaths.
/// Call this after changing path to App.Config.
/// </summary>
private static void ResetConfigMechanism()
{
    BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Static;
      /* s_initState holds one of the four internal configuration state.
          0 - Not Started, 1 - Started, 2 - Usable, 3- Complete

         Setting to 0 indicates the configuration is not started, this will 
         hint the AppDomain to reaload the most recent config file set thru 
         .SetData call
         More [here][1]

      */
    typeof(ConfigurationManager)
        .GetField("s_initState", Flags)
        .SetValue(null, 0);


    /*s_configSystem holds the configuration section, this needs to be set 
        as null to enable reload*/
    typeof(ConfigurationManager)
        .GetField("s_configSystem", Flags)
        .SetValue(null, null);

      /*s_current holds the cached configuration file path, this needs to be 
         made null to fetch the latest file from the path provided 
        */
    typeof(ConfigurationManager)
        .Assembly.GetTypes()
        .Where(x => x.FullName == "System.Configuration.ClientConfigPaths")
        .First()
        .GetField("s_current", Flags)
        .SetValue(null, null);
    return;
}

3

Daniel的解决方案似乎可以适用于下游程序集,我之前使用过AppDomain.SetData,但不知道如何重置内部配置标志。

对于那些感兴趣的人,可以将其转换为C++/CLI。

/// <summary>
/// Remove cached values from ClientConfigPaths.
/// Call this after changing path to App.Config.
/// </summary>
void ResetConfigMechanism()
{
    BindingFlags Flags = BindingFlags::NonPublic | BindingFlags::Static;
    Type ^cfgType = ConfigurationManager::typeid;

    Int32 ^zero = gcnew Int32(0);
    cfgType->GetField("s_initState", Flags)
        ->SetValue(nullptr, zero);

    cfgType->GetField("s_configSystem", Flags)
        ->SetValue(nullptr, nullptr);

    for each(System::Type ^t in cfgType->Assembly->GetTypes())
    {
        if (t->FullName == "System.Configuration.ClientConfigPaths")
        {
            t->GetField("s_current", Flags)->SetValue(nullptr, nullptr);
        }
    }

    return;
}

/// <summary>
/// Use your own App.Config file instead of the default.
/// </summary>
/// <param name="NewAppConfigFullPathName"></param>
void ChangeAppConfig(String ^NewAppConfigFullPathName)
{
    AppDomain::CurrentDomain->SetData(L"APP_CONFIG_FILE", NewAppConfigFullPathName);
    ResetConfigMechanism();
    return;
}

1
如果您的配置文件只是在“appSettings”中以键/值形式编写的,则可以使用以下代码读取另一个文件:
System.Configuration.ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = configFilePath;

System.Configuration.Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
AppSettingsSection section = (AppSettingsSection)configuration.GetSection("appSettings");

然后,您可以将section.Settings读取为KeyValueConfigurationElement集合。


1
正如我之前所说,我想让 ConfigurationManager.GetSection 读取我创建的新文件。你的解决方案并没有做到这一点。 - Daniel Hilgarth
@Daniel:为什么?你可以在“configFilePath”中指定任何文件。所以你只需要知道你新创建的文件的位置。我错过了什么吗?或者你真的需要使用“ConfigurationManager.GetSection”,而不是其他东西吗? - Ron
1
是的,你确实错过了一些东西:ConfigurationManager.GetSection使用默认的app.config。它不关心你用OpenMappedExeConfiguration打开的配置文件。 - Daniel Hilgarth

0

Daniel,如果可能的话,请尝试使用其他配置机制。我们曾经走过这条路,根据环境/配置文件/组别有不同的静态/动态配置文件,最终变得非常混乱。

你可以尝试一些类似于Profile WebService的东西,客户端只需指定一个Web服务URL,根据客户端的详细信息(您可能具有组/用户级别的覆盖),它会加载所需的所有配置。我们还使用了MS Enterprise Library的某些部分。

这样你就不需要将配置与客户端一起部署,而是可以单独管理它。


3
谢谢你的回答。然而,这个做法的整个目的就是为了避免运输配置文件。模块的配置细节从数据库中加载。但是,由于我想让模块开发人员能够享受到默认的.NET配置机制,我想在运行时将这些模块配置合并到一个配置文件中,并将其作为默认配置文件。原因很简单:存在很多可以通过app.config进行配置的库(例如WCF、EntLib、EF等)。如果我引入另一种配置机制,配置会(继续) - Daniel Hilgarth

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