views:

271

answers:

2

I'm modifying an existing winforms application to use the Logging Application Block. For historical reasons this app gets its main database connection string from the registry, and I'd like the Logging Application Block to use the same details for logging to the database. How can I do this?

The approaches i can think of are:

1) Create a new TraceListener and implement the same sort of functionality as in FormattedDatabaseTraceListener. If I take this approach, should I inherit from CustomTraceListener, and if so how do I pass an attribute of the formatter to use?

2) Create a new ConfigurationSource that provides different details when asked for the database connection. All other requests would be passed through to a FileConfigurationSource, but when asked for the database connection details the object would read the appropriate bits from the registry instead.

but it's not obvious which is more appropriate, or how to go about doing it. Any suggestions?

I'm using EntLib 3.1.

thanks,

-Rory

A: 

The solution I came up with:

Initially I looked at approach (1) but it wasn't obvious to me how to pass the AddStoredProcedureName and WriteStoredProcedureName attributes to my custom tracelistener, nor how to create a Database object since it's normally done through factory methods that use a ConfigurationSource. (I suppose I could have used a non-Entlib db object, but I wanted to mostly copy & paste from FormattedDatabaseTraceListener rather than rewrite all the db logic).

So the solution I've come to is based on (2) above. I create a new IConfigurationSource which wraps a FileConfigurationSource, but when GetSection("connectionStrings") is called it first populates the ConnectionStringsSection with a ConnectionStringSettings representing my custom connection string retrieved from the registry instead of from the file:

public class PSConfigurationSource : IConfigurationSource
{
    /// <summary>
    /// Name of the connection string that will be set to use the standard connection
    /// string from the registry. Anything wanting to reference the RM database should 
    /// reference this connection string name.
    /// </summary>
    private const string RMDatabaseName = "RMDatabase"; 

    private IConfigurationSource wrappedSource;

    private ConnectionStringsSection cxnStringsSection;

    /// <summary>
    /// Creates a PSConfigurationSource based on the wrappedSource.
    /// </summary>
    /// <param name="wrappedSource"></param>
    public PSConfigurationSource(IConfigurationSource wrappedSource)
    {
        this.wrappedSource = wrappedSource;
    }

    /// <summary>
    /// Retrieves the specified <see cref="T:System.Configuration.ConfigurationSection"/>, 
    /// unless the connectionStrings section is requested in which case our custom 
    /// config section is returned, which contains our custom connection string.
    /// </summary>
    /// <param name="sectionName">The name of the section to be retrieved.</param>
    /// <returns>
    /// The specified <see cref="T:System.Configuration.ConfigurationSection"/>, or <see langword="null"/> (<b>Nothing</b> in Visual Basic)
    ///             if a section by that name is not found.
    /// </returns>
    public ConfigurationSection GetSection(string sectionName)
    {
        if (sectionName=="connectionStrings")
        {
            EnsureConnectionStringsSectionSet();
            return cxnStringsSection;
        }
        return wrappedSource.GetSection(sectionName);
    }

    /// <summary>
    /// Sets the cxnStringsSection object, populating it with our standard connection 
    /// string retrieved from the registry.
    /// </summary>
    private void EnsureConnectionStringsSectionSet()
    {
        if (cxnStringsSection == null)
        {
            // Get the connectionStrings section from the config file.
            ConfigurationSection configSection = wrappedSource.GetSection("connectionStrings");
            if ((configSection != null) && (configSection is ConnectionStringsSection))
                cxnStringsSection = configSection as ConnectionStringsSection;
            else
                cxnStringsSection = new ConnectionStringsSection();

            // Add in the RM database settings. Seems that ConnectionStringSettingsCollection[<string>] doesn't have a setter, 
            //   despite it being in the documentation, so need to remove then add in case it's already there. 
            cxnStringsSection.ConnectionStrings.Remove(RMDatabaseName);
            cxnStringsSection.ConnectionStrings.Add(new ConnectionStringSettings(
                RMDatabaseName, SomeStaticHelperClass.GetConnectionStringFromRegistry(), "System.Data.SqlClient"));
        }
    }

    #region WrappedMethods


