Cambia app.config predefinito in fase di esecuzione


130

Ho il seguente problema:
Abbiamo un'applicazione che carica i moduli (componenti aggiuntivi). Questi moduli potrebbero richiedere voci in app.config (ad es. Configurazione WCF). Poiché i moduli vengono caricati in modo dinamico, non voglio avere queste voci nel file app.config della mia applicazione.
Quello che vorrei fare è il seguente:

  • Creare un nuovo app.config in memoria che incorpora le sezioni di configurazione dai moduli
  • Di 'alla mia applicazione di usare quella nuova app.config

Nota: non voglio sovrascrivere app.config predefinito!

Dovrebbe funzionare in modo trasparente, in modo che ad esempio ConfigurationManager.AppSettingsutilizzi quel nuovo file.

Durante la mia valutazione di questo problema, ho trovato la stessa soluzione fornita qui: Ricarica app.config con nunit .
Sfortunatamente, non sembra fare nulla, perché continuo a ottenere i dati dalla normale app.config.

Ho usato questo codice per testarlo:

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

Stampa gli stessi valori due volte, sebbene combinedConfigcontenga valori diversi dalla normale app.config.


Ospitare i moduli separatamente AppDomaincon il file di configurazione appropriato non è un'opzione?
João Angelo,

Non proprio, perché ciò comporterebbe molte chiamate Cross-AppDomain, perché l'applicazione interagisce abbastanza pesantemente con i moduli.
Daniel Hilgarth,

Che ne dici di un riavvio dell'applicazione quando è necessario caricare un nuovo modulo?
João Angelo,

Questo non funziona insieme ai requisiti aziendali. Inoltre, non posso sovrascrivere app.config, perché l'utente non ha il diritto di farlo.
Daniel Hilgarth,

Ricaricheresti per caricare un App.config diverso, non quello nei file di programma. L'hacking Reload app.config with nunitpotrebbe funzionare, non è sicuro, se utilizzato sulla voce dell'applicazione prima che venga caricata una configurazione.
João Angelo,

Risposte:


280

L'hack nella domanda collegata funziona se viene utilizzato prima di utilizzare il sistema di configurazione per la prima volta. Dopodiché, non funziona più.
Il motivo:
esiste una classe ClientConfigPathsche memorizza nella cache i percorsi. Quindi, anche dopo aver modificato il percorso con SetData, non viene riletto, poiché esistono già valori memorizzati nella cache. La soluzione è rimuovere anche questi:

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

L'utilizzo è così:

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

Se vuoi cambiare l'app.config usato per l'intero runtime della tua applicazione, mettilo semplicemente AppConfig.Change(tempFileName)senza usare da qualche parte all'inizio della tua applicazione.


4
Questo è davvero eccellente. Grazie mille per aver pubblicato questo.
user981225

3
@Daniel È stato fantastico - l'ho lavorato in un metodo di esenzione per ApplicationSettingsBase, in modo da poter chiamare Settings.Default.RedirectAppConfig (percorso). Ti darei +2 se potessi!
JMarsch,

2
@PhilWhittington: è quello che sto dicendo, sì.
Daniel Hilgarth,

2
per interesse, c'è qualche motivo per sopprimere il finalizzatore se non è stato dichiarato alcun finalizzatore?
Gusdor,

3
A parte questo, l'utilizzo di reflection per accedere ai campi privati ​​potrebbe funzionare ora, ma potrebbe utilizzare un avviso che non è supportato e potrebbe interrompersi nelle versioni future di .NET Framework.

10

Puoi provare a utilizzare Configuration e Aggiungi ConfigurationSection in fase di runtime

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: ecco una soluzione basata sulla riflessione (non molto carina però)

Crea classe derivata da 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
}

quindi, tramite la riflessione, impostalo su un campo privato in 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

Non vedo come questo mi aiuti. Ciò aggiungerà una sezione al file specificato da file_path. Questo non renderà la sezione disponibile agli utenti di ConfigurationManager.GetSection, perché GetSectionusa l'app.config predefinito.
Daniel Hilgarth,

È possibile aggiungere sezioni al file app.config esistente. Ho appena provato questo - funziona per me
Stecya

Citazione dalla mia domanda: "Nota: non voglio sovrascrivere l'app.config predefinito!"
Daniel Hilgarth,

5
Cosa c'è che non va? Semplice: l'utente non ha il diritto di sovrascriverlo, poiché il programma è installato in% ProgramFiles% e l'utente non è un amministratore.
Daniel Hilgarth,

2
@Stecya: grazie per il tuo impegno. Ma per favore vedi la mia risposta per la vera soluzione al problema.
Daniel Hilgarth,

5

La soluzione @ Daniel funziona bene. Una soluzione simile con più spiegazioni è nell'angolo acuto. Per completezza, vorrei condividere la mia versione: con usinge i bit flag abbreviati.

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

Se qualcuno è interessato, ecco un metodo che funziona su 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

La soluzione di Daniel sembra funzionare anche per gli assembly downstream che avevo già usato AppDomain.SetData, ma non ero a conoscenza di come ripristinare i flag di configurazione interni

Convertito in C ++ / CLI per gli interessati

/// <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

Se il tuo file di configurazione è appena scritto con chiave / valori in "appSettings", puoi leggere un altro file con tale codice:

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

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

Quindi puoi leggere section.Settings come raccolta di KeyValueConfigurationElement.


1
Come ho già detto, voglio ConfigurationManager.GetSectionleggere il nuovo file che ho creato. La tua soluzione non lo fa.
Daniel Hilgarth,

@Daniel: perché? È possibile specificare qualsiasi file in "configFilePath". Quindi devi solo conoscere la posizione del tuo nuovo file creato. Ho dimenticato qualcosa ? O hai davvero bisogno di usare "ConfigurationManager.GetSection" e nient'altro?
Ron,

1
Sì, ti manca qualcosa: ConfigurationManager.GetSectionusa l'app.config predefinito. Non importa del file di configurazione che hai aperto OpenMappedExeConfiguration.
Daniel Hilgarth,

1

Discussione meravigliosa, ho aggiunto altri commenti al metodo ResetConfigMechanism per capire la magia dietro l'affermazione / le chiamate nel metodo. Anche il controllo del percorso del file aggiunto esiste

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

Daniel, se possibile, prova ad usare altri meccanismi di configurazione. Abbiamo attraversato questa strada in cui avevamo diversi file di configurazione statici / dinamici a seconda dell'ambiente / profilo / gruppo e alla fine è diventato piuttosto disordinato.

potresti provare una sorta di WebService di profilo in cui specifichi solo un URL del servizio Web dal client e, a seconda dei dettagli del cliente (potresti avere sostituzioni a livello di gruppo / utente), carica tutta la configurazione di cui ha bisogno. Abbiamo anche usato MS Enterprise Library per una parte di esso.

che non hai distribuito config con il tuo client e puoi gestirlo separatamente dai tuoi client


3
Grazie per la tua risposta. Tuttavia, l'intera ragione di ciò è evitare di spedire i file di configurazione. I dettagli di configurazione per i moduli vengono caricati da un database. Ma poiché voglio offrire agli sviluppatori di moduli il comfort del meccanismo di configurazione .NET predefinito, voglio incorporare quelle configurazioni del modulo in un file di configurazione in fase di esecuzione e rendere questo il file di configurazione predefinito. Il motivo è semplice: esistono molte librerie che possono essere configurate tramite app.config (ad esempio WCF, EntLib, EF, ...). Se dovessi introdurre un altro meccanismo di configurazione, la configurazione verrebbe (continua)
Daniel Hilgarth
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.