Does .net custom config section elements have to have keys? - c#

Concider the following config section:
<PluginSection>
<Plugins>
<Plugin name="Plug1">
<add MessageType="1" MessageSubType="1" Ringtone="chime.wav" Vibrate="1000:0:1"/>
<add MessageType="1" MessageSubType="2" Ringtone="chime2.wav" Vibrate="1000:0:1"/>
</Plugin>
<Plugin name="Plug2">
<add MessageType="1" MessageSubType="1" Ringtone="chime.wav"/>
<add MessageType="1" MessageSubType="2" Ringtone="chime2.wav"/>
<add MessageType="2" Ringtone="chime3.wav"/>
</Plugin>
</Plugins>
</PluginSection>
I've implemented the parsing of this as a c# IConfigSectionHandler. Now I understand that this method is deprecated, and that I should use ConfigurationSection, ConfigurationElements and ConfigurationElementCollections. I have no problem understanding the examples of this on the web (msdn and SO). But all the examples I've seen so far have used one of the properties as a key. My elements are unique as a combination of the Plugin name, MessageType and MessageSubType. The MessageSubType is also optional. Could i parse a config section that looks like this using the recommended classes, or do I have to alter my configuration to fit the regime of the ConfigurationClasses by e.g. adding a "dummy" key?

No.
But to avoid keys you need to do more work.
The concrete type KeyValueConfigurationCollection allows easy creation of a configuration collection by setting some properties.
To create a more customised collection requires either extending the abstract ConfigurationElementCollection (but this will still be based on the add/remove/clear model as used by <appSettings>. but allowing the element names to be configured but this is still based on ahaving a key value for each member of the collection (this is determined by an override of GetElementKey so does not need to be directly included in the XML).
Alternatively you can create you own, completely custom configuration collection by extending ConfigurationElement, but you'll need to do all the work of parsing the child elements yourself (remember ConfigurationElementCollection is itself a child class of ConfigurationElement).

So based on the excelent answer from Richard, i decided to rewrite my config parsing. Heres the result:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace PluginConfiguration
{
public class PluginConfigSection : ConfigurationSection
{
[ConfigurationProperty("Plugins", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(PluginCollection), AddItemName = "Plugin")]
public PluginCollection Plugins
{
get
{
PluginCollection coll = (PluginCollection)base["Plugins"];
return coll;
}
}
}
public class PluginCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new MessageMappingElementCollection();
}
protected override object GetElementKey(ConfigurationElement element)
{
MessageMappingElementCollection coll = element as MessageMappingElementCollection;
return coll.Name;
}
}
public class MessageMappingElementCollection : ConfigurationElementCollection
{
[ConfigurationProperty("name", IsRequired = true, IsKey = true)]
public string Name
{
get { return this["name"].ToString(); }
set { this["name"] = value; }
}
protected override ConfigurationElement CreateNewElement()
{
return new MessageMappingElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
MessageMappingElement msgElement = element as MessageMappingElement;
string ret = String.Format("{0}|{1}", msgElement.MessageType, msgElement.MessageSubType);
return ret;
}
}
public sealed class MessageMappingElement : ConfigurationElement
{
public MessageMappingElement()
{
MessageType = 0;
MessageSubType = 0;
RingTone = "";
Description = "";
Vibrate = "";
}
[ConfigurationProperty("MessageType", IsRequired = true)]
public int MessageType
{
get { return int.Parse(this["MessageType"].ToString()); }
set { this["MessageType"] = value; }
}
[ConfigurationProperty("MessageSubType", IsRequired = false)]
public int MessageSubType
{
get { return int.Parse(this["MessageSubType"].ToString()); }
set { this["MessageSubType"] = value; }
}
[ConfigurationProperty("RingTone", IsRequired = false)]
public string RingTone
{
get { return this["RingTone"].ToString(); }
set { this["RingTone"] = value; }
}
[ConfigurationProperty("Description", IsRequired = false)]
public string Description
{
get { return this["Description"].ToString(); }
set { this["Description"] = value; }
}
[ConfigurationProperty("Vibrate", IsRequired = false)]
public string Vibrate
{
get { return this["Vibrate"].ToString(); }
set { this["Vibrate"] = value; }
}
}
}

Related

Custom configuration unrecognized attribute for remove element

