How to create a custom configuration source that works with configuration console in Entlib 5?

Topics: Enterprise Library Core
Jul 1, 2010 at 3:00 PM
Edited Jul 1, 2010 at 11:45 PM

I created an alternate implementation of FileConfigurationSource to address the issues:

http://entlib.codeplex.com/workitem/26990

http://entlib.codeplex.com/workitem/26760

by Creating a new project in a different namespace and pulling in DesignResources.resx, FileConfigurationSource.cs and FileConfigurationSourceElement.cs

FileConfigurationSource.GetRootedCurrentConfigurationFile was modified to create the rooted path prior to checking if the file exists. 

The class works fine for redirection of the configuration sections related to entlib blocks. However, when I open the web.config using the enterprise library configuration console I get the message:

An error occurred loading the configuration source: The type 'FullQualifiedTypeName' defined in the 'ValidationSourceName' configuration source is invalid.

What is invalid about the source? How do I enable a class (Hsb.Configuration.Entlib.FileConfigurationSource) to utilize the existing FileConfigurationSource editor controls for the the editor console? Thank you, Rich

Jul 2, 2010 at 3:38 AM
Edited Aug 13, 2010 at 4:33 AM

You will create a new configurationsourcelement for your new FileConfigurationSource which simply means renaming the FileConfigurationSourceElement you copied from the source code.  Don't forget to change the ConfigurationElementType for your FileConfigurationSource.  ([ConfigurationElementType(typeof(NewFileConfigurationSourceElement))]) 

With this, you'll define it manually in your config by typing:

<enterpriseLibrary.ConfigurationSource selectedSource="">
    <sources>
      <add name="Your File Configuration Source"
        type="MyNamespace.YourFileConfigurationSource, YourAssembly"
        filePath="validation.config" />
    </sources>
 </enterpriseLibrary.ConfigurationSource>

Sarah Urmeneta
Global Technology and Solutions
Avanade, Inc.
entlib.support@avanade.com

Jul 2, 2010 at 2:23 PM

I'm not sure renaming the classes is required since I put them in a different namespace, but for completeness sake, I renamed both classes in my project by appending 2 on to the class names.  This still resulted in an error when trying to use the configuration console, which prevents me from using the configuration console at all.   Application block configuration redirection does work correctly with these modified classes though.  It is just the configuration console that is failing.

Here are the two modified classes:

 

//===============================================================================
// Microsoft patterns & practices Enterprise Library
// Core
//===============================================================================
// Copyright © Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Storage;
using Microsoft.Practices.EnterpriseLibrary.Common.Properties;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

namespace Hsb.Configuration.Entlib
{
    /// <summary>
    /// Represents a configuration source that retrieves configuration information from an arbitrary file.
    /// </summary>
    /// <remarks>
    /// This configuration source uses a <see cref="System.Configuration.Configuration"/> object to deserialize 
    /// configuration, so the configuration file must be a valid .NET Framework configuration file.
    /// </remarks>
    [ConfigurationElementType(typeof(FileConfigurationSourceElement2))]
    public class FileConfigurationSource2 : FileBasedConfigurationSource, IProtectedConfigurationSource
    {
        private readonly ExeConfigurationFileMap fileMap;
        private readonly object cachedConfigurationLock;
        private System.Configuration.Configuration cachedConfiguration;

        private static int defaultPollDelayInMilliseconds = 15000;

        /// <summary>
        /// Initializes a new instance of the <see cref="FileConfigurationSource2"/> class.
        /// </summary>
        /// <param name="configurationFilepath">The configuration file path. The path can be absolute or relative.</param>
        public FileConfigurationSource2(string configurationFilepath)
            : this(configurationFilepath, true)
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="FileConfigurationSource2"/> class that will refresh changes
        /// according to the value of the <paramref name="refresh"/> parameter.
        /// </summary>
        /// <param name="configurationFilepath">The configuration file path. The path can be absolute or relative.</param>
        /// <param name="refresh"><see langword="true"/> if changes to the configuration file should be notified.</param>
        public FileConfigurationSource2(string configurationFilepath, bool refresh)
            : this(configurationFilepath, refresh, defaultPollDelayInMilliseconds)
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="FileConfigurationSource2"/> that will refresh changes
        /// according to the value of the <paramref name="refresh"/> parameter, polling every 
        /// <paramref name="refreshInterval"/> milliseconds.
        /// </summary>
        /// <param name="configurationFilepath">The configuration file path. The path can be absolute or relative.</param>
        /// <param name="refresh"><see langword="true"/> if changes to the configuration file should be notified.</param>
        /// <param name="refreshInterval">The poll interval in milliseconds.</param>
        public FileConfigurationSource2(string configurationFilepath, bool refresh, int refreshInterval)
            : base(GetRootedCurrentConfigurationFile(configurationFilepath), refresh, refreshInterval)
        {
            this.cachedConfigurationLock = new object();
            this.fileMap = new ExeConfigurationFileMap() { ExeConfigFilename = this.ConfigurationFilePath };
        }


