Click here to Skip to main content
15,867,308 members
Articles
(untagged)

Dynamically Setting the Elmah Connection String at Runtime

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
29 Jul 2013CPOL2 min read 29.9K   6   5
Dynamically setting the Elmah connection string at runtime/

If you have read my other articles about setting the SQL Membership provider's connection string at runtime, or automatically detecting the server name and using the appropriate connection strings, then it will come as no surprise to see that I also had to find a way to set the Elmah connection string property dynamically too. If you are reading this, I'll assume that you already know what Elmah is and how to configure it. The problem then is simply that the connection string is supplied in the <elmah><errorLog> section of the web.config using a connection string name, and that while the name may be the same in production as it is in development, chances are high that the connection string itself is different. The connection string property is readonly, so you can't change it at runtime. One solution is to create an elmah.config file, and use Finalbuilder or a web deployment project to change the path to that file when publishing, but if you like the AdvancedSettingsManager class I created and want to use that to set it, you'll need to use a custom ErrorLog. Fortunately, Elmah is open source, so I simply downloaded the source, took a look at their SqlErrorLog class and then copied and pasted most of the code from that class into my own project, modifying it only slightly to suit my own needs.

In the end, the only changes I really needed to make were to pull the connection string by name from my AdvancedSettingsManager class and to copy a couple of helper functions locally into this class since they were marked as internal and therefore unavailable outside of the Elmah solution. I also removed the conditional compilation flags that only applied to .NET 1.x since this was a .NET 3.5 project.

Note: Look in the comments below, this article for a simpler (and in most cases better) approach in which you simply inherit the SqlErrorLog and just override the methods you need to change).

C#
namespace Williablog.Core.Providers
{
    #region Imports
 
    using System;
    using System.Configuration;
    using System.Data;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Threading;
    using System.Xml;
 
    using Elmah;
 
    using ApplicationException = System.ApplicationException;
    using IDictionary = System.Collections.IDictionary;
    using IList = System.Collections.IList;
 
    #endregion
 
    public class SqlErrorLog : ErrorLog
    {
        private readonly string _connectionString;
 
        private const int _maxAppNameLength = 60;
 
        private delegate RV Function<RV, A>(A a);
 
        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// using a dictionary of configured settings.
        /// </summary>
 
