Question

I'm trying to write a very simple custom configuration section for a .NET4 application. My goal is this:

<configuration>
  <configSections>
    <section name="myServices" type="My.ConfigSection, My.Assembly" />
  </configSections>
  <myServices>
    <add name="First" />
    <add name="Second" />
  </myServices>
</configuration>

However, I keep getting a ConfigurationErrorsException: 'Unrecognized element 'add'' when I call ConfigurationManager.GetSection("myServices"). I've been staring at it for a while now but haven't figured out yet what I'm doing wrong. Below is my code. It's three classes: ConfigSection, MyServiceSettingsCollection and MyServiceSettings.

First the class that represents the entire config section. It has a nameless default collection of type MyServiceSettingsCollection. The IsDefaultCollection property should allow me to 'add' directly to my collection from the root element.

public sealed class ConfigSection : ConfigurationSection
{
  private static readonly ConfigurationProperty _propMyServices;

  private static readonly ConfigurationPropertyCollection _properties;

  public static ConfigSection Instance { get { return _instance; } }

  static ConfigSection()
  {
    _propMyServices = new ConfigurationProperty(
          null, typeof(MyServiceSettingsCollection), null,
          ConfigurationPropertyOptions.IsDefaultCollection);
    _properties = new ConfigurationPropertyCollection { _propMyServices };
  }

  [ConfigurationProperty("", IsDefaultCollection = true)]
  public MyServiceSettingsCollection MyServices
  {
    get { return (MyServiceSettingsCollection) base[_propMyServices]; }
    set { base[_propMyServices] = value; }
  }

  protected override ConfigurationPropertyCollection Properties
  { get { return _properties; } }
}

Next, the collection class itself. It is of type AddRemoveClearMap.

[ConfigurationCollection(typeof(MyServiceSettings),
    CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap)]
public sealed class MyServiceSettingsCollection : ConfigurationElementCollection
{
  public MyServiceSettings this[int index]
  {
    get { return (MyServiceSettings) BaseGet(index); }
    set
    {
      if (BaseGet(index) != null) { BaseRemoveAt(index); }
      BaseAdd(index, value);
    }
  }

  public new MyServiceSettings this[string key]
  {
    get { return (MyServiceSettings) BaseGet(key); }
  }

  protected override ConfigurationElement CreateNewElement()
  {
    return new MyServiceSettings();
  }

  protected override object GetElementKey(ConfigurationElement element)
  {
    return ((MyServiceSettings) element).Key;
  }
}

And finally a class for the elements in the collection. For now, this class has one property but there will be more later (which prevents me from using NameValueSectionHandler).

public class MyServiceSettings : ConfigurationElement
{
  private static readonly ConfigurationProperty _propName;

  private static readonly ConfigurationPropertyCollection properties;

  static MyServiceSettings()
  {
    _propName = new ConfigurationProperty("name", typeof(string), null, null,
                                          new StringValidator(1),
                                          ConfigurationPropertyOptions.IsRequired |
                                          ConfigurationPropertyOptions.IsKey);
    properties = new ConfigurationPropertyCollection { _propName };
  }

  [ConfigurationProperty("name", DefaultValue = "",
        Options = ConfigurationPropertyOptions.IsRequired |
                  ConfigurationPropertyOptions.IsKey)]
  public string Name
  {
      get { return (string) base[_propKey]; }
      set { base[_propKey] = value; }
  }

  protected override ConfigurationPropertyCollection Properties
  { get { return properties; } }
}
Was it helpful?

Solution

Ok, I found the seemingly random fix. Instead of this:

[ConfigurationProperty("", IsDefaultCollection = true)]
public ProvisiorServiceSettingsCollection ProvisiorServices
{ ... }

you should use:

[ConfigurationProperty("", Options = ConfigurationPropertyOptions.IsDefaultCollection)]
public ProvisiorServiceSettingsCollection ProvisiorServices
{ ... }

No idea what the difference is between the two. To me, they look strikingly similar... or at least, there is no suggestion anywhere why one is preferred over the other.

OTHER TIPS

Since I spent a good amount of time on this, thought I'd add a real world example I just implemented in this commit: https://github.com/rhythmagency/formulate/commit/4d2a95e1a82eb6b3500ab0869b8f8b15bd3deaa9

