Ändern Sie zur Laufzeit die Standard-app.config

130

Ich habe das folgende Problem:
Wir haben eine Anwendung, die Module lädt (Add-Ons). Diese Module benötigen möglicherweise Einträge in der app.config (z. B. WCF-Konfiguration). Da die Module dynamisch geladen werden, möchte ich diese Einträge nicht in der Datei app.config meiner Anwendung haben.
Was ich tun möchte, ist Folgendes:

  • Erstellen Sie eine neue app.config im Speicher, die die Konfigurationsabschnitte aus den Modulen enthält
  • Sagen Sie meiner Anwendung, dass sie diese neue app.config verwenden soll

Hinweis: Ich möchte die Standard-app.config nicht überschreiben!

Es sollte transparent funktionieren, damit zum Beispiel ConfigurationManager.AppSettingsdiese neue Datei verwendet wird.

Bei der Bewertung dieses Problems habe ich die gleiche Lösung gefunden, die hier bereitgestellt wird: Laden Sie app.config mit nunit neu .
Leider scheint es nichts zu tun, da ich die Daten immer noch aus der normalen app.config bekomme.

Ich habe diesen Code verwendet, um ihn zu testen:

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

Es gibt zweimal dieselben Werte aus, combinedConfigenthält jedoch andere Werte als die normale app.config.

Daniel Hilgarth
quelle
Das separate Hosten der Module AppDomainmit der entsprechenden Konfigurationsdatei ist keine Option?
João Angelo
Nicht wirklich, da dies zu vielen Cross-AppDomain-Aufrufen führen würde, da die Anwendung sehr stark mit den Modulen interagiert.
Daniel Hilgarth
Wie wäre es mit einem Neustart der Anwendung, wenn ein neues Modul geladen werden muss?
João Angelo
Dies funktioniert nicht mit den Geschäftsanforderungen zusammen. Außerdem kann ich die app.config nicht überschreiben, da der Benutzer nicht das Recht dazu hat.
Daniel Hilgarth
Sie würden neu laden, um eine andere App.config zu laden, nicht die in Programmdateien. Der Hack-In Reload app.config with nunitkönnte funktionieren, nicht sicher, ob er beim Anwendungseintrag verwendet wird, bevor eine Konfiguration geladen wird.
João Angelo

Antworten:

280

Der Hack in der verknüpften Frage funktioniert, wenn er verwendet wird, bevor das Konfigurationssystem zum ersten Mal verwendet wird. Danach funktioniert es nicht mehr.
Der Grund:
Es gibt eine Klasse ClientConfigPaths, die die Pfade zwischenspeichert. Daher wird der Pfad auch nach dem Ändern mit SetDatanicht erneut gelesen, da bereits zwischengespeicherte Werte vorhanden sind. Die Lösung besteht darin, auch diese zu entfernen:

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

Die Verwendung ist wie folgt:

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

Wenn Sie die verwendete app.config für die gesamte Laufzeit Ihrer Anwendung ändern möchten, fügen Sie sie einfach AppConfig.Change(tempFileName)ohne Verwendung irgendwo am Anfang Ihrer Anwendung ein.

Daniel Hilgarth
quelle
4
Das ist wirklich sehr, sehr gut. Vielen Dank für die Veröffentlichung.
user981225
3
@ Daniel Das war großartig - ich habe es in eine Erweiterungsmethode für ApplicationSettingsBase eingearbeitet, damit ich Settings.Default.RedirectAppConfig (Pfad) aufrufen kann. Ich würde dir +2 geben, wenn ich könnte!
JMarsch
2
@PhilWhittington: Das sage ich ja.
Daniel Hilgarth
2
Gibt es aus Interesse einen Grund, den Finalizer zu unterdrücken, wenn kein Finalizer deklariert ist?
Gusdor
3
Abgesehen davon funktioniert die Verwendung von Reflection für den Zugriff auf private Felder möglicherweise jetzt, es kann jedoch eine Warnung verwendet werden, dass sie nicht unterstützt wird und in zukünftigen Versionen von .NET Framework möglicherweise nicht funktioniert.
10

Sie können versuchen, Configuration und Add ConfigurationSection zur Laufzeit zu verwenden

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: Hier ist eine Lösung basierend auf Reflexion (aber nicht sehr schön)

Klasse erstellen von abgeleitet 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
}