        public SqlErrorLog(IDictionary config)
        {
            if (config == null)
                throw new ArgumentNullException("config");
 
// Start Williablog changes
 
            string connectionStringName = 
              (string)config["connectionStringName"] ?? string.Empty;
 
            string connectionString = string.Empty;
 
            if (connectionStringName.Length > 0)
            {
 
            //
            // Write your code here to get the connection string as a ConnectionStringSettings object
 
            //
                ConnectionStringSettings settings = 
                  Williablog.Core.Configuration.AdvancedSettingsManager.SettingsFactory(
                   ).ConnectionStrings["ErrorDB"];
                if (settings == null)
                    throw new ApplicationException("Connection string is missing for the SQL error log.");
 
                connectionString = settings.ConnectionString ?? string.Empty;
            }
 
// End Williablog changes
 
            //
            // If there is no connection string to use then throw an 
            // exception to abort construction.
            //
 
            if (connectionString.Length == 0)
                throw new ApplicationException("Connection string is missing for the SQL error log.");
 
            _connectionString = connectionString;
 
            //
            // Set the application name as this implementation provides
            // per-application isolation over a single store.
            //
 
            string appName = NullString((string)config["applicationName"]);
 
            if (appName.Length > _maxAppNameLength)
            {
                throw new ApplicationException(string.Format(
                    "Application name is too long. Maximum length allowed is {0} characters.",
                    _maxAppNameLength.ToString("N0")));
            }
 
            ApplicationName = appName;
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="SqlErrorLog"/> class
        /// to use a specific connection string for connecting to the database.
        /// </summary>
 
        public SqlErrorLog(string connectionString)
        {
            if (connectionString == null)
                throw new ArgumentNullException("connectionString");
 
            if (connectionString.Length == 0)
                throw new ArgumentException(null, "connectionString");
 
            _connectionString = connectionString;
        }
 
        /// <summary>
        /// Gets the name of this error log implementation.
        /// </summary>
 
        public override string Name
        {
            get { return "Microsoft SQL Server Error Log"; }
        }
 
        /// <summary>
        /// Gets the connection string used by the log to connect to the database.
        /// </summary>
 
        public virtual string ConnectionString
        {
            get { return _connectionString; }
        }
 
        /// <summary>
        /// Logs an error to the database.
        /// </summary>
        /// <remarks>
        /// Use the stored procedure called by this implementation to set a
        /// policy on how long errors are kept in the log. The default
        /// implementation stores all errors for an indefinite time.
        /// </remarks>
 
        public override string Log(Error error)
        {
            if (error == null)
                throw new ArgumentNullException("error");
 
            string errorXml = ErrorXml.EncodeString(error);
            Guid id = Guid.NewGuid();
 
            using (SqlConnection connection = new SqlConnection(this.ConnectionString))
            using (SqlCommand command = Commands.LogError(
                id, this.ApplicationName,
                error.HostName, error.Type, error.Source, error.Message, error.User,
                error.StatusCode, error.Time.ToUniversalTime(), errorXml))
            {
                command.Connection = connection;
                connection.Open();
                command.ExecuteNonQuery();
                return id.ToString();
            }
        }
 
        /// <summary>
        /// Returns a page of errors from the databse in descending order 
        /// of logged time.
        /// </summary>
 
        public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
        {
            if (pageIndex < 0)
                throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null);
 
            if (pageSize < 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, null);
 
            using (SqlConnection connection = new SqlConnection(this.ConnectionString))
            using (SqlCommand command = Commands.GetErrorsXml(this.ApplicationName, pageIndex, pageSize))
            {
                command.Connection = connection;
                connection.Open();
 
                XmlReader reader = command.ExecuteXmlReader();
 
                try
                {
                    ErrorsXmlToList(reader, errorEntryList);
                }
                finally
                {
                    reader.Close();
                }
 
                int total;
                Commands.GetErrorsXmlOutputs(command, out total);
                return total;
            }
        }
 
        /// <summary>
        /// Begins an asynchronous version of <see cref="GetErrors"/>.
        /// </summary>
 
        public override IAsyncResult BeginGetErrors(int pageIndex, int pageSize, 
               IList errorEntryList, AsyncCallback asyncCallback, object asyncState)
        {
            if (pageIndex < 0)
                throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null);
 
            if (pageSize < 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, null);
 
            //
            // Modify the connection string on the fly to support async 
            // processing otherwise the asynchronous methods on the
            // SqlCommand will throw an exception. This ensures the
            // right behavior regardless of whether configured
            // connection string sets the Async option to true or not.
            //
 
            SqlConnectionStringBuilder csb = 
               new SqlConnectionStringBuilder(this.ConnectionString);
            csb.AsynchronousProcessing = true;
            SqlConnection connection = new SqlConnection(csb.ConnectionString);
 
            //
            // Create the command object with input parameters initialized
            // and setup to call the stored procedure.
            //
 
            SqlCommand command = 
              Commands.GetErrorsXml(this.ApplicationName, pageIndex, pageSize);
            command.Connection = connection;
 
            //
            // Create a closure to handle the ending of the async operation
            // and retrieve results.
            //
 
            AsyncResultWrapper asyncResult = null;
 
            Function<int, IAsyncResult> endHandler = delegate
            {
                Debug.Assert(asyncResult != null);
 
                using (connection)
                using (command)
                {
                    using (XmlReader reader = 
                           command.EndExecuteXmlReader(asyncResult.InnerResult))
                        ErrorsXmlToList(reader, errorEntryList);
 
                    int total;
                    Commands.GetErrorsXmlOutputs(command, out total);
                    return total;
                }
            };
 
            //
            // Open the connenction and execute the command asynchronously,
            // returning an IAsyncResult that wrap the downstream one. This
            // is needed to be able to send our own AsyncState object to
            // the downstream IAsyncResult object. In order to preserve the
            // one sent by caller, we need to maintain and return it from
            // our wrapper.
            //
 