Here was my goal for my web.config (which I was able to achieve):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="formulateConfiguration">
      <section name="templates" type="formulate.app.Configuration.TemplatesConfigSection, formulate.app" requirePermission="false"/>
    </sectionGroup>
  </configSections>
  <formulateConfiguration>
    <templates>
      <template name="Responsive" path="~/Views/Formulate/Responsive.Bootstrap.Angular.cshtml" />
    </templates>
  </formulateConfiguration>
</configuration>

This is the class for the highest-level "templates" configuration section:

namespace formulate.app.Configuration
{

    // Namespaces.
    using System.Configuration;


    /// <summary>
    /// A configuration section for Formulate templates.
    /// </summary>
    public class TemplatesConfigSection : ConfigurationSection
    {

        #region Properties

        /// <summary>
        /// The templates in this configuration section.
        /// </summary>
        [ConfigurationProperty("", IsDefaultCollection = true)]
        [ConfigurationCollection(typeof(TemplateCollection), AddItemName = "template")]
        public TemplateCollection Templates
        {
            get
            {
                return base[""] as TemplateCollection;
            }
        }

        #endregion

    }

}

Here's the next level down, the collection class:

namespace formulate.app.Configuration
{

    // Namespaces.
    using System.Configuration;


    /// <summary>
    /// A collection of templates from the configuration.
    /// </summary>
    [ConfigurationCollection(typeof(TemplateElement))]
    public class TemplateCollection : ConfigurationElementCollection
    {

        #region Methods

        /// <summary>
        /// Creates a new template element.
        /// </summary>
        /// <returns>The template element.</returns>
        protected override ConfigurationElement CreateNewElement()
        {
            return new TemplateElement();
        }


        /// <summary>
        /// Gets the key for an element.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>The key.</returns>
        protected override object GetElementKey(ConfigurationElement element)
        {
            return (element as TemplateElement).Name;
        }

        #endregion

    }

}

And here's the deepest level class (the individual templates):

namespace formulate.app.Configuration
{

    //  Namespaces.
    using System.Configuration;


    /// <summary>
    /// A "template" configuration element.
    /// </summary>
    public class TemplateElement : ConfigurationElement
    {

        #region Constants

        private const string DefaultPath = "~/*Replace Me*.cshtml";

        #endregion


        #region Properties

        /// <summary>
        /// The name of the template.
        /// </summary>
        [ConfigurationProperty("name", IsRequired = true)]
        public string Name
        {
            get
            {
                return base["name"] as string;
            }
            set
            {
                this["name"] = value;
            }
        }


        /// <summary>
        /// The path to this template.
        /// </summary>
        /// <remarks>
        /// Should start with "~" and end with ".cshtml".
        /// </remarks>
        [ConfigurationProperty("path", IsRequired = true, DefaultValue = DefaultPath)]
        [RegexStringValidator(@"^~.*\.[cC][sS][hH][tT][mM][lL]$")]
        public string Path
        {
            get
            {
                var result = base["path"] as string;
                return result == DefaultPath ? null : result;
            }
            set
            {
                this["path"] = value;
            }
        }

        #endregion

    }

}

The important bit for me was to have the empty string in the ConfigurationPropertyAttribute and setting IsDefaultCollection to true. By the way, I put my config in an external file that looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<templates>
    <template name="Responsive" path="~/Views/Formulate/Responsive.Bootstrap.Angular.cshtml" />
</templates>

And in that case, my web.config looks like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionGroup name="formulateConfiguration">
      <section name="templates" type="formulate.app.Configuration.TemplatesConfigSection, formulate.app" requirePermission="false"/>
    </sectionGroup>
  </configSections>
  <formulateConfiguration>
    <templates configSource="config\Formulate\templates.config"/>
  </formulateConfiguration>
</configuration>

Figured I'd mention that in case somebody else is trying to add it to an external file (it's somewhat non-intuitive that the root-level item in the external file is the same as the externalized element from the web.config).

it seems you are missing something similar to this

[ConfigurationProperty("urls", IsDefaultCollection = false)]
    [ConfigurationCollection(typeof(UrlsCollection),
        AddItemName = "add",
        ClearItemsName = "clear",
        RemoveItemName = "remove")]

for more information see http://msdn.microsoft.com/en-us/library/system.configuration.configurationcollectionattribute.aspx

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top