简体   繁体   中英

.NET custom configuration section: Configuration.GetSection throws 'unable to locate assembly' exception

I have created a custom configuration section for a plugin DLL that stores the .config XML in a separate (from the main executable application) file.

Here's a sample of the custom section class:

using System;   
using System.Configuration;

namespace PluginFramework.MyConfiguration
{

public class MyConfigurationSettings : ConfigurationSection
{
    private Configuration _Config = null;

    #region ConfigurationProperties     
    /// <summary>
    /// A custom XML section for an application's configuration file.
    /// </summary>
    [ConfigurationProperty("MyProjects", IsDefaultCollection = true)]
    public MyProjectConfigurationCollection MyProjects
    {
        get { return (MyProjectConfigurationCollection) base["MyProjects"]; }
    }

    // ...
    #endregion

    /// <summary>
    /// Private Constructor used by our factory method.
    /// </summary>
    private MyConfigurationSettings () : base () {
        // Allow this section to be stored in user.app. By default this is forbidden.
        this.SectionInformation.AllowExeDefinition =
        ConfigurationAllowExeDefinition.MachineToLocalUser;
    }

    // ...

    #region Static Members  
    /// <summary>
    /// Gets the current applications &lt;MyConfigurationSettings&gt; section.
    /// </summary>
    /// <param name="ConfigLevel">
    /// The &lt;ConfigurationUserLevel&gt; that the config file
    /// is retrieved from.
    /// </param>
    /// <returns>
    /// The configuration file's &lt;MyConfigurationSettings&gt; section.
    /// </returns>
    public static MyConfigurationSettings GetSection (ConfigurationUserLevel ConfigLevel) 
    {
        string appDataPath = System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        string localDataPath = System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        System.Configuration.ExeConfigurationFileMap exeMap = new ExeConfigurationFileMap();
        exeMap.ExeConfigFilename = System.IO.Path.Combine(appDataPath, @"MyCompany\MyPluginApp\Default.config");
        exeMap.RoamingUserConfigFilename = System.IO.Path.Combine(appDataPath, @"MyCompany\MyPluginApp\Roaming.config");
        exeMap.LocalUserConfigFilename = System.IO.Path.Combine(localDataPath, @"MyCompany\MyPluginApp\Local.config");

        System.Configuration.Configuration Config = ConfigurationManager.OpenMappedExeConfiguration(exeMap,ConfigLevel);
        MyConfigurationSettings myConfigurationSettings = null;

        try {
            myConfigurationSettings = (MyConfigurationSettings)Config.GetSection("MyConfigurationSettings");
        } 
        catch (System.Exception ex) {
            // ConfigurationErrorsException caught here ...
        }
        if (myConfigurationSettings == null) {
            myConfigurationSettings = new MyConfigurationSettings();
            Config.Sections.Add("MyConfigurationSettings", myConfigurationSettings);                    }
        } 
        if(myConfigurationSettings != null) {
            myConfigurationSettings._Config = Config;
        }

        return myConfigurationSettings;
    }       
    #endregion
}
} // PluginFramework.MyConfiguration

The .config XML generated when saving 1st time looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <!-- The exception complains about the following line (assembly attributes are compliant): -->
        <section name="MyConfigurationSettings" type="PluginFramework.MyConfiguration.MyConfigurationSettings, PluginFramework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" allowDefinition="Everywhere" allowExeDefinition="MachineToLocalUser" />
    </configSections>
    <MyConfigurationSettings>
        <!-- Config properties are serialized fine according MyConfigurationSettings 
             properties marked with the ConfigurationProperty attribute ... -->
        <MyProjects>
            <MyProjectConfiguration GUID="{4307AC92-8180-4686-9322-830312ED59AB}">
                <!-- ... more complex configuration elements -->
            </MyProjectConfiguration>
        </MyProjects>
    </MyConfigurationSettings>
</configuration>