            try
            {
                connection.Open();
 
                asyncResult = new AsyncResultWrapper(
                    command.BeginExecuteXmlReader(
                        asyncCallback != null ? /* thunk */ 
                        delegate { asyncCallback(asyncResult); } : (AsyncCallback)null,
                        endHandler), asyncState);
 
                return asyncResult;
            }
            catch (Exception)
            {
                connection.Dispose();
                throw;
            }
        }
 
        /// <summary>
        /// Ends an asynchronous version of <see cref="ErrorLog.GetErrors"/>.
        /// </summary>
 
        public override int EndGetErrors(IAsyncResult asyncResult)
        {
            if (asyncResult == null)
                throw new ArgumentNullException("asyncResult");
 
            AsyncResultWrapper wrapper = asyncResult as AsyncResultWrapper;
 
            if (wrapper == null)
                throw new ArgumentException("Unexepcted IAsyncResult type.", "asyncResult");
 
            Function<int, IAsyncResult> endHandler = 
              (Function<int, IAsyncResult>)wrapper.InnerResult.AsyncState;
            return endHandler(wrapper.InnerResult);
        }
 
        private void ErrorsXmlToList(XmlReader reader, IList errorEntryList)
        {
            Debug.Assert(reader != null);
 
            if (errorEntryList != null)
            {
                while (reader.IsStartElement("error"))
                {
                    string id = reader.GetAttribute("errorId");
                    Error error = ErrorXml.Decode(reader);
                    errorEntryList.Add(new ErrorLogEntry(this, id, error));
                }
            }
        }
 
        /// <summary>
        /// Returns the specified error from the database, or null 
        /// if it does not exist.
        /// </summary>
        public override ErrorLogEntry GetError(string id)
        {
            if (id == null)
                throw new ArgumentNullException("id");
 
            if (id.Length == 0)
                throw new ArgumentException(null, "id");
 
            Guid errorGuid;
 
            try
            {
                errorGuid = new Guid(id);
            }
            catch (FormatException e)
            {
                throw new ArgumentException(e.Message, "id", e);
            }
 
            string errorXml;
 
            using (SqlConnection connection = new SqlConnection(this.ConnectionString))
            using (SqlCommand command = Commands.GetErrorXml(this.ApplicationName, errorGuid))
            {
                command.Connection = connection;
                connection.Open();
                errorXml = (string)command.ExecuteScalar();
            }
 
            if (errorXml == null)
                return null;
 
            Error error = ErrorXml.DecodeString(errorXml);
            return new ErrorLogEntry(this, id, error);
        }
 
        // These utility functions were marked as internal, so I had to copy them locally
        public static string NullString(string s)
        {
            return s ?? string.Empty;
        }
 
        public static string EmptyString(string s, string filler)
        {
            return NullString(s).Length == 0 ? filler : s;
        }
 
// End
 
        private sealed class Commands
        {
            private Commands() { }
 
            public static SqlCommand LogError(
                Guid id,
                string appName,
                string hostName,
                string typeName,
                string source,
                string message,
                string user,
                int statusCode,
                DateTime time,
                string xml)
            {
                SqlCommand command = new SqlCommand("ELMAH_LogError");
                command.CommandType = CommandType.StoredProcedure;
 
                SqlParameterCollection parameters = command.Parameters;
 
                parameters.Add("@ErrorId", SqlDbType.UniqueIdentifier).Value = id;
                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;
                parameters.Add("@Host", SqlDbType.NVarChar, 30).Value = hostName;
                parameters.Add("@Type", SqlDbType.NVarChar, 100).Value = typeName;
                parameters.Add("@Source", SqlDbType.NVarChar, 60).Value = source;
                parameters.Add("@Message", SqlDbType.NVarChar, 500).Value = message;
                parameters.Add("@User", SqlDbType.NVarChar, 50).Value = user;
                parameters.Add("@AllXml", SqlDbType.NText).Value = xml;
                parameters.Add("@StatusCode", SqlDbType.Int).Value = statusCode;
                parameters.Add("@TimeUtc", SqlDbType.DateTime).Value = time;
 
                return command;
            }
 
