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.