When this XML is tried to be loaded using Config.GetSection() on subsequent runs, I catch a ConfigurationErrorsException at the line marked in the XML sample, stating that the assembly MyPlugin or one of it's dependencies couldn't be located (please forgive that I'm not posting the original exception message, but I have it only in german, and doubt this text would be helpful here). The inner exception comes from System.IO while trying to load the assembly and get reflection to resolve the 'MyConfigurationSettings' class type.

To precise the situation, the code from above is placed inside a framework DLL (assembly), that in turn is referenced by the actual plugin DLL loaded from the main application.

The following UML diagram illustrates the several components' relationships: App的主要插件和框架组件

After looking around a bit about this problem, I have the feeling it's necessary to strong name (sign) the assembly exporting the MyConfigurationSettings class (ie PluginFramework ) and register it with the GAC. I didn't try this yet, and would like to avoid this step for several reasons (before knowing if it could even help and it's the only choice to solve the problem).

So here are the questions (sorry I'm placing actually 4 questions here, but they're so strongly related that it wouldn't make sense to create separate SO questions for them).

  1. Could I solve the locating failure problem by strong naming the assembly in question and registering it with the GAC?

  2. Stupidly enough the assembly the configuration management complains about, is guaranteed to be loaded (since it calls Configuration.GetSection() itself).
    Is there may be a way to register the assembly or the appropriate configuration type de-/serializers explicitly with the ConfigurationManager or Confguration class?

  3. I'm also interested in more information about Hans Passant's comment mentioning this might be a problem caused by the way the (primary) assembly is loaded from the main app. I have no control over this mechanism, and if this causes this behavior inherently I'd like to know if there's a reasonable workaround?

  4. Another idea (if anything of the above fails to show a way) is to completely manage a configuration XML format natively (using XML de-/serialization support) and from where to load and merge the configuration files. If this is the most appropriate option, can anyone give good pointers how to do this efficiently (least necessary code for managing paths' and merging)?

Update:
Since no one seems to be able to give more insight about this question(s) (the 2 answers don't really get me further), I'm changing to option from 4., doing it all manually.

I tried that as well, but I never got it to work like that. I just figured loading a .config automatically doesn't work for .dll's only for .exe's. Then I gave up and decided it would be easier to just load the .config file manually. You can see the full code here: https://github.com/GeertBellekens/Enterprise-Architect-Toolpack/blob/master/EANavigator/NavigatorSettings.cs This is the most relevant part:

public NavigatorSettings() {
     Configuration roamingConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoaming);

     // the roamingConfig now get a path such as C:\Users\<user>\AppData\Roaming\Sparx_Systems_Pty_Ltd\DefaultDomain_Path_2epjiwj3etsq5yyljkyqqi2yc4elkrkf\9,_2,_0,_921\user.config
     // which I don't like. So we move up three directories and then add a directory for the EA Navigator so that we get
     // C:\Users\<user>\AppData\Roaming\GeertBellekens\EANavigator\user.config
     string configFileName =  System.IO.Path.GetFileName(roamingConfig.FilePath);
     string configDirectory = System.IO.Directory.GetParent(roamingConfig.FilePath).Parent.Parent.Parent.FullName;

     string newConfigFilePath = configDirectory + @"\Geert Bellekens\EANavigator\" + configFileName;
     // Map the roaming configuration file. This
     // enables the application to access 
     // the configuration file using the
     // System.Configuration.Configuration class
     ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
     configFileMap.ExeConfigFilename = newConfigFilePath;       

     // Get the mapped configuration file.
     currentConfig = ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
     // merge the default settings
     this.mergeDefaultSettings();
 }

Accessing a configuration property:

public bool trackSelectedElement
{
    get {
        bool result;
        if(bool.TryParse(this.currentConfig.AppSettings.Settings["trackSelectedElement"].Value, out result)) {
            return result;
        }
        else {
            return true;
        }
    }
    set {
        this.currentConfig.AppSettings.Settings["trackSelectedElement"].Value = value.ToString();
    }
}

What you're trying to do is not supported by .NET Framework.

1st - it makes sense that your plugin.dll is configured per host application (.exe or web) that is using it (that why it's configurable)

2nd - config files support inheritance (ex: machine.config -> applicationHost.config -> web.config). That what they are designed for. Your off-the-path config would not work properly in that respect.

So, if you need custom configuration for part of app, or plugin, not following .config concepts, make standard XML file, or jsonconfig and load settings from there.

@g-makulik

Here I have a working copy of what was done in real environment and proven to work.

In the App.config file:

<configSections>
    <sectionGroup name="mySectionGroupName">
        <section name="mySectionName" type="MyNamespace.MySectionHandler,MyNamespace" />
    </sectionGroup>
</configSections>
....
<mySectionGroupName>
    <mySectionName>
        <add key="MyKey" value="MyKeyValue" />
    </mySectionName>
</mySectionGroupName>

In the class where you use config:

....
Hashtable ht = ConfigurationManager.GetSection("mySectionGroupName/mySectionName") as Hashtable; 
// when you call this, your handler will do what you want in there
string keyVal = ht["MyKey"] as String;
....

The class responsible for config handling:

public class MySectionHandler : DictionarySectionHandler 
{
    public override object Create(object parent, object context, XmlNode section) 
    {
        // here do what you want with the value of "MyKey" - "MyKeyValue"
    }
}

I hope, this helps

I am having the same problem and haven't found a fully satyisfying solution so far. Our application loads is compiled with a reference to a configuration section class defined in a dedicated assembly. The application is linked with a strongly named assembly but later when the configuration loader tries to read the configuration, fusion traces show that it tries to load a weak name of the same assembly. For some reason, .net fails to see that it is the same assembly and throws a System.IO.FileNotFound exception. On working solution in my case is to reference strong names in the configuration.

I have also noticed a strange behavior: Once an assembly is loaded by the .net "configuration loader" with a strong name, further references with a weak name actually succeed ! For some reason, the framework "remembers" that the weak name reffers to the same assembly.

Any news from the issue on your end would be interesting !

Add an event handler for AppDomain.CurrentDomain.AssemblyResolve . This should work for option 2 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM