Змініть програму за умовчанням app.config під час виконання


130

У мене є така проблема:
У нас є додаток, який завантажує модулі (додає додатки). Ці модулі можуть потребувати записи в 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з відповідним файлом конфігурації - це не варіант?
Жоао Анджело

Насправді, тому що це призведе до безлічі перехресних додатків 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 (шлях). Я б тобі дав +2, якби міг!
JMarsch

2
@PhilWhittington: Ось що я говорю, так.
Даніель Гілгарт

2
з інтересу, чи є якась причина придушити фіналізатор, чи немає оголошеного фіналізатора?
Гусдор

3
Це вбік, використовуючи відображення для доступу до приватних полів, може працювати зараз, але воно може використовувати попередження про те, що воно не підтримується і може зламатися в майбутніх версіях .NET Framework.

10

Ви можете спробувати використовувати Configuration та Add 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);

EDIT: Ось рішення, засноване на рефлексії (не дуже приємно)

Створіть клас, похідний від 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.
Даніель Гілгарт

Ви можете додати розділи до наявної програми app.config. Щойно спробував це - працює для мене
Стеціа

Цитата з мого запитання: "Примітка. Я не хочу перезаписувати програму 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;
    }

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

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прочитати новий створений файл. Ваше рішення цього не робить.
Даніель Хілгарт

@Daniel: чому? Ви можете вказати будь-який файл у "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, ...). Якби я запровадив інший механізм конфігурації, конфігурація буде (продовження)
Даніель Гілгарт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.