Hierarchical Exception Handling with the Enterprise Library






2.60/5 (6 votes)
Defining exception policies in muliple locations.
Introduction
Recently, I encountered a situation where the solution was to have a common implementation for Exception Handling across all applications. Naturally, Microsoft's Enterprise Library and the Exception Handling Application Block came to mind. To satisfy the need for common handling across applications, the configuration files would need to be the same, or at least the Exception Handling entries would need to be. This could, of course, be handled by linking a common config file via source control, or simple cut & paste. The latter, though, would quickly degenerate into an unsupportable mess. The former isn't perfect either, and would be broken by branching the code base.
One way to handle this situation, while still making use of the Enterprise Library, is to have an assembly that encapsulates the common configuration settings, while still allowing for application level settings.
Exploring Exception Handling Policies
If you have used the Exception Handling Application Block, then you should be familiar with this piece of code:
ExceptionPolicy.HandleException(exception, "Policy to Use");
What happens here is that the static class, ExceptionPolicy
, will search the configuration file for the specified policy under the exceptionHandling/exceptionPolicies
section. If found, it will check if the specified exception type has been defined, and will use any handlers that have been defined to process the exception.
<exceptionHandling>
<exceptionPolicies>
<add name="Policy to Use">
<exceptionTypes>
<add type="System.Exception, ...>
<exceptionHandlers>
The ExceptionPolicy
class, like most of the Enterprise Library blocks, uses the Factory pattern to create an implementation class.
private static readonly ExceptionPolicyFactory defaultFactory =
new ExceptionPolicyFactory();
The ExceptionPolicyFactory
class in derived from the generic class LocatorNameTypeFactoryBase<T>
, which is used to create an instance of the specified type defined by a configuration source. The default implementation of this class uses the ConfigurationSourceFactory
, and the ConfigurationSourceSection
class, which as the code documentation states: Creates a new configuration source based on the configuration information from the application's default configuration file.
From this, it can be seen why the default implementation of ExceptionPolicy
only searches the application configuration file. It is, however, relatively easy to change this.
Using an alternate configuration file
The ExceptionPolicyFactory
class has an overloaded constructor that can be used for creating an instance that uses an alternative configuration source, such as an assembly's configuration file, rather than the application's configuration file.
public ExceptionPolicyFactory(IConfigurationSource configurationSource)
The IConfigurationSource
interface represents a source for getting configuration information. The Enterprise Library contains five classes that implement this interface:
ConfigurationSourceSection
NullConfigurationSource
ManageableConfigurationSource
DictionaryConfigurationSource
FileConfigurationSource
The last one, FileConfigurationSource
, is what we need for this solution. The constructor for this class takes a single parameter, which is an absolute, or relative, path to the configuration file to be used.
public FileConfigurationSource(string configurationFilepath)
HierExceptionPolicy
This static class implements the HandleException
method found in the Enterprise Library's ExceptionPolicy
class, but does so in a hierarchical fashion, starting with the configuration file for the assembly it is defined in, and working upward in the application's path. This allows for some policies to be defined at a high level, while also allowing for others to be local to the application.
The class uses a Dictionary
, which is keyed to file names to prevent duplication, to store any FileConfigurationSource
that has been found. Using lazy initialization, for performance reasons (see warning CA1018), upon first being accessed, the collection is initialized with the configuration file for this assembly. Storing the sources already found eliminates the need to continually search for configuration files on subsequent calls. This still, however, allows for configuration files to be added at runtime, as long as no existing file contains an implementation of the policy trying to be located.
private static Dictionary<string, FileConfigurationSource> ConfigSources
{
get
{
if(m_Sources == null)
{
m_Sources = new Dictionary<string, FileConfigurationSource>();
// Get the path for this assembly
string filePath = Assembly.GetExecutingAssembly().Location + ".config";
// Make sure the file exists, and add it to the collection
if(File.Exists(filePath))
m_Sources.Add(Path.GetFileName(filePath), new FileConfigurationSource(filePath));
}
return m_Sources;
}
}
The HandleException
method is fairly straightforward; it finds a policy matching the name given, and handles the given Exception
. If a policy name is not passed, it will use a default policy name, cleverly called "Default Policy", in this case. A more extensible implementation would store this in the configuration file, but for demo purposes, it is hard coded, and noted in the documentation.
public static bool HandleException(Exception ex, string policyName)
{
// Check if a valid exception is passed in
if(ex == null)
throw new ArgumentNullException("ex");
else
ExceptionToHandle = ex;
// If no policy name were passed in, use the
// default policy
if(string.IsNullOrEmpty(policyName))
PolicyName = "Default Policy";
else
PolicyName = policyName;
// Get a policy to handle the exception
ExceptionPolicyImpl policy = GetPolicy();
if(policy == null)
throw new NoPolicyException("No policy named \"" + policyName +
"\" could be found for the exception " + ex.GetType().Name, ex);
// Handle the exception as per the policy
return policy.HandleException(ex);
}
The interesting work happens in the GetPolicy
method. This is where the code attempts to find a policy, matching the name, in the configuration chain which also handles the exception. If one can't be found, a NoPolicyException
is thrown. It's up to the caller to make sure an infinite loop does not occur by trying to handle this exception.
private static ExceptionPolicyImpl GetPolicy()
{
ExceptionPolicyImpl policy = null;
// Search the existing sources for the policy to handle the exception
foreach(FileConfigurationSource source in ConfigSources.Values)
{
policy = FindPolicy(source);
if(policy != null)
return policy;
}
// Nothing was found so far, so we need to walk the
// folder chain to find a policy
// In theory there should be one config so far,
// so start with the siblings at this level.
string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
policy = FindConfigFile(path);
if(policy != null)
return policy;
return null;
}
As can be seen here, the Dictionary
that stores FileConfigurationSource
s is iterated over first to find a policy in a file that may have already been found. If no policy can be found in the collection so far, which may be the case for a first run, the code then starts at the current folder level and looks for any sibling configuration files.
The FindConfigFile
method takes a path and searches for any config files it can find. After checking to make sure the file has not already been added to the collection, the FindPolicy
method is called to attempt to find an ExceptionPolicyImpl
that can be used to handle the exception. If an ExceptionPolicyImpl
can't be found at this level, the code gets the folder at the next level, and, after making sure it has not gone beyond the root of the application, calls FindConfigFile
recursively.
private static ExceptionPolicyImpl FindConfigFile(string path)
{
ExceptionPolicyImpl policy = null;
// Only interested in config files in the directory
string[] files = Directory.GetFiles(path, "*.config");
foreach(string file in files)
{
// Check if the config file was already added
if(!ConfigSources.ContainsKey(Path.GetFileName(file)))
{
// Create a new source and store it in the collection for later
FileConfigurationSource source = new FileConfigurationSource(file);
ConfigSources.Add(Path.GetFileName(file), source);
// Does the source handle the exception?
policy = FindPolicy(source);
if(policy != null)
return policy;
}
}
// Get the parent path
string parentPath = Path.GetDirectoryName(path.Substring(0,
path.LastIndexOf('\\')+1));
// Make sure we have not passed the root folder for the application
// and call this method recursively if not
if(!IsPastRootFolder(parentPath))
return FindConfigFile(parentPath);
return policy;
}
This method will check the FileConfigurationSource
to see if the policy exists, and handles the exception. The ConfigurationException
is thrown if the policy can't be found in the file, in which case, the code eats it and returns null
.
private static ExceptionPolicyImpl FindPolicy(FileConfigurationSource source)
{
// Create a factory from the source
ExceptionPolicyFactory factory = new ExceptionPolicyFactory(source);
ExceptionPolicyImpl policy = null;
// Try to create a policy implemenatation for the policyname
// If the policy can't be found in the source, this exception
// will be thrown
try
{
policy = factory.Create(PolicyName);
}
catch(ConfigurationException)
{
return null;
}
// Now that a policy has been found, see if it
// handles the exception
if(policy.GetPolicyEntry(ExceptionToHandle.GetType()) != null)
return policy;
else
return null;
}
Conclusion
This project was a simple solution for the requirement to have a global, or enterprise wide, definition for Exception Handling rules, while also allowing for the flexibility to define them at the application level. Possible extensions to this methodology would include the ability to override higher level Exception Polices at the application level.
Of course, this is only one possible solution, and I'm sure there is an easier/better method, but it's the only thing I could think of at the time. Enjoy it, and use it as a learning tool or a stepping stone for other applications.
History
- Initial post: 10/6/07.