I have a custom configuration section that is throwing the following error:
Unrecognized attribute 'path'. Note that attribute names are case-sensitive.
The rest of the section does not have any problems loading. But once I add a <remove> element to the collection in question, the configuration fails to load.
Here's the code for the problematic section:
[ConfigurationCollection(typeof(PathElement), CollectionType = ConfigurationElementCollectionType.AddRemoveClearMap)]
public class PathElementCollection : ConfigurationElementCollection
{
public PathElementCollection()
{
// Load the default values...
BaseAdd(new PathElement() { Path = "/content/" });
}
protected override ConfigurationElement CreateNewElement()
{
return new PathElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((PathElement)element).Path;
}
}
public class PathElement : ConfigurationElement
{
public const string PathPropertyName = "path";
[ConfigurationProperty(PathPropertyName, IsRequired = true)]
public string Path
{
get { return (string)this[PathPropertyName]; }
set { this[PathPropertyName] = value; }
}
}
How I'm loading the section in the module that uses it:
CustomSection configSection = (CustomSection)ConfigurationManager.GetSection(CustomSection.SectionName);
Example configuration:
<ignoredPaths>
<remove path="/content/" />
<add path="/test/" />
</ignoredPaths>
Does anyone have any ideas as to what I'm doing wrong?
Your classes should look like this
[XmlRoot("ignoredPaths")]
public class IgnoredPaths
{
PathElementCollection paths {get;set;}
}
[XmlInclude(typeof(Remove))]
[XmlInclude(typeof(Add))]
public class PathElementCollection
{
[XmlAttribute("path")]
public string path {get;set;}
}
[XmlRoot("remove")]
public class Remove : PathElementCollection
{
}
[XmlRoot("add")]
public class Add : PathElementCollection
{
}​

App.config: custom configuration nested sections

