Introduction
It's quite convenient to put some application's settings
kept together in a separate easy to edit xml file. There are a number
of .NET projects allowing doing that - like Console Application,
Web Application, Services etc. These have a separate precreated
.config file attached on the project's creation stage. In general, this is
either App.config (like ordinary Console Application would have), or
Web.config in case of Web Application. What would you do if you have Class
Library? This kind of projects doesn't have it's configuration file - even if
you'll attach it manually, you'll never find it under the bin folder named
*.dll.config.
Let's specify the sample problem: there is a solution
containing some projects. Additionally, you have a .config file shared among the
projects within your solution. - this could be a web application with some main
startup project, configured by Web.config, and a couple of class libraries.
Class libraries loaded by main project during it's execution, have a
straightforward access to configuration file. What if we'll have to load
some class libraries by a separate third party services/applications? Here we go
- we're missing the domain of main app execution as well as configuration binded
to it.
If application's configuration isn't supposed to be
changed during the runtime of it's components, there is a way to workaround the
problem - simply by adding shortcuts on .config file to class libraries (on
the solution's level), setting Build Action to Embedded Resource for these
shortcuts and reading embedded resource during runtime after the ordinary
ConfigurationSettings.AppSettings failed to
return it's NameValueCollection.
Basics
The best way to fix the problem described above is to
build a wrapper pretty transparent for the rest of the solution's code. I've
named it CustomConfigurationSettings. There should also be a method to pool up
data from assembly resources and transform it to name-value collection in order
to preserve as much as possible of transparence for the rest of the application.
The code
As soon as the main task is an easy and fast xml
(configuration) parsing, I choose forward-only XmlTextReader for the job.
The goal is to read xml, transform it to collection presentation and store for
further use. Let's put the process on points and review:
1. check if ordinary ConfigurationSettings is available
private static NameValueCollection GetNvcConfig() {
NameValueCollection nvc = null;
if(ConfigurationSettings.AppSettings.Count>0) {
nvc = ConfigurationSettings.AppSettings;
}else{
}
return nvc;
}
2. get embedded configuration stream
private static Stream GetEmbeddedConfigStream() {
Stream stream = null;
Assembly assembly = Assembly.GetExecutingAssembly();
string [] resNames = assembly.GetManifestResourceNames();
if(resNames.Length>0) {
stream = assembly.GetManifestResourceStream(resNames[0]);
}
return stream;
}
3. read configuration stream and transform it's content to
NameValueCollection representation
private static NameValueCollection
GetConfigStreamData(Stream stream) {
int level = -1;
string name = SE;
NameValueCollection nvc = new NameValueCollection();
XmlTextReader xReader = new XmlTextReader(stream);
while(xReader.Read()) {
if(xReader.NodeType==XmlNodeType.Element){
if(xReader.IsEmptyElement){
if(level>xReader.Depth){
name=name.Substring(0, name.LastIndexOf(NS));
level = xReader.Depth;
}else
if(level==xReader.Depth){
name=name.Substring(0, name.LastIndexOf(NS));
level = xReader.Depth-1;
}
}else{
if(level<xReader.Depth){
name+=(name.Length>0?NS:SE)+xReader.Name;
level=xReader.Depth;
}else
if(level>xReader.Depth){
name=name.Substring(0, name.LastIndexOf(NS));
level=xReader.Depth;
}else{
name=name.Substring(0,
name.LastIndexOf(NS)+1)+xReader.Name;
}
}
if(xReader.HasAttributes){
string key=xReader.GetAttribute(ATT_KEY);
string val=xReader.GetAttribute(ATT_VAL);
if(key!=null && val!=null){
nvc[name+NS+key]=val;
}
}
}else
if(xReader.NodeType==XmlNodeType.EndElement){
if(level>xReader.Depth){
name=name.Substring(0, name.LastIndexOf(NS));
level=xReader.Depth;
}
}
}
return nvc;
}
- as you can see, name-value collection keys will have
[itm1]/[itm2].../[itmN] form. Empty elements at the leaves of xml hierarchy
(containing desired attributes) will not form key names. Key values generation
logic stands on not empty xml elements (IsEmptyElement==false) and their depth
in configuration xml content.
The actual values for configuration.appSettings nodes
level will have <configuration/appSettings/[key_attribute_name]> as key,
and key_attribute_value as the value of corresponding key. So having prefix part
(configuration/appSettings) for certain configuration section (not necessary
appSettings, this could be any section if needed) allow us to gain
NameValueCollection for the level we're looking for (by iterating through
keys with certain prefixes).
4. store overall configuration collection in some static
variable (we don't want to read assembly resources again and again for every
conf read call, so let's cache it)
protected static readonly NameValueCollection
_nvcConfig = GetNvcConfig();
5. filter appSettings node (on demand)
private static NameValueCollection GetAppSettingsConfig() {
NameValueCollection nvc = new NameValueCollection();
if(ConfigurationSettings.AppSettings.Count>0){
nvc = _nvcConfig;
}else{
foreach(string key in _nvcConfig.Keys){
if(key.StartsWith(_appSettings_path)){
nvc[key.Substring(_appSettings_path.Length+1)] =
_nvcConfig[key];
}
}
}
return nvc;
}
6. and here we go...
public static NameValueCollection AppSettings {
get{
if(_appSettings==null){
_appSettings = GetAppSettingsConfig();
}
return _appSettings;
}
}
Afterword
Wondering... are you still reading
this? ;)