139

I have the following problem:
We have an application that loads modules (add ons). These modules might need entries in the app.config (e.g. WCF configuration). Because the modules are loaded dynamically, I don't want to have these entries in the app.config file of my application.
What I would like to do is the following:

  • Create a new app.config in memory that incorporates the config sections from the modules
  • Tell my application to use that new app.config

Note: I do not want to overwrite the default app.config!

It should work transparently, so that for example ConfigurationManager.AppSettings uses that new file.

During my evaluation of this problem, I came up with the same solution as is provided here: Reload app.config with nunit.
Unfortunately, it doesn't seem to do anything, because I still get the data from the normal app.config.

I used this code to test it:

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

It prints the same values twices, although combinedConfig contains other values than the normal app.config.

Community
  • 1
  • 1
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • Hosting the modules in separate `AppDomain` with the appropriate configuration file is not an option? – João Angelo May 27 '11 at 10:36
  • Not really, because that would result in lots of Cross-AppDomain calls, because the application interacts quite heavily with the modules. – Daniel Hilgarth May 27 '11 at 10:37
  • How about an application restart when a new module needs to be loaded? – João Angelo May 27 '11 at 10:47
  • This doesn't work together with the business requirements. Furthermore, I can't overwrite the app.config, because the user doesn't have the right to do so. – Daniel Hilgarth May 27 '11 at 10:48
  • You would be reloading to load a different App.config, not the one in program files. The hack in `Reload app.config with nunit` could work, not sure, if used on application entry before any configuration is loaded. – João Angelo May 27 '11 at 10:51
  • This hack doesn't work with restarting the application, because it is supposed to change the app.config of the *current* app domain. Basically, what this hack is supposed to do is exactly, what I want. But it is not working :( – Daniel Hilgarth May 27 '11 at 10:56
  • Let me try to explain it better, you wouldn't be applying the hack before restarting, you would be restarting to be able to apply the hack before any application configuration is loaded, that is, the hack would be in Main, and you could signal it maybe through a command line argument. – João Angelo May 27 '11 at 10:59
  • I see. I guess that would work. But restarting isn't an option, unfortunately... – Daniel Hilgarth May 27 '11 at 11:01
  • @João: I found a solution. Please see my answer, if you are interested. – Daniel Hilgarth May 27 '11 at 12:11

8 Answers8

296

The hack in the linked question works if it is used before the configuration system is used the first time. After that, it doesn't work any more.
The reason:
There exists a class ClientConfigPaths that caches the paths. So, even after changing the path with SetData, it is not re-read, because there already exist cached values. The solution is to remove these, too:

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

Usage is like this:

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

If you want to change the used app.config for the whole runtime of your application, simply put AppConfig.Change(tempFileName) without the using somewhere at the start of your application.