I have found a great example for custom configuration handler and tried to use it for my own implementation.
I have set up App.config like this:
<configSections>
<section name="DocumentationSettings" type="ConfigHandler.DocumentationSettings,Settings"/>
</configSections>
<DocumentationSettings>
<DocumentationSections>
<DocumentationSection Id="AAA">
<SectionDescription Value="SectionDescriptionAAA"/>
</DocumentationSection>
<DocumentationSection Id="BBB">
<SectionDescription Value="SectionDescriptionBBB"/>
</DocumentationSection>
<DocumentationSection Id="CCC">
<SectionDescription Value="SectionDescriptionCCC"/>
</DocumentationSection>
</DocumentationSections>
</DocumentationSettings>
I use this code to access my custom configuration:
DocumentationSettings documentationSettings = ConfigurationManager.GetSection("DocumentationSettings") as DocumentationSettings;
foreach (DocumentationSectionConfigElement section in (documentationSettings.DocumentationSections.Sections))
{
Console.WriteLine(section.Id);
Console.WriteLine(section.SectionDescription.Properties.Value);
}
First 'Console.WriteLine' works perfect.
So I have the following problems and implementation related questions:
I get error on second 'Console.WriteLine', error: Unrecognized attribute 'Value'. I have put "public SectionDescription SectionDescription" since "DocumentationSectionConfigElement" Class exposes property access, but I might be wrong, I tried first to put it into "DocumentationSectionCollection" but I don't know how to implement it there and for me it seems that "DocumentationSectionCollection" only implements "Collection" logic.
I want to access "fields" either both like this:
section.Id
section.SectionDescription.Value
or like this:
section.Properties.Id
section.SectionDescription.Properties.Value
I see that "Collection" allows to use these properties directly using indexer methods like this:
public DocumentationSectionConfigElement this[int index]
But I can't implement indexer method on "SectionDescription" class, because it is a single section not a collection, so this "Properties" name persists when I access fields.
What do I need to add to be able to use LINQ on these configuration objects?
I mean like this:
(documentationSettings.DocumentationSections.Sections).Select(x => x.Id)
Are there any really good examples of complex XML structure configuration handler? From those which I found there were mostly simple structures like these:
but no any example of complex structure like this:
<section>
<subSections>
<subSection name="111">
<Description Value="AAA"></Description>
<Headers>
<Header type="Main">
<Properties>
<Property name="Property1"/>
<Property name="Property2"/>
<Property name="Property3"/>
</Properties>
</Header>
</Headers>
</subSection>
</subSections>
</section>
which is more close to real scenarios when you need to better organize application configuration.
What is the point of "sectionGroup" tag in "configSections" if "Custom Configuration Handler" works and it is enough with one "section" registered ?
Why all this stuff so complex? I have spent so much time these "custom configuration" things that shouldn't be so complex I believe. Aren't there any Visual Studio Add-ins that handle this stuff and generate code based on XML configuration structure?
Here is my Configuration Handler implementation:
public class DocumentationSettings : ConfigurationSection
{
[ConfigurationProperty("DocumentationSections")]
public DocumentationSections DocumentationSections
{
get { return (DocumentationSections)base["DocumentationSections"]; }
}
}
public class DocumentationSections : ConfigurationElement
{
[ConfigurationProperty("", IsDefaultCollection = true)]
public DocumentationSectionCollection Sections
{
get { return (DocumentationSectionCollection)base[""]; }
}
}
public class SectionDescription : ConfigurationElement
{
[ConfigurationProperty("SectionDescription")]
public new SectionDescriptionConfigElement Properties
{
get { return (SectionDescriptionConfigElement)base["SectionDescription"]; }
}
}
public class DocumentationSectionCollection : ConfigurationElementCollection
{
public DocumentationSectionCollection()
{
DocumentationSectionConfigElement details = (DocumentationSectionConfigElement)CreateNewElement();
if (details.Id != "")
{
Add(details);
}
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
protected override ConfigurationElement CreateNewElement()
{
return new DocumentationSectionConfigElement();
}
protected override Object GetElementKey(ConfigurationElement element)
{
return ((DocumentationSectionConfigElement)element).Id;
}
public DocumentationSectionConfigElement this[int index]
{
get { return (DocumentationSectionConfigElement)BaseGet(index); }
set
{
if (BaseGet(index) != null)
{
BaseRemoveAt(index);
}
BaseAdd(index, value);
}
}
new public DocumentationSectionConfigElement this[string name]
{
get { return (DocumentationSectionConfigElement)BaseGet(name); }
}
public int IndexOf(DocumentationSectionConfigElement details)
{
return BaseIndexOf(details);
}
public void Add(DocumentationSectionConfigElement details)
{
BaseAdd(details);
}
protected override void BaseAdd(ConfigurationElement element)
{
BaseAdd(element, false);
}
public void Remove(DocumentationSectionConfigElement details)
{
if (BaseIndexOf(details) >= 0)
BaseRemove(details.Id);
}
public void RemoveAt(int index)
{
BaseRemoveAt(index);
}
public void Remove(string name)
{
BaseRemove(name);
}
public void Clear()
{
BaseClear();
}
protected override string ElementName
{
get { return "DocumentationSection"; }
}
}
public class DocumentationSectionConfigElement : ConfigurationElement
{
[ConfigurationProperty("Id", IsRequired = true, IsKey = true)]
[StringValidator(InvalidCharacters = " ~!##$%^&*()[]{}/;’\"|\\")]
public string Id
{
get { return (string)this["Id"]; }
set { this["Id"] = value; }
}
[ConfigurationProperty("Name", IsRequired = false)]
public string Name
{
get { return (string)this["Name"]; }
set { this["Name"] = value; }
}
[ConfigurationProperty("SectionDescription")]
public SectionDescription SectionDescription
{
get { return ((SectionDescription)base["SectionDescription"]); }
}
}
public class SectionDescriptionConfigElement : ConfigurationElement
{
[ConfigurationProperty("Value", IsRequired = true)]
public string Value
{
get { return (string)this["Value"]; }
set { this["Value"] = value; }
}
}
Since no activity here, I will try answering my questions one by one:
Found this nice article that describes a lot of things and shows somewhat complex example of custom configuration implementation.
I found this great tool - ".NET Configuration Code Generator", that does exactly what I wanted - it takes XML structure and generates custom configuration code.

C# app.config with multiple values with a delimited value

I have my app.config file with an ArrayOfString entry. Each entry contains a semi-colon delimited string. I want to be able to use lambda, if possible, to parse out the values from a List<> based on input criteria. But I want the first entry it finds bases on that criteria. Or is there a better way USING the app.config file?
For instance ..
If I wanted to find the first entry that contained the [source],[filetype] and just return the file path.
Example app.config entry.
SOURCE;FLAC;112;2;\\sourcepath\music\
DEST;FLAC;112;2;\\destpath\music\
Rather than relying on having your values fall at the correct index of a string split operation, you should create your own ConfigurationSection definition.
See the How To on MSDN and the MSDN ConfigurationProperty example.
Here is some code to get you started:
class CustomConfig : ConfigurationSection
{
private readonly CustomElementCollection entries =
new CustomElementCollection();
[ConfigurationProperty("customEntries", IsDefaultCollection = true)]
[ConfigurationCollection(typeof(CustomElementCollection), AddItemName = "add")]
public CustomElementCollection CustomEntries { get { return entries; } }
}
class CustomElementCollection : ConfigurationElementCollection
{
public CustomElement this[int index]
{
get { return (CustomElement) BaseGet(index); }
set
{
if (BaseGet(index) != null)
{
BaseRemoveAt(index);
}
BaseAdd(index, value);
}
}
protected override ConfigurationElement CreateNewElement()
{
return new CustomElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((CustomElement)element).Name;
}
}
class CustomElement : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true)]
public string Name
{
get { return this["name"] as string; }
set { this["name"] = value; }
}
[ConfigurationProperty("direction", IsRequired = true)]
public string Direction
{
get { return this["direction"] as string; }
set { this["direction"] = value; }
}
[ConfigurationProperty("filePath", IsRequired = true)]
public string FilePath
{
get { return this["filePath"] as string; }
set { this["filePath"] = value; }
}
}
Once you have your custom configuration specified, then you can Select with a lambda using any property specified in your custom ConfigurationElement.

