Zmień domyślny plik app.config w czasie wykonywania

131

Mam następujący problem:
Mamy aplikację, która ładuje moduły (dodatki). Te moduły mogą wymagać wpisów w pliku app.config (np. Konfiguracji WCF). Ponieważ moduły są ładowane dynamicznie, nie chcę, aby te wpisy były w pliku app.config mojej aplikacji.
Chciałbym wykonać następujące czynności:

  • Utwórz nowy plik app.config w pamięci, który zawiera sekcje konfiguracyjne z modułów
  • Powiedz mojej aplikacji, aby używała tego nowego pliku app.config

Uwaga: nie chcę nadpisywać domyślnego pliku app.config!

Powinien działać w sposób przejrzysty, aby na przykład ConfigurationManager.AppSettingsużywał tego nowego pliku.

Podczas oceny tego problemu wpadłem na to samo rozwiązanie, które podano tutaj: Załaduj ponownie plik app.config za pomocą nunit .
Niestety wydaje się, że nic nie daje, ponieważ nadal otrzymuję dane z normalnego pliku app.config.

Użyłem tego kodu, aby to przetestować:

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

Drukuje te same wartości dwa razy, chociaż combinedConfigzawiera inne wartości niż zwykły plik app.config.

Daniel Hilgarth
źródło
Hostowanie modułów osobno AppDomainz odpowiednim plikiem konfiguracyjnym nie wchodzi w grę?
João Angelo
Niezupełnie, ponieważ spowodowałoby to wiele wywołań Cross-AppDomain, ponieważ aplikacja dość mocno współdziała z modułami.
Daniel Hilgarth
A co z ponownym uruchomieniem aplikacji, gdy trzeba załadować nowy moduł?
João Angelo
To nie działa razem z wymaganiami biznesowymi. Ponadto nie mogę nadpisać pliku app.config, ponieważ użytkownik nie ma do tego prawa.
Daniel Hilgarth
Załadowałbyś ponownie, aby załadować inny plik App.config, a nie ten w plikach programu. Włamanie Reload app.config with nunitmoże zadziałać, nie jestem pewien, jeśli zostanie użyte przy wejściu do aplikacji przed załadowaniem jakiejkolwiek konfiguracji.
João Angelo

Odpowiedzi:

281

Hack w powiązanym pytaniu działa, jeśli zostanie użyty przed pierwszym użyciem systemu konfiguracyjnego. Potem już nie działa.
Powód:
istnieje klasa, ClientConfigPathsktóra buforuje ścieżki. Tak więc nawet po zmianie ścieżki za pomocą SetDatanie jest ona ponownie odczytywana, ponieważ istnieją już wartości w pamięci podręcznej. Rozwiązaniem jest usunięcie również tych:

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

Użycie jest takie:

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

Jeśli chcesz zmienić używany plik app.config na cały czas wykonywania swojej aplikacji, po prostu umieść go AppConfig.Change(tempFileName)bez użycia gdzieś na początku aplikacji.

Daniel Hilgarth
źródło
4
To jest naprawdę świetne. Dziękuję bardzo za opublikowanie tego.
user981225
3
@Daniel To było niesamowite - zastosowałem to w metodzie exentension dla ApplicationSettingsBase, dzięki czemu mogę wywołać Settings.Default.RedirectAppConfig (ścieżka). Dałbym ci +2, gdybym mógł!
JMarsch,
2
@PhilWhittington: Tak właśnie mówię, tak.
Daniel Hilgarth,
2
z interesującego nas, czy jest jakiś powód do pomijania finalizatora, czy nie ma zadeklarowanego finalizatora?
Gusdor
3
Poza tym użycie odbicia w celu uzyskania dostępu do pól prywatnych może teraz działać, ale może zawierać ostrzeżenie, że nie jest obsługiwane i może się zepsuć w przyszłych wersjach .NET Framework.
10

Możesz spróbować użyć Configuration i Add ConfigurationSection w czasie wykonywania

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

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

EDYCJA: Oto rozwiązanie oparte na refleksji (choć niezbyt ładne)

Utwórz klasę pochodną z 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
}