Alex Nolasco
  • 18,750
  • 9
  • 86
  • 81
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • @Daniel this is awesome! Thank you! No need to load another AppDomain anymore! – bigfoot Nov 23 '12 at 23:58
  • 3
    @Daniel That was awesome -- I worked it into an exentension method for ApplicationSettingsBase, so that I can call Settings.Default.RedirectAppConfig(path). I'd give you +2 if I could! – JMarsch Feb 14 '13 at 17:50
  • Hi Daniel - thanks for this; it's awesome. Is there any way to load app.config XML straight in to the AppDomain? I intend to store different configurations in a database and load one at run time. I don't really want to write it down to a file first. Thanks, Phil Whittington – Phil Whittington Aug 06 '13 at 10:54
  • 1
    @PhilWhittington: If we are talking about the default configuration that is accessed by `Settings.Default` or `ConfigurationManager.AppSettings`, then, no, there is no way. The scenario I solved with this approach is actually similar to yours: The contents of my new app.config exist in memory and they are only written to a file because of the internal workings of the configuration system. – Daniel Hilgarth Aug 06 '13 at 10:57
  • OK - are you saying the only way to feed in settings to the config system is to provide a file path? If that's the case I'll write down to a temp location the data I get from the DB. Thanks. – Phil Whittington Aug 06 '13 at 12:32
  • 2
    @PhilWhittington: That's what I am saying, yes. – Daniel Hilgarth Aug 06 '13 at 12:33
  • I added Properties.Settings.Default.Reset() my own code to force a reload of these settings. This after calling ChangeAppConfig(string) – lvmeijer Aug 29 '13 at 21:33
  • 2
    out of interest, is there any reason to suppress the finalizer is there is no finalizer declared? – Gusdor Apr 01 '14 at 10:19
  • @Gusdor: That's just the standard pattern of implementing Dispose. – Daniel Hilgarth Sep 22 '14 at 12:22
  • MS code analyzer gives a CA1063 and CA1063 on line "public abstract void Dispose();" – edelwater Nov 18 '14 at 14:18
  • 2
    @DanielHilgarth The standard pattern of implementing Dispose calls `GC.SuppressFinalize` involves a `Dispose()` non-virtual method, and a `Dispose(bool)` virtual method. You aren't actually using the standard Dispose pattern. Anyway, the point of the `GC.SuppressFinalize` even if the base class has no finalizer is because a derived class might, but unless you actually want to support other classes deriving from `AppConfig`, there is no benefit to it. In fact, I would give `AppConfig` a private constructor precisely so that other classes *cannot* use it as a base. –  Mar 17 '15 at 09:35
  • 3
    That aside, using reflection to access private fields may work now, but it could use a warning that it isn't supported and may break in future versions of the .NET Framework. –  Mar 17 '15 at 09:36
  • I used this to load the web.config of my MVC app in an externally launched EXE and it worked perfectly. – Bron Davies Jan 21 '16 at 06:16
  • Is there a reason you are setting the config file path to the old value ? AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", oldConfig); – M0-3E Feb 09 '16 at 21:50
  • @Moez: It's in the dispose, see the usage sample. – Daniel Hilgarth Feb 10 '16 at 01:24
  • Fusionlog reports using the old app config even though ConfigurationManager says path is to the new app config: LOG: Using application configuration file: – magnusarinell Apr 06 '16 at 13:09
  • @Drutten: Quite possible that this message is printed before the code to change the app.config runs. Did you actually verify the values you get inside your application to see from which app.config they are coming? – Daniel Hilgarth Apr 06 '16 at 13:10
  • @DanielHilgarth Hmm, System.IO.FileNotFountException.FusionLog says === Pre-bind state information === LOG: DisplayName = System.Web.Mvc, Version=3.0.0.0, ... LOG: Appbase = file:///C:/Program Files (x86)/Microsoft Visual Studio 14.0/Common7/IDE/ Calling assembly : ... === LOG: This bind starts in LoadFrom load context. WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load(). LOG: Using application configuration file: C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\QTAgent32_40.exe.Config – magnusarinell Apr 06 '16 at 13:19
  • @Drutten Again: Did you verify the values? I never checked FusionLog, so I don't know if it misbehaves in this scenario – Daniel Hilgarth Apr 06 '16 at 13:21
  • @DanielHilgarth Yes I verified the values, they come from the new app config. – magnusarinell Apr 06 '16 at 13:24
  • @Drutten: So... what's your problem/question? – Daniel Hilgarth Apr 06 '16 at 13:25
  • 1
    @DanielHilgarth Im having problems with bindingRedirect not read from the new app config but it seems that it is only done during application startup. Now I fixed it using `AppDomain.CurrentDomain.AssemblyResolve` instead. Sorry for the buzz. – magnusarinell Apr 06 '16 at 13:37
  • 1
    @Drutten The reason why this is happening is very simple: That assembly is being resolved before the code is executed that changes the app.config – Daniel Hilgarth Apr 06 '16 at 13:39
  • 2
    All `typeof(ConfigurationManager).GetField` in `ResetConfigMechanism` return null... what could be wrong? – Homer1982 Apr 27 '16 at 15:11
  • 2
    Regarding my previous comment: adding `using System.Configuration` makes the reset mechanism to work properly. Unfortunately the new app.config is not used and application still refer to the original one. – Homer1982 Apr 28 '16 at 07:09
  • 2
    The solution to my previous comment is to create a static class that allows to call `AppConfig.Change(tempFileName)` before instantiating the class that requires the modified app.config. – Homer1982 Apr 28 '16 at 17:01
  • Note that path must be absolute. If it's a relative path, the data getting loaded is from the defaults in the compile-time generated settings class. – erict Nov 30 '16 at 00:35
  • What's the inheritance for? Wouldn't a private constructor suffice? – BartoszKP Jan 19 '17 at 16:31
  • Does not work well for me, I have log4net, it still looks for default config file. – OKEEngine Apr 10 '19 at 06:35
  • 1
    @JerryLiang It's very well possible that log4net caches the configuration values and doesn't re-read them using the `ConfigurationManager` class. Try creating a new instance after you used this code and see if this new instance picks up the new settings – Daniel Hilgarth Apr 10 '19 at 08:57