        /// <summary>
        /// Adds a <see cref="ConfigurationSection"/> to the configuration and saves the configuration source.
        /// </summary>
        /// <remarks>
        /// If a configuration section with the specified name already exists it will be replaced.
        /// </remarks>
        /// <param name="sectionName">The name by which the <paramref name="configurationSection"/> should be added.</param>
        /// <param name="configurationSection">The configuration section to add.</param>
        public override void DoAdd(string sectionName, ConfigurationSection configurationSection)
        {
            Save(sectionName, configurationSection);
        }

        /// <summary>
        /// Removes a <see cref="ConfigurationSection"/> from the configuration and saves the configuration source.
        /// </summary>
        /// <param name="sectionName">The name of the section to remove.</param>
        public override void DoRemove(string sectionName)
        {
            if (string.IsNullOrEmpty(sectionName)) throw new ArgumentException(Resources.ExceptionStringNullOrEmpty, "sectionName");

            var fileMap = new ExeConfigurationFileMap() { ExeConfigFilename = ConfigurationFilePath };
            var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

            if (config.Sections.Get(sectionName) != null)
            {
                config.Sections.Remove(sectionName);
                config.Save();

                UpdateCache(true);
            }
        }

        /// <summary>
        /// Adds a <see cref="ConfigurationSection"/> to the configuration and saves the configuration source using encryption.
        /// </summary>
        /// <remarks>
        /// If a configuration section with the specified name already exists it will be replaced.<br/>
        /// If a configuration section was retrieved from an instance of <see cref="FileBasedConfigurationSource"/>, a <see cref="System.InvalidOperationException"/> will be thrown.
        /// </remarks>
        /// <param name="sectionName">The name by which the <paramref name="configurationSection"/> should be added.</param>
        /// <param name="configurationSection">The configuration section to add.</param>
        /// <param name="protectionProviderName">The name of the protection provider to use when encrypting the section.</param>
        /// <exception cref="System.InvalidOperationException">The configuration section was retrieved from an instance of  <see cref="FileBasedConfigurationSource"/> or <see cref="Configuration"/> and cannot be added to the current source.</exception>
        public void Add(
            string sectionName,
            ConfigurationSection configurationSection,
            string protectionProviderName)
        {
            Save(sectionName, configurationSection, protectionProviderName);
        }

        /// <summary>
        /// This method supports the Enterprise Library infrastructure and is not intended to be used directly from your code.
        /// Adds or replaces <paramref name="configurationSection"/> under name <paramref name="section"/> in the configuration and saves the configuration file.
        /// </summary>
        /// <param name="section">The name for the section.</param>
        /// <param name="configurationSection">The configuration section to add or replace.</param>
        public void Save(string section, ConfigurationSection configurationSection)
        {
            ValidateArgumentsAndFileExists(ConfigurationFilePath, section, configurationSection);

            InternalSave(ConfigurationFilePath, section, configurationSection, string.Empty);
        }

        /// <summary>
        /// This method supports the Enterprise Library infrastructure and is not intended to be used directly from your code.
        /// Adds or replaces <paramref name="configurationSection"/> under name <paramref name="section"/> in the configuration 
        /// file and saves the configuration file using encryption.
        /// </summary>
        /// <param name="section">The name for the section.</param>
        /// <param name="configurationSection">The configuration section to add or replace.</param>
        /// <param name="protectionProvider">The name of the protection provider to use when encrypting the section.</param>
        public void Save(string section, ConfigurationSection configurationSection, string protectionProvider)
        {
            ValidateArgumentsAndFileExists(ConfigurationFilePath, section, configurationSection);
            if (string.IsNullOrEmpty(protectionProvider)) throw new ArgumentException(Resources.ExceptionStringNullOrEmpty, "protectionProvider");

            InternalSave(ConfigurationFilePath, section, configurationSection, protectionProvider);
        }