następnie przez odbicie ustaw na pole prywatne w 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
źródło
Nie wiem, jak mi to pomaga. Spowoduje to dodanie sekcji do pliku określonego przez file_path. Nie spowoduje to udostępnienia sekcji użytkownikom programu ConfigurationManager.GetSection, ponieważ GetSectionużywa domyślnego pliku app.config.
Daniel Hilgarth
Możesz dodać sekcje do istniejącego pliku app.config. Właśnie próbowałem tego - działa dla mnie
Stecya
Cytat z mojego pytania: „Uwaga: nie chcę nadpisywać domyślnego pliku app.config!”
Daniel Hilgarth
5
Co jest nie tak? Proste: użytkownik nie ma prawa go nadpisać, ponieważ program jest zainstalowany w% ProgramFiles%, a użytkownik nie jest administratorem.
Daniel Hilgarth
2
@Stecya: Dziękuję za twój wysiłek. Ale proszę zapoznać się z moją odpowiedzią na rzeczywiste rozwiązanie problemu.
Daniel Hilgarth
5

Rozwiązanie @Daniel działa OK. Podobne rozwiązanie z większym wyjaśnieniem znajduje się w ostrym narożniku. Dla kompletności chciałbym udostępnić moją wersję: z using, a flagi bitowe zostały skrócone.

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
źródło
4

Jeśli ktoś jest zainteresowany, oto metoda, która działa na 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
źródło
3

Rozwiązanie Daniela wydaje się działać nawet w przypadku zestawów podrzędnych, z których korzystałem wcześniej AppDomain.SetData, ale nie wiedziałem, jak zresetować wewnętrzne flagi konfiguracji

Przekonwertowany na C ++ / CLI dla zainteresowanych

/// <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;
}
Rachunek
źródło
1

Jeśli plik konfiguracyjny jest właśnie zapisany z kluczami / wartościami w „appSettings”, możesz odczytać inny plik z takim kodem:

System.Configuration.ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = configFilePath;

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

Następnie możesz przeczytać sekcję.Settings jako kolekcja KeyValueConfigurationElement.

Ron
źródło
1
Jak już powiedziałem, chcę, aby ConfigurationManager.GetSectionodczytał nowy plik, który utworzyłem. Twoje rozwiązanie tego nie robi.
Daniel Hilgarth
@Daniel: dlaczego? Możesz określić dowolny plik w „configFilePath”. Musisz więc tylko znać lokalizację nowo utworzonego pliku. Przegapiłem coś ? Czy naprawdę potrzebujesz użyć „ConfigurationManager.GetSection” i niczego więcej?
Ron,
1
Tak, coś przeoczyłeś: ConfigurationManager.GetSectionużywa domyślnego pliku app.config. Nie obchodzi go plik konfiguracyjny, który otworzyłeś OpenMappedExeConfiguration.
Daniel Hilgarth
1

Cudowna dyskusja, dodałem więcej komentarzy do metody ResetConfigMechanism, aby zrozumieć magię instrukcji / wywołań w metodzie. Dodano również sprawdzenie istnienia ścieżki pliku

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;
}
Venkatesh Muniyandi
źródło
0

Daniel, jeśli to możliwe, spróbuj użyć innych mechanizmów konfiguracyjnych. Przeszliśmy przez tę trasę, w której mieliśmy różne statyczne / dynamiczne pliki konfiguracyjne w zależności od środowiska / profilu / grupy i na końcu stało się dość niechlujne.

możesz wypróbować pewnego rodzaju usługę Profile WebService, w której określasz tylko jeden adres URL usługi sieci Web z klienta iw zależności od szczegółów klienta (możesz mieć nadpisania na poziomie grupy / użytkownika), ładuje całą potrzebną konfigurację. W części wykorzystaliśmy również bibliotekę MS Enterprise.

to znaczy, że nie wdrażasz konfiguracji z klientem i możesz nią zarządzać niezależnie od klientów

Bek Raupov
źródło
3
Dzięki za odpowiedź. Jednak jedynym tego powodem jest uniknięcie wysyłania plików konfiguracyjnych. Szczegóły konfiguracji modułów są ładowane z bazy danych. Ale ponieważ chcę zapewnić programistom modułów komfort domyślnego mechanizmu konfiguracji .NET, chcę włączyć te konfiguracje modułów do jednego pliku konfiguracyjnego w czasie wykonywania i ustawić go jako domyślny plik konfiguracyjny. Powód jest prosty: istnieje wiele bibliotek, które można skonfigurować za pomocą app.config (np. WCF, EntLib, EF, ...). Gdybym wprowadził inny mechanizm konfiguracyjny, konfiguracja byłaby (cd.)
Daniel Hilgarth