11

You can try to use Configuration and Add ConfigurationSection on 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: Here is solution based on reflection (not very nice though)

Create class derived from 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
}

then via reflection set it to private field 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
Stecya
  • 22,896
  • 10
  • 72
  • 102
  • I don't see how this helps me. This will add a section to the file specified by `file_path`. This will not make the section available to users of `ConfigurationManager.GetSection`, because `GetSection` uses the default app.config. – Daniel Hilgarth May 27 '11 at 10:13
  • You can add Sections to your existing app.config. Just tried this - works for me – Stecya May 27 '11 at 10:24
  • Quote from my question: *"Note: I do not want to overwrite the default app.config!"* – Daniel Hilgarth May 27 '11 at 10:26
  • What's wrong with overwriting app.config? You always can remove those unnecessary sections from app.config when your are done working with add-ons – Stecya May 27 '11 at 10:31
  • 5
    What's wrong? Simple: The user has no right to overwrite it, because the program is installed in %ProgramFiles% and the user is no administrator. – Daniel Hilgarth May 27 '11 at 10:33
  • 2
    @Stecya: Thanks for your effort. But please see my answer for the real solution to the problem. – Daniel Hilgarth May 27 '11 at 11:35
5

@Daniel solution works OK. A similar solution with more explanation is in c-sharp corner. For completeness I'd like to share my version: with using, and the bit flags abbreviated.

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
  • 4,619
  • 7
  • 49
  • 81
5

If anybody is interested, here is a method that works on 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
  • 611
  • 12
  • 36
4

Wonderful discussion, I've adding more comments to ResetConfigMechanism method to understand the magic behind the statement/calls in the method. Also added file path exist check

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
  • 5,132
  • 2
  • 37
  • 40
3

Daniel's solution seems to work even for downstream assemblies I had used AppDomain.SetData before, but was unaware of how to reset the internal configuration flags

Converted to C++/CLI for those interested

/// <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;
}
Bill
  • 79
  • 5
1

If your config file is just written with key/values in "appSettings", then you can read another file with such 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");

Then you can read section.Settings as collection of KeyValueConfigurationElement.

Stecya
  • 22,896
  • 10
  • 72
  • 102
Ron
  • 11
  • 1
  • 1
    As I already said, I want to make `ConfigurationManager.GetSection` read the new file I created. Your solution doesn't do that. – Daniel Hilgarth May 27 '11 at 10:42
  • @Daniel : why ? You can specify any file in "configFilePath". So you just need to know the location of your new created file. Did I miss something ? Or you really need to use "ConfigurationManager.GetSection" and nothing else ? – Ron May 27 '11 at 10:43
  • 1
    Yes you do miss something: `ConfigurationManager.GetSection` uses the default app.config. It doesn't care about the config file you opened with `OpenMappedExeConfiguration`. – Daniel Hilgarth May 27 '11 at 10:45
0

Daniel, if possible try to use other config mechanisms. We have been through this route where we had different static/dynamic config files depending on environment/profile/group and it became quite messy at the end.

you could try out some sort of Profile WebService where you only specify one Web Service URL from the client and depending on Client's details (you might have Group/User level overrides), it loads up all the config it needs. We have also used MS Enterprise Library for some part of it.

that was you dont deploy config with your client and you can manage it separately from your clients

Bek Raupov
  • 3,782
  • 3
  • 24
  • 42
  • 3
    Thanks for your answer. However, the whole reason of this is to avoid shipping config files. The configuration details for the modules are loaded from a database. But because I want to give module developers the comfort of the default .NET configuration mechanism, I want to incorporate those module configurations into one config file at runtime and make this the default config file. The reason is simple: There exist a lot of libraries that can be configured through app.config (e.g. WCF, EntLib, EF, ...). If I would introduce another config mechanism, the configuration would (cont.) – Daniel Hilgarth May 27 '11 at 10:22