App.config is very useful in most of .NET applications. Although this file is enough for many applications, there are cases when it's reasonable to divide your configuration into several files. There also can be a situation when it's more natural to present configuration as plain text rather than xml file. Current library is created for easy extraction of configuration in this case and allows you yo change extraction behavior so that you can extract configuration from custom configuration files of different structure.
The idea of the library is rather simple: client has some class whose properties (which should be extracted from configuration file) are marked by special attribute. Configuration extractor then creates the instance of the class and extracts values for these properties. This idea has much in common with that of ConfigurationPropertyAttribute. You can find more info on usage of ConfigurationPropertyAttribute at MSDN and Code Project.
ConfigurationPropertyAttribute
There are 4 essential parts of the ConfigurationExtractor library:
ConfigurationValueAttribute
ConfigurationExtractor
IValueExtractor
Activator.CreateInstance()
IStringConverter
Library contains several implementations of IValueExtractor for value extraction from plain text (PlainTextExtractor), plain text divided into several sections (PlainTextSectionExtractor) and xml structured text (XmlExtractor). It also contains some implementations of IStringConverter for the most typical conversion to Int32, Int64, Double, Decimal, DateTime etc. Client can create his own implementations (some examples are shown in Example console application shipped with the source code and in Using the code section).
PlainTextExtractor
PlainTextSectionExtractor
XmlExtractor
Int32
Int64
Double
Decimal
DateTime
Here are some examples of what is meant by different types of text in configuration file.
Plain text:
a=123 b=456
Plain text with sections:
[sectionA] a=123 [sectionB] b=456
XML
<config> <a>123</a> <b>456</b> </config>
Here are some examples of using the code. The same examples can be found in Example application in source code.
First, simple configuration is considered. Simple here means that configuration consists of some values of standard types. For example:
class Config { [ConfigurationValue(LongName = "BoolVal", ShortName = "BVal", ConverterType = typeof(StringBoolConverter), DefaultValue = "False")] public bool BoolValue { get; set; } [ConfigurationValue(LongName = "DoubleVal", ShortName = "DVal", ConverterType = typeof(StringDoubleConverter), DefaultValue = "36.6")] public double DoubleValue { get; set; } }
The instance of this class can be achieved the following way (given the plain text configuration file):
ConfigurationExtractor<Config> converter = new ConfigurationExtractor<Config>(new PlainTextExtractor()); string confFile = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), "PlainTextConfig.cfg"); var conf = converter.Convert(System.IO.File.ReadAllText(confFile));
Plain text configuration file for this case can contain the following text:
BoolVal=True DVal=12.2
If some more complex configuration is needed than client has to modify value extraction by creating new implementation of IStringConverter. For example, you can easily create list of elements for your configuration. Consider the following configuration class:
class ExtendedConfig { [ConfigurationValue(LongName = "MonthsList", ShortName = "M", ConverterType = typeof(StringMonthsConverter), DefaultValue = "Jan,Sep")] public List<Months> Months { get; set; } } enum Months { Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec }
You can extract list of values using following configuration:
M=Jan,Feb,Oct,Sep
and following converter:
public class StringMonthsConverter: IStringConverter { static Dictionary<string, Months> _corresp = new Dictionary<string, Months> { {"Jan", Months.Jan}, {"Feb", Months.Feb}, {"Mar", Months.Mar}, {"Apr", Months.Apr}, {"May", Months.May}, {"Jun", Months.Jun}, {"Jul", Months.Jul}, {"Aug", Months.Aug}, {"Sep", Months.Sep}, {"Oct", Months.Oct}, {"Nov", Months.Nov}, {"Dec", Months.Dec}, }; public object Convert(string obj) { string[] strings = obj.Split(','); return strings.Select(x => _corresp[x]).ToList(); } }
Extraction itself remains the same:
ConfigurationExtractor<ExtendedConfig> converter = new ConfigurationExtractor<ExtendedConfig>(new PlainTextExtractor()); string confFile = System.IO.Path.Combine( System.IO.Directory.GetCurrentDirectory(), "ExtendedConfig.cfg"); var conf = converter.Convert(System.IO.File.ReadAllText(confFile));
The most complex case is when you have some complex types in configuration class. In this case values inside complex type can be also regarded as configuration values. For example:
class ComplexConfig_Text { [ConfigurationValue(LongName = "connection", ShortName = "con", ConverterType = typeof(ConnectionConfigConverter), DefaultValue = PlainTextExtractor.DefaultValue)] public ConnectionConfig ConnectionConfig { get; set; } [ConfigurationValue(LongName = "user", ShortName = "user", ConverterType = typeof(UserConfigConverter), DefaultValue = PlainTextExtractor.DefaultValue)] public UserConfig UserConfig { get; set; } } class ConnectionConfig { [ConfigurationValue(LongName = "Address", ShortName = "Adr", DefaultValue = "localhost")] public string RemoteAddress { get; set; } } class UserConfig { [ConfigurationValue(LongName = "UserID", ShortName = "user", ConverterType = typeof(StringStringSpecialConverter), DefaultValue = StringStringSpecialConverter.Empty)] public string UserID { get; set; } [ConfigurationValue(LongName = "Password", ShortName = "pwd", ConverterType = typeof(StringStringSpecialConverter), DefaultValue = StringStringSpecialConverter.Empty)] public string Password { get; set; } }
(Note that PlainTextExtractor.DefaultValue is used for default value of ConnectionConfig and UserConfig. This default value is simply an empty string, so when default value is applied this configuration classes are created as if there were no configuration for them. This allows RemoteAddress, UserID, Password to get their initial values as specified in ConfigurationValueAttribute.)
PlainTextExtractor.DefaultValue
ConnectionConfig
UserConfig
RemoteAddress
UserID
Password
Configuration file for this case can be plain text with sections:
[connection] Address=google.com [user] UserID=Admin Password=12345
One needs to use PlainTextSectionExtractor to extract value for ComplexConfig_Text and to create special converters for extraction of ConnectionConfig and UserConfig. Since ConnectionConfig and UserConfig are configuration classes themselves, these converters should use ConfigurationExtractor with PlainTextExtractor to extract their values. Here how it looks like:
ComplexConfig_Text
class ConnectionConfigConverter : IStringConverter { public object Convert(string obj) { return new ConfigurationExtractor<ConnectionConfig>( new PlainTextExtractor()).Convert(obj); } } class UserConfigConverter : IStringConverter { public object Convert(string obj) { return new ConfigurationExtractor<UserConfig>( new PlainTextExtractor()).Convert(obj); } }
The extraction of ComplexConfig_Text looks the following way:
ConfigurationExtractor<Example.Text.ComplexConfig_Text> converter = new ConfigurationExtractor<Example.Text.ComplexConfig_Text>( new PlainTextSectionExtractor()); string confFile = System.IO.Path.Combine( System.IO.Directory.GetCurrentDirectory(), "ComplexTextConfig.cfg"); var conf = converter.Convert(System.IO.File.ReadAllText(confFile));
Solution provided in this article is not very general but I like it for it's simplicity. It's easy to extend it's functionality to gain desirable data in the form you want it. In addition it provides you with unified way to deal with external configuration files (as opposed to app.config which is considered as internal). Some functionality is worth being added to this library: it provides only "get" functionality and it would be nice to have some "set" one, i.e. the possibility to rewrite properties in the configuration file.