Saturday, December 18, 2010

Enterprise Library 5.0: Creating a custom configuration source

During the course of a project, it became apparent that a number of applications were in fact using the same, or very similar, enterprise library configuration. Such configuration items included:
  • Logging
  • Exception handling
  • Policy injection
  • Authorization
The situation was such that a simple use of externalized files would not really be much of a step forward. So I decided that I'd investigate and implement a custom configuration source using the semi-documented enterprise library 'hooks'.

Before outlining how this is actually done, it is important to note that nearly all the applications in question are deployed in a DMZ, and the only viable mechanism of accessing centralized configuration is by employing a web service call. The DMZ cluster machines are hardened to an almost excessive degree, and hosting (for example) a database in that environment, or configuring Microsoft DFS, would simply not be possible. As SSL secured web services underpin all communication from the DMZ, this then became the obvious base technology.

There are a few steps involved in this solution.

Step #1: app.config/web.config entries
The integration point for the custom configuration source is in the app.config/web.config file, replacing the whole of 'standard' enterprise library configuration with an entry similar to that which follows. The key entry here is that of the 'add' element with name 'Remote EL Configuration'. This registers the custom configuration source that is described here, and will ultimately be called by the enterprise library 'bootstrap machinery' to configure enterprise library for use (apologies for the formatting):


<enterpriseLibrary.ConfigurationSource 
  selectedSource="Remote EL Configuration">    
    <sources>
       <add name="System Configuration Source" type="Microsoft.Practices.EnterpriseLibrary.Common.Configuration.SystemConfigurationSource, Microsoft.Practices.EnterpriseLibrary.Common, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
       <add name="Remote EL Configuration" type="ApplicationConfiguration.EnterpriseLibraryConfigurator, Application.Configuration, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> 

    </sources> 
</enterpriseLibrary.ConfigurationSource>

It should be obvious that the 'type' attribute value is the assembly qualified name of the custom configuration source we describe next.

Step #2: The 'configurator' itself
The full source appears below, and the section after this describes some of the more interesting aspects of the implementation.
 