            public static SqlCommand GetErrorXml(string appName, Guid id)
            {
                SqlCommand command = new SqlCommand("ELMAH_GetErrorXml");
                command.CommandType = CommandType.StoredProcedure;
 
                SqlParameterCollection parameters = command.Parameters;
                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;
                parameters.Add("@ErrorId", SqlDbType.UniqueIdentifier).Value = id;
 
                return command;
            }
 
            public static SqlCommand GetErrorsXml(string appName, int pageIndex, int pageSize)
            {
                SqlCommand command = new SqlCommand("ELMAH_GetErrorsXml");
                command.CommandType = CommandType.StoredProcedure;
 
                SqlParameterCollection parameters = command.Parameters;
 
                parameters.Add("@Application", SqlDbType.NVarChar, _maxAppNameLength).Value = appName;
                parameters.Add("@PageIndex", SqlDbType.Int).Value = pageIndex;
                parameters.Add("@PageSize", SqlDbType.Int).Value = pageSize;
                parameters.Add("@TotalCount", SqlDbType.Int).Direction = ParameterDirection.Output;
 
                return command;
            }
 
            public static void GetErrorsXmlOutputs(SqlCommand command, out int totalCount)
            {
                Debug.Assert(command != null);
 
                totalCount = (int)command.Parameters["@TotalCount"].Value;
            }
        }
 
        /// <summary>
        /// An <see cref="IAsyncResult"/> implementation that wraps another.
        /// </summary>
 
        private sealed class AsyncResultWrapper : IAsyncResult
        {
            private readonly IAsyncResult _inner;
            private readonly object _asyncState;
 
            public AsyncResultWrapper(IAsyncResult inner, object asyncState)
            {
                _inner = inner;
                _asyncState = asyncState;
            }
 
            public IAsyncResult InnerResult
            {
                get { return _inner; }
            }
 
            public bool IsCompleted
            {
                get { return _inner.IsCompleted; }
            }
 
            public WaitHandle AsyncWaitHandle
            {
                get { return _inner.AsyncWaitHandle; }
            }
 
            public object AsyncState
            {
                get { return _asyncState; }
            }
 
            public bool CompletedSynchronously
            {
                get { return _inner.CompletedSynchronously; }
            }
        }
    }
}

Finally, all you need to do is modify the web.config file to use this SqlErrorlog instead of the built in one:

XML
<elmah>   
    <errorLog type="Williablog.Core.Providers.SqlErrorLog, Williablog.Core"
            connectionStringName="ErrorDB" />
<!--
            Other elmah settings ommitted for clarity
-->
</elmah>

Note: You will still need to reference the Elmah DLL in your project as all we have done here is subclass the ErrorLog type, all of the remaining Elmah goodness is still locked up inside the Elmah DLL. You could of course make these changes directly inside the Elmah source code and recompile it to produce your own version of the Elmah DLL, but these changes were project specific and I didn't want to end up one day with dozens of project specific versions of the Elmah DLL. This way, the project specific code stays with the project and the Elmah DLL remains untouched. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Salem Web Network
United States United States
Robert Williams has been programming web sites since 1996 and employed as .NET developer since its release in 2002.

Comments and Discussions

 
SuggestionBroken markupi Pin
Alexander Batishchev27-Jul-13 22:45
Alexander Batishchev27-Jul-13 22:45 
Hi,
Thanks for the article!
But you have the markup broken so a half of code is not visible, another half is messed with HTML.
GeneralRe: Broken markupi Pin
Williarob29-Jul-13 4:11
Williarob29-Jul-13 4:11 
QuestionSimpler appraoch Pin
Stan Shillis14-Jun-12 3:03
Stan Shillis14-Jun-12 3:03 
AnswerRe: Simpler appraoch Pin
Williarob14-Jun-12 3:24
Williarob14-Jun-12 3:24 
AnswerRe: Simpler appraoch Pin
Nilzor3-Jan-14 3:38
Nilzor3-Jan-14 3:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.