Skip to content

Commit

Permalink
Added config validation and more detailed docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
nemec committed Jan 1, 2013
1 parent c677f66 commit fab01bb
Show file tree
Hide file tree
Showing 13 changed files with 628 additions and 122 deletions.
78 changes: 78 additions & 0 deletions RATIONALE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
There's a lot of talk about configuration, and rightfully so. It's a very
complex topic and no one configuration system cater to everyone's needs.
Most of the talk focuses on one very specific aspect of configuration,
file formats, while ignoring an equally important piece of the puzzle. Namely,
*where* do the settings come from and what do we do once we've loaded them
into our program via our favorite serializer?

While it's natural for different categories of settings to reside in different
locations (think logging settings vs. general application settings), all too
often it's necessary for the *same* settings to be defined in multiple
locations. Two situations come to mind:

* In an application installed on a user's machine, you may keep default
machine-wide settings stored in a privilaged area while allowing users
to customize and override any or all of those settings and store those
changes in their own user folder, away from other users.
* In an environment where one application may be distributed on multiple
machines, a file of sane default settings may be stored in source
control and distributed to each machine, while using a separate file
to selectively override those settings on each server (think development
vs. production Connection Strings). This is a typical pattern seen in
Django (Python) settings modules, where there's a "base" settings module
that imports settings from "local.py" and "dev.py" if they exist.

Existing solutions get us some of the way there, but XML transforms can
only do so much. There are times when that local "configuration file" isn't
even a file but instead drawn from a SQL database or the command line. In
times like that, configuration aggregation must be done within the code itself.

Once all configuration sources are identified, how are the sources accessed?
If each source represents a different section or area of configuration, there
isn't much issue -- logging settings are always stored in the LoggingConfig
variable, and so on. But once sources start overlapping, you need to remember
which one to check first and where to go next if that value is "null" or some
other arbitrary default value. In my opinion, a good configuration manager
should take care of that for you: set the order once and *it* knows which
sources to fall back on when the first try comes up blank.

Not content to just consume configuration files, many applications require
that some or all of their settings be mutable and allow those modified
settings to be persisted across sessions, usually by writing the values back
to whatever source they were retrieved from. A good configuration manager
(implemented in a sufficiently advanced programming language) should be able
to detect those changes and *only* write back those settings that changed
to prevent unchanged settings from finding their way into configuration
files they did not originate from. For example, say config A defines a `Name`,
`Age`, and `Favorite Color` and is *read only*. Say config B defines a
different `Favorite Color` and overrides values in config A. If all three
properties are written back to config B, config A might as well not exist
anymore because config B will just override each of them, even if config A
later changes `Age` to be something different.

I've referred to a generic "configuration manager" earlier, but haven't yet
defined what one is.

