Modifier app.config par défaut au moment de l'exécution

130

J'ai le problème suivant:
nous avons une application qui charge des modules (add-ons). Ces modules peuvent avoir besoin d'entrées dans app.config (par exemple, configuration WCF). Étant donné que les modules sont chargés dynamiquement, je ne veux pas avoir ces entrées dans le fichier app.config de mon application.
Ce que je voudrais faire est le suivant:

  • Créez un nouveau app.config en mémoire qui incorpore les sections de configuration des modules
  • Dites à mon application d'utiliser cette nouvelle application.config

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

Il devrait fonctionner de manière transparente, de sorte que par exemple ConfigurationManager.AppSettingsutilise ce nouveau fichier.

Lors de mon évaluation de ce problème, j'ai trouvé la même solution que celle fournie ici: Recharger app.config avec nunit .
Malheureusement, cela ne semble rien faire, car je récupère toujours les données du fichier app.config normal.

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 deux fois les mêmes valeurs, bien qu'il combinedConfigcontienne d'autres valeurs que le fichier app.config normal.

Daniel Hilgarth
la source
Héberger les modules séparément AppDomainavec le fichier de configuration approprié n'est pas une option?
João Angelo
Pas vraiment, car cela entraînerait de nombreux appels Cross-AppDomain, car l'application interagit assez fortement avec les modules.
Daniel Hilgarth
Que diriez-vous d'un redémarrage d'application lorsqu'un nouveau module doit être chargé?
João Angelo
Cela ne fonctionne pas avec les exigences de l'entreprise. De plus, je ne peux pas écraser app.config, car l'utilisateur n'a pas le droit de le faire.
Daniel Hilgarth
Vous rechargeriez pour charger un autre App.config, pas celui des fichiers programme. Le piratage Reload app.config with nunitpourrait fonctionner, pas sûr, s'il est utilisé sur l'entrée de l'application avant le chargement de toute configuration.
João Angelo

Réponses:

280

Le piratage de la question liée fonctionne s'il est utilisé avant la première utilisation du système de configuration. Après ça, ça ne marche plus.
La raison:
il existe une classe ClientConfigPathsqui 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 mises en cache. La solution est de les supprimer également:

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 ceci:

// 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 l'ensemble de l'exécution de votre application, il suffit de le mettre AppConfig.Change(tempFileName)sans utiliser quelque part au début de votre application.

Daniel Hilgarth
la source
4
C'est vraiment, vraiment excellent. Merci beaucoup d'avoir posté cela.
user981225
3
@Daniel C'était génial - je l'ai intégré dans une méthode d'extension pour ApplicationSettingsBase, afin que je puisse appeler Settings.Default.RedirectAppConfig (chemin). Je te donnerais +2 si je le pouvais!
JMarsch
2
@PhilWhittington: C'est ce que je dis, oui.
Daniel Hilgarth
2
par intérêt, y a-t-il une raison de supprimer le finaliseur s'il n'y a aucun finaliseur déclaré?
Gusdor
3
Cela mis à part, l'utilisation de la réflexion pour accéder aux champs privés peut fonctionner maintenant, mais elle pourrait utiliser un avertissement indiquant qu'elle n'est pas prise en charge et pourrait être interrompue dans les versions futures du .NET Framework.
10

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 sympa 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
Stecya
la source
Je ne vois pas comment cela m'aide. Cela ajoutera une section au fichier spécifié par file_path. Cela ne rendra pas la section disponible pour les utilisateurs de ConfigurationManager.GetSection, car GetSectionutilise le fichier app.config par défaut.
Daniel Hilgarth
Vous pouvez ajouter des sections à votre app.config existant. Je viens d'essayer ça - ça marche pour moi
Stecya
Citation de ma question: "Remarque: je ne veux pas écraser le fichier app.config par défaut!"
Daniel Hilgarth
5
Qu'est-ce qui ne va pas? Simple: l'utilisateur n'a pas le droit de l'écraser, car le programme est installé dans% ProgramFiles% et l'utilisateur n'est pas administrateur.
Daniel Hilgarth
2
@Stecya: Merci pour vos efforts. Mais s'il vous plaît voir ma réponse pour la vraie solution au problème.
Daniel Hilgarth
5

La solution @Daniel fonctionne bien. Une solution similaire avec plus d'explications est dans le coin c-sharp. Par souci d'exhaustivité, j'aimerais partager ma version: avec using, et les indicateurs de bits abrégés.

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
la source
4

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);
LiohAu
la source
3

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

Converti en C ++ / CLI pour les personnes intéressées

/// <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;
}
Facture
la source
1

Si votre fichier de configuration est juste écrit avec des clés / valeurs dans "appSettings", vous pouvez lire un autre fichier avec un tel 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.

Ron
la source
1
Comme je l'ai déjà dit, je souhaite faire ConfigurationManager.GetSectionlire le nouveau fichier que j'ai créé. Votre solution ne fait pas cela.
Daniel Hilgarth
@Daniel: pourquoi? Vous pouvez spécifier n'importe quel fichier dans "configFilePath". Il vous suffit donc de connaître l'emplacement de votre nouveau fichier créé. Ai-je oublié quelque chose ? Ou vous avez vraiment besoin d'utiliser "ConfigurationManager.GetSection" et rien d'autre?
Ron
1
Oui, vous manquez quelque chose: ConfigurationManager.GetSectionutilise le fichier app.config par défaut. Il ne se soucie pas du fichier de configuration avec lequel vous avez ouvert OpenMappedExeConfiguration.
Daniel Hilgarth
1

Merveilleuse discussion, j'ai ajouté plus de commentaires à la méthode ResetConfigMechanism pour comprendre la magie derrière l'instruction / les appels dans la méthode. Vérification de l'existence du chemin de fichier également ajoutée

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
la source
0

Daniel, si possible, essayez d'utiliser d'autres mécanismes de configuration. Nous avons parcouru cette route où nous avions différents fichiers de configuration statiques / dynamiques en fonction de l'environnement / du profil / du groupe et cela est devenu assez compliqué à la fin.

vous pouvez essayer une sorte de service Web de profil où vous ne spécifiez qu'une seule URL de service Web à partir du client et, en fonction des détails du client (vous pouvez avoir des remplacements de niveau de groupe / utilisateur), il charge toute la configuration dont il a besoin. Nous avons également utilisé MS Enterprise Library pour une partie de celui-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

Bek Raupov
la source
3
Merci pour votre réponse. Cependant, la raison en est d'éviter l'envoi de fichiers de configuration. Les détails de configuration des modules sont chargés à partir d'une base de données. Mais parce que je veux donner aux développeurs de modules le confort du mécanisme de configuration par défaut .NET, je veux incorporer ces configurations de module dans un fichier de configuration au moment de l'exécution et en faire le fichier de configuration par défaut. La raison est simple: il existe de nombreuses bibliothèques qui peuvent être configurées via app.config (par exemple WCF, EntLib, EF, ...). Si je devais introduire un autre mécanisme de configuration, la configuration serait (suite)
Daniel Hilgarth