        /// <summary>
        /// Retrieves the specified <see cref="ConfigurationSection"/> from the configuration file.
        /// </summary>
        /// <param name="sectionName">The section name.</param>
        /// <returns>The section, or <see langword="null"/> if it doesn't exist.</returns>
        protected override ConfigurationSection DoGetSection(string sectionName)
        {
            System.Configuration.Configuration configuration = GetConfiguration();

            return configuration.GetSection(sectionName) as ConfigurationSection;
        }


        /// <summary>
        /// Refreshes the configuration sections from the main configuration file and determines which sections have 
        /// suffered notifications and should be notified to registered handlers.
        /// </summary>
        /// <param name="localSectionsToRefresh">A dictionary with the configuration sections residing in the main 
        /// configuration file that must be refreshed.</param>
        /// <param name="externalSectionsToRefresh">A dictionary with the configuration sections residing in external 
        /// files that must be refreshed.</param>
        /// <param name="sectionsToNotify">A new collection with the names of the sections that suffered changes and 
        /// should be notified.</param>
        /// <param name="sectionsWithChangedConfigSource">A new dictionary with the names and file names of the sections 
        /// that have changed their location.</param>
        protected override void RefreshAndValidateSections(
            IDictionary<string, string> localSectionsToRefresh,
            IDictionary<string, string> externalSectionsToRefresh,
            out ICollection<string> sectionsToNotify,
            out IDictionary<string, string> sectionsWithChangedConfigSource)
        {
            UpdateCache(true);

            sectionsToNotify = new List<string>();
            sectionsWithChangedConfigSource = new Dictionary<string, string>();

            // refresh local sections and determine what to do.
            foreach (KeyValuePair<string, string> sectionMapping in localSectionsToRefresh)
            {
                ConfigurationSection section = DoGetSection(sectionMapping.Key);
                string refreshedConfigSource = section != null ? section.SectionInformation.ConfigSource : NullConfigSource;
                if (!sectionMapping.Value.Equals(refreshedConfigSource))
                {
                    sectionsWithChangedConfigSource.Add(sectionMapping.Key, refreshedConfigSource);
                }

                // notify anyway, since it might have been updated.
                sectionsToNotify.Add(sectionMapping.Key);
            }

            // refresh external sections and determine what to do.
            foreach (KeyValuePair<string, string> sectionMapping in externalSectionsToRefresh)
            {
                ConfigurationSection section = DoGetSection(sectionMapping.Key);
                string refreshedConfigSource = section != null ? section.SectionInformation.ConfigSource : NullConfigSource;
                if (!sectionMapping.Value.Equals(refreshedConfigSource))
                {
                    sectionsWithChangedConfigSource.Add(sectionMapping.Key, refreshedConfigSource);

                    // notify only if che config source changed
                    sectionsToNotify.Add(sectionMapping.Key);
                }
            }
        }

        /// <summary>
        /// Refreshes the configuration sections from an external configuration file.
        /// </summary>
        /// <param name="sectionsToRefresh">A collection with the names of the sections that suffered changes and should 
        /// be refreshed.</param>
        protected override void RefreshExternalSections(IEnumerable<string> sectionsToRefresh)
        {
            UpdateCache(true);
        }

        private void InternalSave(string fileName, string section, ConfigurationSection configurationSection, string protectionProvider)
        {
            var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = fileName };
            var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

            if (typeof(ConnectionStringsSection) == configurationSection.GetType())
            {
                config.Sections.Remove(section);
                UpdateConnectionStrings(section, configurationSection, config, protectionProvider);
            }
            else if (typeof(AppSettingsSection) == configurationSection.GetType())
            {
                UpdateApplicationSettings(section, configurationSection, config, protectionProvider);
            }
            else
            {
                config.Sections.Remove(section);
                config.Sections.Add(section, configurationSection);
                ProtectConfigurationSection(configurationSection, protectionProvider);
            }

            config.Save();

            UpdateCache(true);
        }

        private static void ProtectConfigurationSection(ConfigurationSection configurationSection, string protectionProvider)
        {
            if (!string.IsNullOrEmpty(protectionProvider))
            {
                if (configurationSection.SectionInformation.ProtectionProvider == null
                    || configurationSection.SectionInformation.ProtectionProvider.Name != protectionProvider)
                {
                    configurationSection.SectionInformation.ProtectSection(protectionProvider);
                }
            }
            else
            {
                if (configurationSection.SectionInformation.ProtectionProvider != null)
                {
                    configurationSection.SectionInformation.UnprotectSection();
                }
            }
        }

        private void UpdateApplicationSettings(
            string section,
            ConfigurationSection configurationSection,
            System.Configuration.Configuration config,
            string protectionProvider)
        {
            AppSettingsSection current = config.AppSettings;
            if (current == null)
            {
                config.Sections.Add(section, configurationSection);
                ProtectConfigurationSection(configurationSection, protectionProvider);
            }
            else
            {
                AppSettingsSection newApplicationSettings = configurationSection as AppSettingsSection;
                if (current.File != newApplicationSettings.File)
                {
                    current.File = newApplicationSettings.File;
                }

                List<string> newKeys = new List<string>(newApplicationSettings.Settings.AllKeys);
                List<string> currentKeys = new List<string>(current.Settings.AllKeys);

                foreach (string keyInCurrent in currentKeys)
                {
                    if (!newKeys.Contains(keyInCurrent))
                    {
                        current.Settings.Remove(keyInCurrent);
                    }
                }

                foreach (string newKey in newKeys)
                {
                    if (!currentKeys.Contains(newKey))
                    {
                        current.Settings.Add(newKey, newApplicationSettings.Settings[newKey].Value);
                    }
                    else
                    {
                        if (current.Settings[newKey].Value != newApplicationSettings.Settings[newKey].Value)
                        {
                            current.Settings[newKey].Value = newApplicationSettings.Settings[newKey].Value;
                        }
                    }
                }
                ProtectConfigurationSection(current, protectionProvider);
            }

        }

        private void UpdateConnectionStrings(
            string section,
            ConfigurationSection configurationSection,
            System.Configuration.Configuration config,
            string protectionProvider)
        {
            ConnectionStringsSection current = config.ConnectionStrings;
            if (current == null)
            {
                config.Sections.Add(section, configurationSection);
                ProtectConfigurationSection(configurationSection, protectionProvider);
            }
            else
            {
                ConnectionStringsSection newConnectionStrings = (ConnectionStringsSection)configurationSection;
                foreach (ConnectionStringSettings connectionString in newConnectionStrings.ConnectionStrings)
                {
                    if (current.ConnectionStrings[connectionString.Name] == null)
                    {
                        current.ConnectionStrings.Add(connectionString);
                    }
                }
                ProtectConfigurationSection(current, protectionProvider);
            }
        }

        private static string GetRootedCurrentConfigurationFile(string configurationFile)
        {
            if (string.IsNullOrEmpty(configurationFile))
                throw new ArgumentException(Resources.ExceptionStringNullOrEmpty, "configurationFile");


            string strPath =
                Path.IsPathRooted(configurationFile)
                ? configurationFile
                : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, configurationFile);

            if (!File.Exists(strPath))
            {
                throw new FileNotFoundException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Resources.ExceptionConfigurationLoadFileNotFound,
                        configurationFile));
            }
            return strPath;
        }

        private System.Configuration.Configuration GetConfiguration()
        {
            if (cachedConfiguration == null)
            {
                UpdateCache(false);
            }

            return cachedConfiguration;
        }

        internal void UpdateCache(bool forceUpdate)
        {
            lock (cachedConfigurationLock)
            {
                if (forceUpdate || cachedConfiguration == null)
                {
                    System.Configuration.Configuration newConfiguration
                        = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);

                    cachedConfiguration = newConfiguration;
                }
            }
        }
    }
}

//===============================================================================
// Microsoft patterns & practices Enterprise Library
// Core
//===============================================================================
// Copyright © Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================

using System;
using System.ComponentModel;
using System.Configuration;
using System.IO;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Design;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.Design.Validation;
using Microsoft.Practices.EnterpriseLibrary.Common.Properties;

namespace Hsb.Configuration.Entlib
{
    /// <summary>
    /// Represents the configuration settings that describe a <see cref="FileConfigurationSource2"/>.
    /// </summary>
    [ResourceDescription(typeof(DesignResources), "FileConfigurationSourceElementDescription")]
    [ResourceDisplayName(typeof(DesignResources), "FileConfigurationSourceElementDisplayName")]
    [Browsable(true)]
    [EnvironmentalOverrides(false)]
    public class FileConfigurationSourceElement2 : ConfigurationSourceElement
    {
        private const string filePathProperty = "filePath";