* Given multiple configuration "sources" and a hierarchy of "most important"
to "least important", this configuration manager should intelligently
pick the most important source that defines a property and return that
value when you ask for that property by name.
* A configuration manager should be able to write values back to a source
(if it has the right permissions) and should only write the minimum
set of properties needed to completely recreate the current state of
the configuration, assuming that the same set of sources are used with
the same hierarchy.
* It should aggregate the settings into an easily digestible source. This
is less important in dynamically typed languages that allow more freedom
in defining objects, but in statically typed languages it's encouraged
that a configuration manager produce strongly typed objects so that
the settings object can take advantage of IDE tools like refactoring.
* The application that uses the configuration output should know as little
as possible about the details of how the output was generated as possible.
To steal a .Netism, the ideal configuration manager should generate a
[Plain Old CLR Object](http://en.wikipedia.org/wiki/Plain_Old_CLR_Object),
in short an object that requires no more dependencies than what's
already in use by the application. The configuration manager should be as
decoupled as possible from the application.
* This is less of a requirement, but allowing validation of the resulting
output is also desirable.
138 changes: 100 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ Smart Configuration Management for C#

[Listed on NuGet!](http://nuget.org/packages/SmartConf/)

I recently ran into a number of compounding issues that prompted me to create this project.
See RATIONALE.md for a more complete explanation, but suffice to say that
existing configuration systems (specifically in C#) are missing key
functionality that I end up needing at one point or another.

Specifically:

* I need a configuration file to store settings. This, alone, is easily covered by
C#'s standard library ConfigurationManager.
* I have a subset of settings that need to be configured per-host. This is
accomplished with [configSource](http://blog.andreloker.de/post/2008/06/Keep-your-config-clean-with-external-config-files.aspx),
also a standard feature of the ConfigurationManager.
* **I need to write settings back to a file**. This is the crux of the issue. ConfigurationManager
is readonly, but I need to be able to store a value back into the config file before my program
closes (for example, to record the last time the application was run).
also a standard feature of the ConfigurationManager, but it only
works for merging XML configurations into other XML configurations.
* I'd like whatever configuration settings I deal with to be strongly
typed since Visual Studio's refactoring tools are awesome.
* I need to write settings back to a file. And it would be nice if it
didn't automatically write out *every* setting in the configuration object
or require me to manually track each and every setting I change.

This configuration manager allows multiple sources to be merged, in order, into a
single, strongly typed configuration object that can be passed into a program.
Expand All @@ -24,27 +31,34 @@ Features
========

* By default, uses C#'s standard XML-object [serialization](http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx),
so defining a configuration class is no different than defining a standard class.
* Custom IConfigurationSources may be defined to allow settings to be composed from
alternate sources like AppConfig and the command line.
* Automatically detects changes using reflection. No need to explicitly mark a property as
modified.
* Default constructors can also be used to set values not overridden by the base or local
settings file (see note below about "dynamic" default values).
* The configuration manager intelligently *unmerges* the config instance before saving,
so only properties that differ from the default (constructor) value and base settings
value are serialized to a file. This allows you to change default (constructor) values
and reuse the same config files even if they rely on the default values.
* The configuration object is separate from the configuration manager. There is no need to
rewrite existing code to understand the config manager if all it needs are the settings
themselves. So long as the same settings instance is passed to everything, the
configuration manager is able to track changes to the object.
* Since configuration objects are plain objects, you can define instance methods, custom
getters/setters, and more.
so defining a configuration class is no different than defining a standard
class.
* Custom IConfigurationSources may be defined to allow settings to be composed
from alternate sources like AppConfig and the command line.
* Automatically detects changes using reflection. No need to explicitly mark a
property as modified.
* Default constructors can also be used to set values not overridden by the
base or local settings file (see note below about "dynamic"
default values).
* The configuration manager intelligently *unmerges* the config instance
before saving, so only properties that differ from the default
(constructor) value and base settings value are serialized to a file.
This allows you to change default (constructor) values and reuse the same
config files even if they rely on the default values.
* The configuration object is separate from the configuration manager. There
is no need to rewrite existing code to understand the config manager
if all it needs are the settings themselves. So long as the same settings
instance is passed to everything, the configuration manager is able to
track changes to the object.
* Since configuration objects are plain objects, you can define instance
methods, custom getters/setters, and more.
* Validation can be performed on the final configuration object to ensure that
all of its values are valid.

Existing Sources
================
Check out the [wiki](https://github.com/nemec/smartconf/wiki/Existing-Sources) for a list.
Check out the [wiki](https://github.com/nemec/smartconf/wiki/Existing-Sources)
for a list.

Usage
=====
Expand All @@ -53,7 +67,7 @@ BaseSettings.xml:

<Config>
<Name>Horace</Name>
<Webpage>google.com</Webpage>
<Occupation>Web Developer</Occupation>
</Config>

LocalSettings.xml:
Expand All @@ -68,7 +82,12 @@ Config object:
{
public string Name { get; set; }
public int Age { get; set; }
public string Webpage { get; set; }
public string Occupation { get; set; }

public Config()
{
Occupation = "Unemployed";
}
}

Test code:
Expand All @@ -78,7 +97,8 @@ Test code:
config.Age = 20;
}

var configManager = new ConfigurationManager<Config>("BaseSettings.xml", "LocalSettings.xml");
var configManager = new ConfigurationManager<Config>(
"BaseSettings.xml", "LocalSettings.xml");
Config config = configManager.Out;

Console.WriteLine(config.Name);
Expand All @@ -87,8 +107,8 @@ Test code:
Console.WriteLine(config.Age); // Default int value is 0
//> 0

Console.WriteLine(config.Webpage);
//> google.com
Console.WriteLine(config.Occupation);
//> Web Developer

SetAge(config);
Console.WriteLine(config.Age);
Expand All @@ -102,6 +122,46 @@ Test code:
//> Name: Tim The Enchanter
//> Age: 20

Validation
==========

Occasionally, it may be necessary to perform validation on the resulting
configuration object. If an IValidator is passed to the ConfigurationManager
constructor, the final configuration will be tested to ensure it
validates. If validation fails, a ValidationException will be thrown.

To aid in simple validation, a RuleBasedValidator is built in. Boolean
rules (functions that return True if valid and False if not) may be added
and more complex delegates that throw ValidationExceptions to indicate
failure may also be added prior to constructing the ConfigurationManager.
If your needs are more complicated than this, custom IValidators may
be created.

Note that each source will not be tested individually -- since
a source is not required to fill in every single property, there is not
much sense in testing them on their own.

Example code:

var validator = new Validation.RuleBasedValidator<Config>();

// Example of a BooleanRule
validator.AddRule(c => c.Name != null, "You must have a name!");

// Example of a ComplexRule
validator.AddRule(c => {
if(c.Age < 16 && c.Occupation != "Unemployed")
{
throw new ValidationException("Child labor is illegal.");
}
}

var configManager = new ConfigurationManager<Config>(
validator, "BaseSettings.xml", "LocalSettings.xml");

// [...snip...]


TODO
====

Expand All @@ -112,14 +172,16 @@ TODO
Notes
=====

* Since the XmlSerializer automatically initializes the default constructor on serialization,
constructors that use a "dynamic" default value such as DateTime.Now may not correctly
track changes when relying on that default value (ie. if neither the base settings file
nor the local settings file explicitly set a DateTime initialized with Now, the time
difference between serialization will result in different "default" values in the two
objects). This may inadvertently cause the "default" value to be written to the local
settings file as the manager detects the values are different. It is recommended to set
properties to a constant value in the constructor and provide a separate initialization
* Since the XmlSerializer automatically initializes the default constructor
on serialization, constructors that use a "dynamic" default value such
as DateTime.Now may not correctly track changes when relying on that
default value (ie. if neither the base settings file nor the local
settings file explicitly set a DateTime initialized with Now, the time
difference between serialization will result in different "default"
values in the two objects). This may inadvertently cause the "default"
value to be written to the local settings file as the manager detects
the values are different. It is recommended to set properties to a
constant value in the constructor and provide a separate initialization
method that overwrites the constant (if not already overwritten).
* Only *public properties* are tracked by the manager, due in part to limitations with
reflection.
* Only *public properties* are tracked by the manager, due in part to
limitations with reflection.
52 changes: 52 additions & 0 deletions SmartConf.UnitTest/Mocks/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Diagnostics;
using SmartConf.Validation;

namespace SmartConf.UnitTest.Mocks
{
[DebuggerDisplay("Name: {Name}, Age: {Age}, Occupation: {Occupation}")]
internal class Config
{
public string Name { get; set; }
public int Age { get; set; }
public string Occupation { get; set; }

public Config()
{
Occupation = "Unemployed";
}
}

internal class ConfigComparer : IEqualityComparer<Config>
{

public bool Equals(Config x, Config y)
{
if (x == null || y == null)
{
return x != y;
}

return x.Age == y.Age &&
x.Name == y.Name &&
x.Occupation == y.Occupation;
}

public int GetHashCode(Config obj)
{
return 3 * (obj.Name != null ? obj.Name.GetHashCode() : 0) +
5 * (obj.Age.GetHashCode());
}
}

internal class ConfigValidator : IValidator<Config>
{
public void Validate(Config obj)
{
if (obj.Age < 18)
{
throw new ValidationException("Minors not allowed.");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace SmartConf.UnitTest
namespace SmartConf.UnitTest.Mocks
{
public class DummyConfigurationSource<T> : IConfigurationSource<T> where T : class
{
Expand Down
Loading

0 comments on commit fab01bb

Please sign in to comment.