1:  using System;  
2:  using System.Collections.Generic;  
3:  using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;  
4:  using System.Configuration;  
5:  using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;  
6:  using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.Configuration;  
7:  using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Configuration;  
8:  using Microsoft.Practices.EnterpriseLibrary.Security.Configuration;  
9:  using System.Diagnostics;  
10:  using System.Xml;  
11:  using System.IO;  
12:  using System.Reflection;  
13:    
14:  namespace ApplicationConfiguration {  
15:    
16:    [ConfigurationElementType(typeof(ServiceDrivenConfigurationElement))]  
17:    public sealed class EnterpriseLibraryConfigurator : IConfigurationSource {  
18:    
19:      // Prepended to served configuration to ensure that the EL can interpret (the EL requires a white space lead in)  
20:      private const string RequiredLead = " ";  
21:      private const string EventLogSource = "Custom Configuration Source";  
22:      private static readonly Dictionary<string, SettingsContainer> mSettingsCreation =  
23:                new Dictionary<string, SettingsContainer> {   
24:                  { LoggingSettings.SectionName, new SettingsContainer { Creator = () => new LoggingSettings() } },   
25:                  { PolicyInjectionSettings.SectionName, new SettingsContainer { Creator = () => new PolicyInjectionSettings() } },  
26:                  { ExceptionHandlingSettings.SectionName, new SettingsContainer { Creator = () => new ExceptionHandlingSettings() } },  
27:                  { SecuritySettings.SectionName, new SettingsContainer { Creator = () => new SecuritySettings() } }  
28:                };  
29:    
30:      public EnterpriseLibraryConfigurator(DistributedConfiguration service) {  
31:        Service = service;  
32:      }  
33:    
34:      private DistributedConfiguration Service { get; set; }  
35:    
36:      #region IConfigurationSource Members  
37:    
38:      public static string RequestingSystem { get; set; }  
39:    
40:      public void Add(string sectionName, ConfigurationSection configurationSection) {  
41:        NoImplementation(MethodInfo.GetCurrentMethod().Name);  
42:      }  
43:    
44:      public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) {  
45:        NoImplementation(MethodInfo.GetCurrentMethod().Name);  
46:      }  
47:    
48:      public ConfigurationSection GetSection(string sectionName) {  
49:        DBC.AssertNotNullOrEmpty(RequestingSystem, "This type expects a delivery channel key to be specified before execution");  
50:        ConfigurationSection section = null;  
51:        try {  
52:          if (mSettingsCreation.ContainsKey(sectionName)) {  
53:            LogFacade.CreateEventLogEntry(string.Format("EL Configurator {0} attempt construction of section: \"{1}\"", DCSFactory.ResolveDenotation(RequestingSystem), sectionName), EventLogEntryType.Information, EventLogSource);  
54:            InstrumentationConfigurationRequest request = DCSFactory.CreateRequest<InstrumentationConfigurationRequest>(RequestingSystem);  
55:            request.InstrumentationName = sectionName;  
56:            InstrumentationConfigurationResponse response = Service.GetInstrumentationConfiguration(request, String.Empty);  
57:            DBC.Assert(response != null && response.Success, string.Format("Failed request for {0}", sectionName));  
58:            if (!String.IsNullOrEmpty(response.EncodedConfiguration)) {  
59:              LogFacade.CreateEventLogEntry(string.Format("EL Configurator section: \"{0}\", source: {1}", sectionName, response.EncodedConfiguration), EventLogEntryType.Information, EventLogSource);  
60:              SerializableConfigurationSection newSection = mSettingsCreation[sectionName].Creator();  
61:              string normalizedConfiguration = string.Format("{0}{1}", RequiredLead, response.EncodedConfiguration.Trim());  
62:              using (StringReader reader = new StringReader(normalizedConfiguration)) {  
63:                newSection.ReadXml(XmlReader.Create(reader));  
64:              }  
65:              section = newSection;  
66:              if (mSettingsCreation[sectionName].PostCreationAction != null)  
67:                mSettingsCreation[sectionName].PostCreationAction();  
68:            }  
69:          }  
70:        }  
71:        catch (Exception ex) {  
72:          LogFacade.CreateEventLogEntry(string.Format("EL Configurator failed: {0}", ex.ToString()), EventLogEntryType.Error, EventLogSource);  
73:          //// 
75:        return section;  
76:      }  
77:    
78:      public void Remove(string sectionName) {  
79:        NoImplementation(MethodInfo.GetCurrentMethod().Name);  
80:      }  
81:    
82:      public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler) {  
83:        NoImplementation(MethodInfo.GetCurrentMethod().Name);  
84:      }  
85:    
86:  #pragma warning disable 67  
87:      // This pragma suppresses a warning re: lack of use of this event; however, it is legitimately unused, but required to  
88:      // fulfill the interface contract. Hence, suppression is allowable in this instance.  
89:      public event EventHandler<ConfigurationSourceChangedEventArgs> SourceChanged;  
90:  #pragma warning restore 67  
91:    
92:      #endregion  
93:    
94:      #region IDisposable Members  
95:    
96:      public void Dispose() {  
97:        GC.SuppressFinalize(this);  
98:      }  
99:    
100:      #endregion  
101:    
102:      private void NoImplementation(string name) {  
103:        throw new NotImplementedException(String.Format("This type does not implement {0}", name));  
104:      }  
105:    
106:      private class SettingsContainer {  
107:    
108:        internal Func<SerializableConfigurationSection> Creator { get; set; }  
109:    
110:        internal Action PostCreationAction { get; set; }  
111:      }  
112:    
113:    }  
114:    
115:    public class ServiceDrivenConfigurationElement : ConfigurationSourceElement {  
116:    
117:      // Magic string; means nothing but needs to be included  
118:      public ServiceDrivenConfigurationElement() : base("SDCE", typeof(EnterpriseLibraryConfigurator)) {  
119:      }  
120:    
121:      public override IConfigurationSource CreateSource() {  
122:        return new EnterpriseLibraryConfigurator(DCSFactory.CreateService());  
123:      }  
124:    }  
125:  }  
126:    

Basic implementation
As the EnterpriseLibraryConfigurator is specified as the custom source in app.config/web.config, it will be called by the executing Enterprise Library 'instance' when configuration data is required for one of the supported enterprise library (EL) 'modules', such as logging, exception handling and so on.

EL adopts a 'greedy' approach to configuration activities, in that it will request resolution for all 'modules' that can be configured externally. I suppose that this is the preferred route in case of inter dependent configurations, for example if your exception handling configuration exhibited a dependency on your logging configuration.

The method of interest for configuration fetching is the following one, implemented as part of the standard EL IConfigurationSource interface:

48:      public ConfigurationSection GetSection(string sectionName) {  

The sectionName argument will match the SectionName property on one of the supported EL settings objects: SecuritySettings, PolicyInjectionSettings, ExceptionHandlingSettings, LoggingSettings). In the configurator class, there is a dictionary defined that has as it's key the SectionName of one of the mentioned 'Settings' objects, and a value of an instance of the self defined private class SettingsContainer


106:      private class SettingsContainer {  
107:    
108:        internal Func<SerializableConfigurationSection> Creator { get; set; }  
109:    
110:        internal Action PostCreationAction { get; set; }  
111:      }  

The signature of GetSection is such that we are expected to, on demand, return an appropriately initialized configuration section. Since we are only supplied with the section name, we of course retain associations between the section name and some mechanism for creating the correct object. SettingsContainer provides a simple Func to do this. The initialization of the dictionary is shown next: 

23:                new Dictionary<string, SettingsContainer> {   
24:                  { LoggingSettings.SectionName, new SettingsContainer { Creator = () => new LoggingSettings() } },   
25:                  { PolicyInjectionSettings.SectionName, new SettingsContainer { Creator = () => new PolicyInjectionSettings() } },  
26:                  { ExceptionHandlingSettings.SectionName, new SettingsContainer { Creator = () => new ExceptionHandlingSettings() } },  
27:                  { SecuritySettings.SectionName, new SettingsContainer { Creator = () => new SecuritySettings() } }  
28:                };

Looking at this implementation now, it occurs to me that it could be simplified. If SettingsContainer took a generic type argument, that was constrained to be a SerializableConfigurationSection and a new() provider, then SettingsContainer.Creator  could provide the implementation directly for creating a section. We could genericise at the method level but not at the type level.

The first semi-interesting part of  GetSection involves seeing if we can service the request and then constructing a request to a configuration service (which as you will recall is a web service - in this case a WCF service):

51:        try {  
52:          if (mSettingsCreation.ContainsKey(sectionName)) {  
53:            LogFacade.CreateEventLogEntry(string.Format("EL Configurator {0} attempt construction of section: \"{1}\"", DCSFactory.ResolveDenotation(RequestingSystem), sectionName), EventLogEntryType.Information, EventLogSource);  
54:            InstrumentationConfigurationRequest request = DCSFactory.CreateRequest<InstrumentationConfigurationRequest>(RequestingSystem);  
55:            request.InstrumentationName = sectionName;  
56:            InstrumentationConfigurationResponse response = Service.GetInstrumentationConfiguration(request, String.Empty);  
57:            DBC.Assert(response != null && response.Success, string.Format("Failed request for {0}", sectionName));  
58:            if (!String.IsNullOrEmpty(response.EncodedConfiguration)) {  

The details of this are not that important, as all we are doing is fetching an XML fragment that represents the current section of interest. An example of an XML fragment (not truly a document, since it lacks the formal structure of a document) for security settings is shown below:

 <securityConfiguration defaultAuthorizationInstance="RuleProvider" defaultSecurityCacheInstance="">  
   <authorizationProviders>  
     <add type="Microsoft.Practices.EnterpriseLibrary.Security.AuthorizationRuleProvider, Microsoft.Practices.EnterpriseLibrary.Security, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" name="RuleProvider"/>  
   </authorizationProviders>  
 </securityConfiguration>  
   

Assuming we have a non trivial fragment to examine, we set about next to actually construct an EL object of the correct type, using objects from the the dictionary we have previously defined and the XML fragment we now have:

60:              SerializableConfigurationSection newSection = mSettingsCreation[sectionName].Creator();  
61:              string normalizedConfiguration = string.Format("{0}{1}", RequiredLead, response.EncodedConfiguration.Trim());  
62:              using (StringReader reader = new StringReader(normalizedConfiguration)) {  
63:                newSection.ReadXml(XmlReader.Create(reader));  
64:              }  
65:              section = newSection;  
66:              if (mSettingsCreation[sectionName].PostCreationAction != null)  
67:                mSettingsCreation[sectionName].PostCreationAction();  

An important action to note is the insertion of white space (declared in our class as RequiredLead) at the start of the XML fragment - without this, and if not present in the fragment, then the call to ReadXml will fail! A bizarre and annoying issue, which had me confused for a good hour or so. So, we construct the section via a SettingsContainer instance call, pass the constructed section an XmlReader created over the fragment to use for its initialization, and then call the post create action (if defined) of the associated SettingsContainer.

All we need to complete the implementation is the decoration of the configurator with an attribute that references a type that knows how to construct the an appropriate instance, shown below:

16:    [ConfigurationElementType(typeof(ServiceDrivenConfigurationElement))]


And a few minor points to finish. This piece of code has a warning suppressed for the stated reason:


86:  #pragma warning disable 67  
87:      // This pragma suppresses a warning re: lack of use of this event; however, it is legitimately unused, but required to  
88:      // fulfill the interface contract. Hence, suppression is allowable in this instance.  
89:      public event EventHandler<ConfigurationSourceChangedEventArgs> SourceChanged;  
90:  #pragma warning restore 67  

Since the configurator does not provide an implementation that integrates with enterprise library editor, this form of implementation is used to provide the necessary exceptions if such methods are called:


78:      public void Remove(string sectionName) {  
79:        NoImplementation(MethodInfo.GetCurrentMethod().Name);  
80:      }  

2 comments:

Grigori Melnik said...

Tony, nice experimentation.
FYI, we've released a set of Enterprise Library Extensibility hands-on labs. Take a look at Lab 3: http://blogs.msdn.com/agile/archive/2011/01/12/enterprise-library-extensibility.aspx

Tony Beveridge said...

Grigori,
They are very useful indeed. My implementation is about 6-7 months old, so pre-dates the labs it seems. I created the post primarily because I couldn't find a decent guide for creating custom config sources...but that's obviously been addressed!