    /// <summary>
    /// Adds a <see cref="T:System.Configuration.ConfigurationSection"/> to the configuration source location specified by 
    ///             <paramref name="saveParameter"/> and saves the configuration source.
    /// </summary>
    /// <remarks>
    /// If a configuration section with the specified name already exists in the location specified by 
    ///             <paramref name="saveParameter"/> it will be replaced.
    /// </remarks>
    /// <param name="saveParameter">The <see cref="T:Microsoft.Practices.EnterpriseLibrary.Common.Configuration.IConfigurationParameter"/> that represents the location where 
    ///             to save the updated configuration.</param><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 void Add(IConfigurationParameter saveParameter, string sectionName, ConfigurationSection configurationSection)
    {
        wrappedSource.Add(saveParameter, sectionName, configurationSection);
    }

    /// <summary>
    /// Removes a <see cref="T:System.Configuration.ConfigurationSection"/> from the configuration source location specified by 
    ///             <paramref name="removeParameter"/> and saves the configuration source.
    /// </summary>
    /// <param name="removeParameter">The <see cref="T:Microsoft.Practices.EnterpriseLibrary.Common.Configuration.IConfigurationParameter"/> that represents the location where 
    ///             to save the updated configuration.</param><param name="sectionName">The name of the section to remove.</param>
    public void Remove(IConfigurationParameter removeParameter, string sectionName)
    {
        wrappedSource.Remove(removeParameter, sectionName);
    }

    /// <summary>
    /// Adds a handler to be called when changes to the section named <paramref name="sectionName"/> are detected.
    /// </summary>
    /// <param name="sectionName">The name of the section to watch for.</param><param name="handler">The handler for the change event to add.</param>
    public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
    {
        wrappedSource.AddSectionChangeHandler(sectionName, handler);
    }

    /// <summary>
    /// Removes a handler to be called when changes to section 
    /// <code>
    /// sectionName
    /// </code>
    ///  are detected.
    /// </summary>
    /// <param name="sectionName">The name of the watched section.</param><param name="handler">The handler for the change event to remove.</param>
    public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
    {
        wrappedSource.RemoveSectionChangeHandler(sectionName, handler);
    }

    #endregion  


}

I then use this ConfigurationSource instead of the default. The call to SomeStaticHelperClass.GetConnectionStringFromRegistry() gets the connection string that is used elsewhere in the app. This solution also has the advantage that I don't need to reproduce the functionality of the FormattedDatabaseTraceListener - ie actually handling the database logic.

To use the PSConfigurationSource instead of the default, I created a static method to get a PSConfigurationSource. In my case I put it in a class I called EnvironmentAssistant:

public class EnvironmentAssistant
{
...
        public static IConfigurationSource GetConfigurationSource()
        {
            return
                new PSConfigurationSource(
                    new FileConfigurationSource(GetConfigFilePath()));
        }
...
}

Then instead of using the normal EntLib classes ExceptionPolicy and Logger (for exception handling and logging) I created new classes in my namespace for each of them. Simply copy the code for ExceptionPolicy and Logger and rename to, say, MyExceptionPolicy and MyLogger. Then make a couple of minor changes so that instead of using the default configuration source they use that static method GetConfigurationSource().

MyExceptionPolicy (Just Initialise appears modified. I'm not sure if I added this method or just modified it. If I added it then you'll need to add calls to it as the first line within HandleFirstException() and HandleException().)

    private static void Initialise()
    {
        if (defaultFactory == null)
        {
            // Nested check should mean that locking overhead isn't applied unless necessary, 
            // and means factory won't be overwritten if two threads hit locked section.
            lock (sync)
            {
                if (defaultFactory == null)
                {
                    exceptionsSource = EnvironmentAssistant.GetConfigurationSource();
                    defaultFactory = new ExceptionPolicyFactory(exceptionsSource);
                }
            }
        }
    }

MyLogger:

    private static LogWriterFactory factory = new LogWriterFactory(EnvironmentAssistant.GetConfigurationSource());

    ...

    internal static void TryLogConfigurationFailure(ConfigurationErrorsException configurationException)
    {
        try
        {
            DefaultLoggingEventLogger logger = EnterpriseLibraryFactory.BuildUp<DefaultLoggingEventLogger>(EnvironmentAssistant.GetConfigurationSource());
            logger.LogConfigurationError(configurationException);
        }
        catch
        { }
    }

Then throughout my code I use MyExceptionPolicy.HandleException( ex, "policy name" ) or MyLogger.Log(...) just like you would the default EntLib classes.

Hopefully this is helpful to someone, sometime.

Rory
A: 

When you say you used the "PSConfigurationSource" instead of the default. How did you set that up? Where did put that?

Thanks in advance for your help.

Jay

Jay Clarke
I updated my answer. Hopefully that's sufficient detail. If not feel free to ask.
Rory