Cambiar la aplicación predeterminada.config en tiempo de ejecución

130

Tengo el siguiente problema:
tenemos una aplicación que carga módulos (complementos). Es posible que estos módulos necesiten entradas en app.config (por ejemplo, configuración de WCF). Debido a que los módulos se cargan dinámicamente, no quiero tener estas entradas en el archivo app.config de mi aplicación.
Lo que me gustaría hacer es lo siguiente:

  • Cree una nueva aplicación.config en memoria que incorpore las secciones de configuración de los módulos
  • Dile a mi aplicación que use esa nueva aplicación.

Nota: ¡No quiero sobrescribir la aplicación predeterminada.config!

Debería funcionar de manera transparente, de modo que, por ejemplo ConfigurationManager.AppSettings use ese nuevo archivo.

Durante mi evaluación de este problema, se me ocurrió la misma solución que se proporciona aquí: Recargar app.config con nunit .
Desafortunadamente, no parece hacer nada, porque todavía obtengo los datos de la app.config normal.

Usé este código para probarlo:

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

Imprime los mismos valores dos veces, aunque combinedConfigcontiene otros valores distintos de la aplicación.config normal.

Daniel Hilgarth
fuente
¿Alojar los módulos por separado AppDomaincon el archivo de configuración apropiado no es una opción?
João Angelo
En realidad no, porque eso daría lugar a muchas llamadas Cross-AppDomain, porque la aplicación interactúa bastante con los módulos.
Daniel Hilgarth
¿Qué tal un reinicio de la aplicación cuando se necesita cargar un nuevo módulo?
João Angelo
Esto no funciona junto con los requisitos comerciales. Además, no puedo sobrescribir la aplicación.config, porque el usuario no tiene derecho a hacerlo.
Daniel Hilgarth
Estaría recargando para cargar una App.config diferente, no la que está en los archivos de programa. El pirateo Reload app.config with nunitpodría funcionar, no estoy seguro, si se usa en la entrada de la aplicación antes de cargar cualquier configuración.
João Angelo

Respuestas:

280

El truco en la pregunta vinculada funciona si se usa antes de que el sistema de configuración se use por primera vez. Después de eso, ya no funciona.
La razón:
existe una clase ClientConfigPathsque almacena en caché las rutas. Entonces, incluso después de cambiar la ruta con SetData, no se vuelve a leer, porque ya existen valores en caché. La solución es eliminar estos también:

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

El uso es así:

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

Si desea cambiar el archivo app.config usado durante todo el tiempo de ejecución de su aplicación, simplemente colóquelo AppConfig.Change(tempFileName)sin usarlo en algún lugar al inicio de su aplicación.

Daniel Hilgarth
fuente
44
Esto es realmente, realmente excelente. Muchas gracias por publicar esto.
user981225
3
@Daniel Eso fue increíble: lo trabajé en un método de exentensión para ApplicationSettingsBase, para poder llamar a Settings.Default.RedirectAppConfig (ruta). ¡Te daría +2 si pudiera!
JMarsch
2
@PhilWhittington: Eso es lo que estoy diciendo, sí.
Daniel Hilgarth
2
fuera de interés, ¿hay alguna razón para suprimir el finalizador si no hay un finalizador declarado?
Gusdor 01 de
3
Aparte de eso, usar la reflexión para acceder a campos privados puede funcionar ahora, pero podría usar una advertencia de que no es compatible y puede romperse en futuras versiones de .NET Framework.
10

Puede intentar usar Configuración y Agregar Sección de Configuración en tiempo de ejecución

Configuration applicationConfiguration = ConfigurationManager.OpenMappedExeConfiguration(
                        new ExeConfigurationFileMap(){ExeConfigFilename = path_to_your_config,
                        ConfigurationUserLevel.None
                        );

applicationConfiguration.Sections.Add("section",new YourSection())
applicationConfiguration.Save(ConfigurationSaveMode.Full,true);

EDITAR: Aquí hay una solución basada en la reflexión (aunque no es muy agradable)

Crear clase 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
}