dann über Reflexion auf privates Feld in setzen 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
quelle
Ich sehe nicht, wie mir das hilft. Dadurch wird der durch angegebenen Datei ein Abschnitt hinzugefügt file_path. Dadurch wird der Abschnitt für Benutzer von nicht verfügbar ConfigurationManager.GetSection, da GetSectiondie Standard-app.config verwendet wird.
Daniel Hilgarth
Sie können Ihrer vorhandenen app.config Abschnitte hinzufügen. Ich habe es gerade versucht - funktioniert für mich
Stecya
Zitat aus meiner Frage: "Hinweis: Ich möchte die Standard-app.config nicht überschreiben!"
Daniel Hilgarth
5
Was ist los mit dir? Einfach: Der Benutzer hat kein Recht, es zu überschreiben, da das Programm in% ProgramFiles% installiert ist und der Benutzer kein Administrator ist.
Daniel Hilgarth
2
@ Stecya: Danke für deine Mühe. Aber bitte sehen Sie meine Antwort für die wirkliche Lösung des Problems.
Daniel Hilgarth
5

@ Daniel-Lösung funktioniert OK. Eine ähnliche Lösung mit mehr Erklärung ist in der scharfen Ecke. Der Vollständigkeit halber möchte ich meine Version teilen: mit usingund die Bit-Flags abgekürzt.

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
quelle
4

Wenn jemand interessiert ist, ist hier eine Methode, die auf Mono funktioniert.

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
quelle
3

Daniels Lösung scheint sogar für nachgeschaltete Assemblys zu funktionieren, die ich zuvor mit AppDomain.SetData verwendet hatte, aber nicht wusste, wie die internen Konfigurationsflags zurückgesetzt werden sollten

Für Interessierte in C ++ / CLI konvertiert

/// <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;
}
Rechnung
quelle
1

Wenn Ihre Konfigurationsdatei nur mit Schlüssel / Werten in "appSettings" geschrieben wurde, können Sie eine andere Datei mit folgendem Code lesen:

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

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

Dann können Sie den Abschnitt lesen. Einstellungen als Auflistung von KeyValueConfigurationElement.

Ron
quelle
1
Wie ich bereits sagte, möchte ConfigurationManager.GetSectionich die neue Datei, die ich erstellt habe , lesen lassen. Ihre Lösung macht das nicht.
Daniel Hilgarth
@ Daniel: warum? Sie können eine beliebige Datei in "configFilePath" angeben. Sie müssen also nur den Speicherort Ihrer neu erstellten Datei kennen. Habe ich etwas verpasst ? Oder müssen Sie wirklich "ConfigurationManager.GetSection" und sonst nichts verwenden?
Ron
1
Ja, Sie vermissen etwas: ConfigurationManager.GetSectionVerwendet die Standard-app.config. Die Konfigurationsdatei, mit der Sie geöffnet haben, ist ihm egal OpenMappedExeConfiguration.
Daniel Hilgarth
1

Wunderbare Diskussion, ich habe der ResetConfigMechanism-Methode weitere Kommentare hinzugefügt, um die Magie hinter den Anweisungen / Aufrufen in der Methode zu verstehen. Es wurde auch eine Überprüfung des Dateipfads hinzugefügt

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
quelle
0

Daniel, wenn möglich, versuche andere Konfigurationsmechanismen zu verwenden. Wir haben diese Route durchlaufen, auf der wir je nach Umgebung / Profil / Gruppe unterschiedliche statische / dynamische Konfigurationsdateien hatten, und es wurde am Ende ziemlich chaotisch.

Sie können eine Art Profil-WebService ausprobieren, bei dem Sie nur eine Webdienst-URL vom Client angeben. Abhängig von den Details des Clients (möglicherweise Überschreibungen auf Gruppen- / Benutzerebene) werden alle erforderlichen Konfigurationen geladen. Wir haben auch MS Enterprise Library für einen Teil davon verwendet.

Das heißt, Sie stellen die Konfiguration nicht mit Ihrem Client bereit und können sie getrennt von Ihren Clients verwalten

Bek Raupov
quelle
3
Danke für deine Antwort. Der ganze Grund dafür ist jedoch, den Versand von Konfigurationsdateien zu vermeiden. Die Konfigurationsdetails für die Module werden aus einer Datenbank geladen. Da ich Modulentwicklern jedoch den Komfort des standardmäßigen .NET-Konfigurationsmechanismus bieten möchte, möchte ich diese Modulkonfigurationen zur Laufzeit in eine Konfigurationsdatei integrieren und diese zur Standardkonfigurationsdatei machen. Der Grund ist einfach: Es gibt viele Bibliotheken, die über app.config konfiguriert werden können (z. B. WCF, EntLib, EF, ...). Wenn ich einen anderen Konfigurationsmechanismus einführen würde, würde die Konfiguration (Forts.)
Daniel Hilgarth