Измените app.config по умолчанию во время выполнения

130

У меня следующая проблема: у
нас есть приложение, которое загружает модули (надстройки). Этим модулям могут потребоваться записи в app.config (например, конфигурация WCF). Поскольку модули загружаются динамически, я не хочу, чтобы эти записи были в файле app.config моего приложения.
Я бы хотел сделать следующее:

  • Создайте новый app.config в памяти, который включает разделы конфигурации из модулей
  • Скажите моему приложению использовать этот новый app.config

Примечание: я не хочу перезаписывать app.config по умолчанию!

Он должен работать прозрачно, чтобы, например, ConfigurationManager.AppSettingsиспользовать этот новый файл.

Во время оценки этой проблемы я пришел к тому же решению, что и здесь: перезагрузите app.config с помощью 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с соответствующим файлом конфигурации не вариант?
Жуан Анджело
Не совсем, потому что это приведет к большому количеству вызовов Cross-AppDomain, потому что приложение довольно сильно взаимодействует с модулями.
Дэниел Хилгарт
Как насчет перезапуска приложения, когда необходимо загрузить новый модуль?
Жуан Анджело
Это не работает вместе с бизнес-требованиями. Кроме того, я не могу перезаписать app.config, потому что пользователь не имеет на это права.
Дэниел Хилгарт
Вы бы перезагрузили, чтобы загрузить другой App.config, а не тот, который находится в программных файлах. Взлом Reload app.config with nunitмог бы сработать, не уверен, если использовать его при входе в приложение до загрузки какой-либо конфигурации.
Жуан Анджело

Ответы:

280

Взлом в связанном вопросе работает, если он используется до первого использования системы конфигурации. После этого больше не работает.
Причина:
существует класс, 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)без использования где-нибудь в начале вашего приложения.

Дэниэл Хилгарт
источник
4
Это действительно очень хорошо. Спасибо вам большое за размещение этого.
user981225
3
@Daniel Это было потрясающе - я превратил его в метод расширения для ApplicationSettingsBase, чтобы я мог вызвать Settings.Default.RedirectAppConfig (path). Я бы дал тебе +2, если бы мог!
JMarsch
2
@PhilWhittington: Да, я об этом и говорю.
Дэниел Хилгарт
2
из интереса, есть ли причина для подавления финализатора, если финализатор не объявлен?
Gusdor 01
3
Кроме того, использование отражения для доступа к закрытым полям сейчас может работать, но может использоваться предупреждение о том, что оно не поддерживается и может выйти из строя в будущих версиях .NET Framework.
10

Вы можете попробовать использовать 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
Stecya
источник
Не понимаю, как это мне помогает. Это добавит раздел в файл, указанный в file_path. Это не сделает этот раздел доступным для пользователей ConfigurationManager.GetSection, поскольку GetSectionпо умолчанию используется app.config.
Дэниел Хилгарт
Вы можете добавить разделы в существующий файл app.config. Только что попробовал - у меня работает
Stecya
Цитата из моего вопроса: «Примечание: я не хочу перезаписывать app.config по умолчанию!»
Дэниел Хилгарт
5
В чем дело? Просто: пользователь не имеет права перезаписывать его, потому что программа установлена ​​в% ProgramFiles% и пользователь не является администратором.
Дэниел Хилгарт
2
@Stecya: Спасибо за ваши усилия. Но, пожалуйста, посмотрите мой ответ, чтобы узнать о реальном решении проблемы.
Дэниел Хилгарт
5

Решение @Daniel работает нормально. Аналогичное решение с дополнительными пояснениями находится в остром углу. Для полноты картины хочу поделиться своей версией: с 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;
    }
Roland
источник
4

Если кому-то интересно, вот метод, который работает на 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);
LiohAu
источник
3

Решение Даниэля, похоже, работает даже для последующих сборок, которые я использовал ранее 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прочитать созданный мной новый файл. Ваше решение этого не делает.
Дэниел Хилгарт
@ Даниэль: почему? Вы можете указать любой файл в "configFilePath". Так что вам просто нужно знать, где находится ваш новый созданный файл. Я что-то пропустил ? Или вам действительно нужно использовать "ConfigurationManager.GetSection" и больше ничего?
Рон
1
Да, вы что-то упускаете: ConfigurationManager.GetSectionиспользуется app.config по умолчанию. Его не волнует файл конфигурации, который вы открыли OpenMappedExeConfiguration.
Дэниел Хилгарт
1

Замечательное обсуждение, я добавил больше комментариев к методу 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;
}
Венкатеш Муниянди
источник
0

Даниэль, если возможно, попробуйте использовать другие механизмы конфигурации. Мы прошли этот путь, где у нас были разные статические / динамические файлы конфигурации в зависимости от среды / профиля / группы, и в конце он стал довольно беспорядочным.

вы можете попробовать какой-то профиль WebService, где вы указываете только один URL-адрес веб-службы от клиента и в зависимости от деталей клиента (у вас могут быть переопределения на уровне группы / пользователя), он загружает всю необходимую конфигурацию. Мы также использовали библиотеку MS Enterprise для некоторой ее части.

это было то, что вы не развертывали конфигурацию со своим клиентом, и вы можете управлять ею отдельно от своих клиентов

Бек Раупов
источник
3
Спасибо за Ваш ответ. Однако вся причина этого заключается в том, чтобы избежать доставки файлов конфигурации. Детали конфигурации для модулей загружаются из базы данных. Но поскольку я хочу дать разработчикам модулей возможность пользоваться механизмом конфигурации .NET по умолчанию, я хочу объединить эти конфигурации модулей в один файл конфигурации во время выполнения и сделать его файлом конфигурации по умолчанию. Причина проста: существует множество библиотек, которые можно настроить через app.config (например, WCF, EntLib, EF, ...). Если бы я представил другой механизм конфигурации, конфигурация была бы (продолжение)
Дэниел Хилгарт