luego, a través de la reflexión, configúrelo en un campo privado en 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
fuente
No veo cómo esto me ayuda. Esto agregará una sección al archivo especificado por file_path. Esto no hará que la sección esté disponible para los usuarios de ConfigurationManager.GetSection, porque GetSectionusa la aplicación predeterminada.
Daniel Hilgarth
Puede agregar secciones a su app.config existente. Acabo de probar esto - funciona para mí
Stecya
Cita de mi pregunta: "Nota: ¡No quiero sobrescribir la aplicación predeterminada.config!"
Daniel Hilgarth
55
Que pasa Simple: el usuario no tiene derecho a sobrescribirlo, porque el programa está instalado en% ProgramFiles% y el usuario no es administrador.
Daniel Hilgarth
2
@ Stecya: Gracias por tu esfuerzo. Pero por favor vea mi respuesta para la solución real al problema.
Daniel Hilgarth
5

La solución @Daniel funciona bien. Una solución similar con más explicación está en la esquina c-sharp. Para completar, me gustaría compartir mi versión: con using, y las banderas de bits abreviadas.

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

Si alguien está interesado, aquí hay un método que funciona en 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
fuente
3

La solución de Daniel parece funcionar incluso para los ensambles posteriores que usé anteriormente AppDomain.SetData, pero no sabía cómo restablecer los indicadores de configuración interna

Convertido a C ++ / CLI para aquellos interesados

/// <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;
}
Cuenta
fuente
1

Si su archivo de configuración solo está escrito con clave / valores en "appSettings", puede leer otro archivo con dicho 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");

Luego puede leer section.Settings como colección de KeyValueConfigurationElement.

Ron
fuente
1
Como ya dije, quiero hacer ConfigurationManager.GetSectionleer el nuevo archivo que creé. Tu solución no hace eso.
Daniel Hilgarth
@Daniel: ¿por qué? Puede especificar cualquier archivo en "configFilePath". Entonces solo necesita saber la ubicación de su nuevo archivo creado. Me he perdido algo ? ¿O realmente necesita usar "ConfigurationManager.GetSection" y nada más?
Ron
1
Sí, te pierdes algo: ConfigurationManager.GetSectionutiliza la aplicación predeterminada.config. No le importa el archivo de configuración con el que abrió OpenMappedExeConfiguration.
Daniel Hilgarth
1

Maravillosa discusión, he agregado más comentarios al método ResetConfigMechanism para comprender la magia detrás de la declaración / llamadas en el método. También existe verificación de ruta de archivo agregado

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

Daniel, si es posible, intenta usar otros mecanismos de configuración. Hemos pasado por esta ruta donde teníamos diferentes archivos de configuración estáticos / dinámicos dependiendo del entorno / perfil / grupo y al final se volvió bastante desordenado.

podría probar algún tipo de servicio web de perfil en el que solo especifique una URL de servicio web del cliente y, según los detalles del cliente (puede tener anulaciones de nivel de grupo / usuario), carga toda la configuración que necesita. También hemos usado MS Enterprise Library para alguna parte de ella.

es decir, no implementa la configuración con su cliente y puede administrarla por separado de sus clientes

Bek Raupov
fuente
3
Gracias por tu respuesta. Sin embargo, todo el motivo es para evitar el envío de archivos de configuración. Los detalles de configuración de los módulos se cargan desde una base de datos. Pero debido a que quiero darles a los desarrolladores de módulos la comodidad del mecanismo de configuración .NET predeterminado, quiero incorporar esas configuraciones de módulo en un archivo de configuración en tiempo de ejecución y convertirlo en el archivo de configuración predeterminado. La razón es simple: existen muchas bibliotecas que se pueden configurar a través de app.config (por ejemplo, WCF, EntLib, EF, ...). Si introdujera otro mecanismo de configuración, la configuración sería (cont.)
Daniel Hilgarth