        /// <summary>
        /// Initializes a new instance of the <see cref="FileConfigurationSourceElement2"/> class with a default name and an empty path.
        /// </summary>
        public FileConfigurationSourceElement2()
            : this(Resources.FileConfigurationSourceName, string.Empty)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FileConfigurationSourceElement2"/> class with a name and an path.
        /// </summary>
        /// <param name="name">The instance name.</param>
        /// <param name="filePath">The file path.</param>
        public FileConfigurationSourceElement2(string name, string filePath)
            : base(name, typeof(FileConfigurationSource2))
        {
            this.FilePath = filePath;
        }


        /// <summary>
        /// Gets or sets the file path. This is a required field.
        /// </summary>
        [ConfigurationProperty(filePathProperty, IsRequired = true)]
        [ResourceDescription(typeof(DesignResources), "FileConfigurationSourceElementFilePathDescription")]
        [ResourceDisplayName(typeof(DesignResources), "FileConfigurationSourceElementFilePathDisplayName")]
        [Editor(CommonDesignTime.EditorTypes.FilteredFilePath, CommonDesignTime.EditorTypes.UITypeEditor)]
        [FilteredFileNameEditorAttribute(typeof(DesignResources), "FileConfigurationSourceElementFilePathFilter", CheckFileExists = false)]
        [Validation(CommonDesignTime.ValidationTypeNames.PathExistsValidator)]
        [Validation(CommonDesignTime.ValidationTypeNames.FileWritableValidator)]
        public string FilePath
        {
            get { return (string)this[filePathProperty]; }
            set { this[filePathProperty] = value; }
        }

        /// <summary>
        /// Returns a new <see cref="FileConfigurationSource2"/> configured with the receiver's settings.
        /// </summary>
        /// <returns>A new configuration source.</returns>
        public override IConfigurationSource CreateSource()
        {
            IConfigurationSource createdObject = new FileConfigurationSource2(FilePath);

            return createdObject;
        }

        ///<summary>
        /// Returns a new <see cref="IDesignConfigurationSource"/> configured based on this configuration element.
        ///</summary>
        ///<returns>Returns a new <see cref="IDesignConfigurationSource"/> or null if this source does not have design-time support.</returns>
        public override IDesignConfigurationSource CreateDesignSource(IDesignConfigurationSource rootSource)
        {
            return DesignConfigurationSource.CreateDesignSource(rootSource, FilePath);


        }
    }
}

Jul 5, 2010 at 1:03 AM
Edited Dec 6, 2010 at 11:37 PM

Sorry, renaming the classes is yes, unneccesary.  Forgot that it is being fully qualified in the config. 

 

Sarah Urmeneta
Global Technology and Solutions
Avanade, Inc.
entlib.support@avanade.com

Jul 12, 2010 at 5:52 AM
Our applications are using the signed pre-compiled Entlib assemblies. Is there a timeframe for when the bug with configuration redirection using relative paths is going to be fixed and released? It's such a simple fix for a big problem.
Jul 12, 2010 at 6:50 AM
Edited Dec 6, 2010 at 11:38 PM

Ok, I just remembered how to integrate it with the config tool like the SqlConfigurationSource.  You just need to deploy the assembly where your FileConfigurationSource and FileConfigurationSourceElement is to the bin folder of your entlib installation directory (Microsoft Enterprise Library 5.0\Bin).  What will happen is that when you click on Add Configuration Sources, you should see the option to add your custom configuration source.  (So it's really best you renamed your configuration source so as not to confuse yourself when using it in the config tool.) 

Regarding the fix, it'll probably just be fixed in the next version.  Version 5.0 has just been released so I wouldn't wait for the next version just yet.  I suggest sticking to the workaround for now. 

Sarah Urmeneta
Global Technology and Solutions
Avanade, Inc.
entlib.support@avanade.com

Jul 12, 2010 at 6:55 PM
Success! I put my custom configuration source class in the directory specified by the solution's "Enterprise Library 5 Assembly Set" property and the GUI configuration tool is now working fine. I will create an issue to hopefully correct the error message related to this. It didn't help me identify the true issue at all. Thank you for hanging in there on this discussion.