Alterar app.config padrão em tempo de execução

130

Eu tenho o seguinte problema:
Temos um aplicativo que carrega módulos (complementos). Esses módulos podem precisar de entradas no app.config (por exemplo, configuração do WCF). Como os módulos são carregados dinamicamente, não quero ter essas entradas no arquivo app.config do meu aplicativo.
O que eu gostaria de fazer é o seguinte:

  • Crie um novo app.config na memória que incorpore as seções de configuração dos módulos
  • Diga ao meu aplicativo para usar esse novo app.config

Nota: Não quero substituir o app.config padrão!

Deve funcionar de forma transparente, de modo que, por exemplo ConfigurationManager.AppSettings use esse novo arquivo.

Durante minha avaliação desse problema, eu vim com a mesma solução que é fornecida aqui: Recarregue app.config com nunit .
Infelizmente, ele parece não fazer nada, porque ainda recebo os dados do app.config normal.

Eu usei esse código para testá-lo:

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

Ele imprime os mesmos valores duas vezes, embora combinedConfigcontenha outros valores além do app.config normal.

Daniel Hilgarth
fonte
Hospedar os módulos separadamente AppDomaincom o arquivo de configuração apropriado não é uma opção?
João Angelo
Na verdade não, porque isso resultaria em muitas chamadas entre aplicativos do Domínio, porque o aplicativo interage bastante com os módulos.
Daniel Hilgarth 27/05
Que tal reiniciar um aplicativo quando um novo módulo precisa ser carregado?
João Angelo
Isso não funciona junto com os requisitos de negócios. Além disso, não consigo substituir o app.config, porque o usuário não tem o direito de fazê-lo.
Daniel Hilgarth 27/05
Você estaria recarregando para carregar um App.config diferente, não aquele nos arquivos de programa. A invasão Reload app.config with nunitpode funcionar, não tenho certeza, se usada na entrada do aplicativo antes de qualquer configuração ser carregada.
João Angelo

Respostas:

280

O hack na pergunta vinculada funciona se for usado antes que o sistema de configuração seja usado pela primeira vez. Depois disso, não funciona mais.
O motivo:
existe uma classe ClientConfigPathsque armazena em cache os caminhos. Portanto, mesmo depois de alterar o caminho com SetData, ele não é relido, porque já existem valores em cache. A solução é removê-los também:

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

O uso é assim:

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

Se você deseja alterar o app.config usado durante todo o tempo de execução do seu aplicativo, basta colocá-lo AppConfig.Change(tempFileName)sem o uso em algum lugar no início do seu aplicativo.

Daniel Hilgarth
fonte
4
Isso é realmente, realmente excelente. Muito obrigado por publicar isto.
user981225
3
@ Daniel Isso foi incrível - trabalhei em um método de extensão para ApplicationSettingsBase, para que eu possa chamar Settings.Default.RedirectAppConfig (path). Eu daria a você +2 se eu pudesse!
21413 JMarsch
2
@ PhilWhittington: É o que estou dizendo, sim.
Daniel Hilgarth
2
por interesse, existe algum motivo para suprimir o finalizador? não há declarado?
Gusdor #
3
Além disso, o uso da reflexão para acessar campos particulares pode funcionar agora, mas pode usar um aviso de que não é suportado e pode ser interrompido em versões futuras do .NET Framework.
10

Você pode tentar usar Configuration e Add ConfigurationSection no tempo de execução

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: Aqui está a solução baseada na reflexão (embora não seja muito agradável)

Criar classe derivada 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
}

depois, através da reflexão, defina-o como campo privado em 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
fonte
Não vejo como isso me ajuda. Isso adicionará uma seção ao arquivo especificado por file_path. Isso não tornará a seção disponível para os usuários de ConfigurationManager.GetSection, porque GetSectionusa o app.config padrão.
Daniel Hilgarth 27/05
Você pode adicionar seções ao seu app.config existente. Apenas tentei isso - funciona para mim
Stecya
Cite minha pergunta: "Nota: não quero substituir o app.config padrão!"
Daniel Hilgarth 27/05
5
O que há de errado? Simples: o usuário não tem o direito de substituí-lo, porque o programa está instalado em% ProgramFiles% e o usuário não é administrador.
Daniel Hilgarth 27/05
2
@ Stecya: Obrigado pelo seu esforço. Mas, por favor, veja minha resposta para a verdadeira solução do problema.
Daniel Hilgarth 27/05
5

A solução @Daniel funciona bem. Uma solução semelhante com mais explicações está no canto c-sharp. Para completar, gostaria de compartilhar minha versão: with using, e os sinalizadores de bits abreviados.

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

Se alguém estiver interessado, aqui está um método que funciona no 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
fonte
3

A solução de Daniel parece funcionar mesmo para montagens a jusante que eu havia usado o AppDomain.SetData antes, mas não sabia como redefinir os sinalizadores de configuração internos

Convertido em C ++ / CLI para os interessados

/// <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;
}
Conta
fonte
1

Se o seu arquivo de configuração foi gravado com chave / valores em "appSettings", você pode ler outro arquivo com esse código:

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

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

Em seguida, você pode ler section.Settings como coleção de KeyValueConfigurationElement.

Ron
fonte
1
Como eu já disse, quero fazer ConfigurationManager.GetSectionler o novo arquivo que criei. Sua solução não faz isso.
Daniel Hilgarth 27/05
@ Daniel: por que? Você pode especificar qualquer arquivo em "configFilePath". Então, você só precisa saber a localização do seu novo arquivo criado. Perdi alguma coisa ? Ou você realmente precisa usar o "ConfigurationManager.GetSection" e nada mais?
27411 Ron
1
Sim, você sente falta de algo: ConfigurationManager.GetSectionusa o app.config padrão. Ele não se importa com o arquivo de configuração que você abriu OpenMappedExeConfiguration.
Daniel Hilgarth 27/05
1

Maravilhosa discussão, adicionei mais comentários ao método ResetConfigMechanism para entender a mágica por trás da declaração / chamadas no método. Também existe verificação de caminho do arquivo adicionado

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

Daniel, se possível, tente usar outros mecanismos de configuração. Passamos por essa rota em que tínhamos arquivos de configuração estáticos / dinâmicos diferentes, dependendo do ambiente / perfil / grupo, e isso ficou bastante confuso no final.

você pode experimentar algum tipo de perfil WebService no qual especifica apenas uma URL de serviço da Web do cliente e, dependendo dos detalhes do cliente (você pode ter substituições no nível de grupo / usuário), ele carrega toda a configuração necessária. Também usamos a MS Enterprise Library para alguma parte dela.

ou seja, você não implanta a configuração com o seu cliente e pode gerenciá-lo separadamente dos seus clientes

Bek Raupov
fonte
3
Obrigado pela sua resposta. No entanto, todo o motivo disso é evitar o envio de arquivos de configuração. Os detalhes de configuração dos módulos são carregados de um banco de dados. Mas como eu quero dar aos desenvolvedores de módulos o conforto do mecanismo de configuração padrão do .NET, quero incorporar essas configurações em um arquivo de configuração em tempo de execução e torná-lo o arquivo de configuração padrão. O motivo é simples: existem muitas bibliotecas que podem ser configuradas através do app.config (por exemplo, WCF, EntLib, EF, ...). Se eu iria introduzir um outro mecanismo de configuração, a configuração seria (cont.)
Daniel Hilgarth