web-dev-qa-db-fra.com

Changer app.config par défaut à l'exécution

J'ai le problème suivant:
Nous avons une application qui charge des modules (add ons). Ces modules peuvent nécessiter des entrées dans le fichier app.config (par exemple, la configuration WCF). Les modules étant chargés dynamiquement, je ne souhaite pas que ces entrées soient dans le fichier app.config de mon application.
Ce que je voudrais faire est le suivant:

  • Créer un nouveau fichier app.config en mémoire qui intègre les sections de configuration des modules
  • Dire à mon application d'utiliser ce nouveau fichier app.config

Remarque: je ne veux pas écraser le fichier app.config par défaut!

Cela devrait fonctionner de manière transparente, de sorte que, par exemple, ConfigurationManager.AppSettings utilise ce nouveau fichier.

Lors de mon évaluation de ce problème, j'ai proposé la même solution que celle fournie ici: Recharger app.config avec nunit .
Malheureusement, il ne semble rien y faire, car j’obtiens toujours les données du fichier normal app.config.

J'ai utilisé ce code pour le tester:

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

Il imprime les mêmes valeurs deux fois, bien que combinedConfig contienne des valeurs autres que le fichier app.config normal.

126
Daniel Hilgarth

Le hack de la question liée fonctionne s'il est utilisé avant la première utilisation du système de configuration. Après cela, ça ne marche plus.
La raison:
Il existe une classe ClientConfigPaths qui met en cache les chemins. Ainsi, même après avoir changé le chemin avec SetData, il n'est pas relu, car il existe déjà des valeurs en cache. La solution consiste également à supprimer ceux-ci:

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'utilisation est comme ça:

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

Si vous souhaitez modifier le fichier app.config utilisé pour toute la durée d'exécution de votre application, il vous suffit de mettre AppConfig.Change(tempFileName) sans l'utiliser quelque part au début de votre application.

267
Daniel Hilgarth

Vous pouvez essayer d'utiliser Configuration et ajouter ConfigurationSection au moment de l'exécution

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: Voici une solution basée sur la réflexion (pas très agréable cependant)

Créer une classe dérivée de 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
}

puis, par réflexion, définissez-le sur un champ privé dans 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
10
Stecya

La solution @Daniel fonctionne bien. Une solution similaire, avec plus d'explications, se trouve dans coin c-sharp. Pour être complet, j'aimerais partager ma version: avec using, et l'indicateur de bits abrégé.

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
Roland

La solution de Daniel semble fonctionner même pour les assemblys en aval. J'avais déjà utilisé AppDomain.SetData auparavant, mais je ne savais pas comment réinitialiser les indicateurs de configuration internes.

Converti en C++/CLI pour les intéressés

/// <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;
}
2
Bill

Si quelqu'un est intéressé, voici une méthode qui fonctionne sur 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);
2
LiohAu

Si votre fichier de configuration est simplement écrit avec les clés/valeurs dans "appSettings", alors vous pouvez lire un autre fichier avec ce code:

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

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

Ensuite, vous pouvez lire section.Settings en tant que collection de KeyValueConfigurationElement.

1
Ron

Excellente discussion, j'ai ajouté plus de commentaires à la méthode ResetConfigMechanism pour comprendre la magie derrière la déclaration/les appels dans la méthode. Le chemin d'accès au fichier ajouté existe également

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

Daniel, essayez si possible d'utiliser d'autres mécanismes de configuration. Nous sommes passés par cette route où nous avions différents fichiers de configuration statiques/dynamiques en fonction de l’environnement/du profil/du groupe et qui sont devenus assez compliqués à la fin.

vous pouvez essayer une sorte de Profile WebService dans lequel vous ne spécifiez qu'une seule URL de service Web à partir du client et, en fonction des détails de celui-ci (vous pouvez définir des remplacements au niveau groupe/utilisateur), il charge toute la configuration dont il a besoin. Nous avons également utilisé MS Enterprise Library pour une partie de celle-ci.

c'est-à-dire que vous ne déployez pas la configuration avec votre client et que vous pouvez la gérer séparément de vos clients

0
Bek Raupov