Not able to save changes to config file

I've been following this Stackoverflow topic. Everything works on the read side. I get a collection which I transform into a dictionary of the section contents. I publish this to other projects. The problems occur when I try to save a modified section entry. One of the external projects send a key/Value pair and I need to save it to my section. Here is my App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="fileEnvironmentSection" type="MyApp.Infrastructure.Configuration.FileEnvironmentSection, MyApp.Infrastructure"/>
</configSections>
<fileEnvironmentSection>
<fileEnvironment>
<add key="TestEntry1" value="A nice value"/>
<add key="TestEntry2" value="Another value"/>
</fileEnvironment>
</fileEnvironmentSection>
</configuration>
Here are the three layers of types needed to interact with the config section.
using System.Configuration;
namespace SlamDunk.Infrastructure.Configuration {
public class FileEnvironmentSection : ConfigurationSection {
[ConfigurationProperty("fileEnvironment", IsDefaultCollection = false)]
[ConfigurationCollection(typeof(FileEnvironmentCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
public FileEnvironmentCollection FileEnvironmentList {
get { return (FileEnvironmentCollection)base["fileEnvironment"]; }
}
}
}
using System.Configuration;
namespace SlamDunk.Infrastructure.Configuration {
public class FileEnvironmentCollection : ConfigurationElementCollection {
public FileEnvironmentElement this[int index] {
get { return (FileEnvironmentElement)BaseGet(index); }
set {
if(BaseGet(index) != null) BaseRemoveAt(index);
BaseAdd(index, value);
}
}
public void Add(FileEnvironmentElement fileEnvironmentElement) { BaseAdd(fileEnvironmentElement); }
public void Clear() { BaseClear(); }
protected override ConfigurationElement CreateNewElement() { return new FileEnvironmentElement(); }
protected override object GetElementKey(ConfigurationElement element) { return ((FileEnvironmentElement)element).Key; }
public void Remove(FileEnvironmentElement fileEnvironmentElement) { BaseRemove(fileEnvironmentElement.Key); }
public void Remove(string key) { BaseRemove(key); }
public void RemoveAt(int index) { BaseRemoveAt(index); }
}
}
using System.Configuration;
namespace SlamDunk.Infrastructure.Configuration {
public class FileEnvironmentElement : ConfigurationElement {
private const string appConfigDefaultString = "missing";
private const string _appConfigNameKey = "key";
private const string _appConfigNameValue = "value";
public FileEnvironmentElement() { }
public FileEnvironmentElement(string key, string value) {
Key = key;
Value = value;
}
[ConfigurationProperty(_appConfigNameKey, DefaultValue = appConfigDefaultString, IsRequired = true, IsKey = true)]
public string Key {
get { return (string)this[_appConfigNameKey]; }
set { this[_appConfigNameKey] = value; }
}
[ConfigurationProperty(_appConfigNameValue, DefaultValue = appConfigDefaultString, IsRequired = true, IsKey = false)]
public string Value {
get { return (string)this[_appConfigNameValue]; }
set { this[_appConfigNameValue] = value; }
}
}
}
Here is the code I'm using to save a key/value change.
var appConfiguration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var fileEnvironmentSection = appConfiguration.GetSection("fileEnvironmentSection") as FileEnvironmentSection;
var fileEnvironmentList = fileEnvironmentSection.FileEnvironmentList;
fileEnvironmentList.Remove(key);
var element = new FileEnvironmentElement(key, value);
fileEnvironmentList.Add(element);
//appConfiguration.Save(ConfigurationSaveMode.Modified);
//fileEnvironmentSection.CurrentConfiguration.Save(ConfigurationSaveMode.Modified);
fileEnvironmentList.CurrentConfiguration.Save(ConfigurationSaveMode.Modified);
I've checked and the changes appear in the list as expected. I've tried the two commented save call plus the last one. I think I'm getting a new instance of FileEnvironmentSection to avoid ConfigurationManager caching issues. After each test run I look in MyApp.exe.config and do not find any changes. I'm missing something and could use some help figuring out what. Thanks.
appConfiguration.Save(ConfigurationSaveMode.Modified, true);
appConfiguration.Save(ConfigurationSaveMode.Full, true);
the latter should force a full save of the config file, i think the former will as well.
i noticed that you are re-adding the same key to you collect, possibly that's why it is not picked up as modified?

how to read in a list of custom configuration objects

I want to implement Craig Andera's custom XML configuration handler in a slightly different scenario. What I want to be able to do is to read in a list of arbitrary length of custom objects defined as:
public class TextFileInfo
{
public string Name { get; set; }
public string TextFilePath { get; set; }
public string XmlFilePath { get; set; }
}
I managed to replicate Craig's solution for one custom object but what if I want several?
Craig's deserialization code is:
public class XmlSerializerSectionHandler : IConfigurationSectionHandler
{
public object Create(object parent, object configContext, XmlNode section)
{
XPathNavigator nav = section.CreateNavigator();
string typename = (string)nav.Evaluate("string(#type)");
Type t = Type.GetType(typename);
XmlSerializer ser = new XmlSerializer(t);
return ser.Deserialize(new XmlNodeReader(section));
}
}
I think I could do this if I could get
Type t = Type.GetType("System.Collections.Generic.List<TextFileInfo>")
to work but it throws
Could not load type 'System.Collections.Generic.List<Test1.TextFileInfo>' from assembly 'Test1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
I don't think this would work in that scenario. Craig's solution works well for simple object graphs, but collections are a little trickier. Lists are serialised as arrays, so putting your example in, a serialised List is something like:
<ArrayOfTextFileInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlsns:xsd="http://www.w3.org/2001/XMLSchema>
<TextFileInfo>
<Name>My Text File</Name>
<TextFilePath>C:\MyTextFile.txt</TextFilePath>
<XmlFilePath>C:\MyXmlFile.xml</XmlFilePath>
</TextFileInfo>
</ArrayOfTextFileInfo>
Now, I'm guessing you could probably put that in a config, as long as the config section is named as "ArrayOfTextFileInfo". Not exactly that friendly. I think what you should probably do is use the standard configuration classes to build this:
public class TextFileConfigurationElement : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true, IsKey = true)]
public string Name {
get { return (string)this["name"]; }
set { this["name"] = value; }
}
[ConfigurationProperty("textFilePath")]
public string TextFilePath {
get { return (string)this["textFilePath"]; }
set { this["textFilePath"] = value; }
}
[ConfigurationProperty("xmlFilePath")]
public string XmlFilePath {
get { return (string)this["xmlFilePath"]; }
set { this["xmlFilePath"] = value; }
}
}
[ConfigurationCollection(typeof(TextFileConfigurationElement))]
public class TextFileConfigurationElementCollection : ConfigurationElementCollection
{
protected override void CreateNewElement() {
return new TextFileConfigurationElement();
}
protected override object GetElementKey(ConfigurationElement element) {
return ((TextFileConfigurationElement)element).Name;
}
}
public class TextFilesConfigurationSection : ConfigurationSection
{
[ConfigurationProperty("files")]
public TextFileConfigurationElementCollection Files {
get { return (TextFileConfigurationElementCollection)this["files"]; }
set { this["files"] = value; }
}
public static TextFilesConfigurationSection GetInstance() {
return ConfigurationManager.GetSection("textFiles") as TextFilesConfigurationSection;
}
}
Once you've registered the config section:
<configSections>
<add name="textFiles" type="...{type here}..." />
</configSections>
You can add in the configs:
<textFiles>
<files>
<add name="File01" textFilePath="C:\File01.txt" xmlTextFile="C:\File01.xml" />
</files>
</textFiles>
Using that in code:
public List<TextFileInfo> GetFiles() {
var list = new List<TextFileInfo>();
var config = TextFileConfigurationSection.GetInstance();
if (config == null)
return list;
foreach (TextFileConfigurationElement fileConfig in config.Files) {
list.Add(new TextFileInfo
{
Name = fileConfig.Name,
TextFilePath = fileConfig.TextFilePath,
XmlFilePath = fileConfig.XmlFilePath
});
}
return list;
}
Also, this:
Type t = Type.GetType("System.Collections.Generic.List<TextFileInfo>")
Won't work for a couple of reasons, you haven't fully qualified the TextFileInfo type (needs a namespace), and your definition of a generic type is wrong (I can see why you haven't specified it that way), it should look like:
Type t = Type.GetType("System.Collections.Generic.List`1[MyNamespace.TextFileInfo]");
Hope that helps!

Categories