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).
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);
public SqlErrorLog(IDictionary config)
{
if (config == null)
throw new ArgumentNullException("config");
string connectionStringName =
(string)config["connectionStringName"] ?? string.Empty;
string connectionString = string.Empty;
if (connectionStringName.Length > 0)
{
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;
}
if (connectionString.Length == 0)
throw new ApplicationException("Connection string is missing for the SQL error log.");
_connectionString = connectionString;
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;
}
public SqlErrorLog(string connectionString)
{
if (connectionString == null)
throw new ArgumentNullException("connectionString");
if (connectionString.Length == 0)
throw new ArgumentException(null, "connectionString");
_connectionString = connectionString;
}
public override string Name
{
get { return "Microsoft SQL Server Error Log"; }
}
public virtual string ConnectionString
{
get { return _connectionString; }
}
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();
}
}
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;
}
}
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);
SqlConnectionStringBuilder csb =
new SqlConnectionStringBuilder(this.ConnectionString);
csb.AsynchronousProcessing = true;
SqlConnection connection = new SqlConnection(csb.ConnectionString);
SqlCommand command =
Commands.GetErrorsXml(this.ApplicationName, pageIndex, pageSize);
command.Connection = connection;
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;
}
};
try
{
connection.Open();
asyncResult = new AsyncResultWrapper(
command.BeginExecuteXmlReader(
asyncCallback != null ?
delegate { asyncCallback(asyncResult); } : (AsyncCallback)null,
endHandler), asyncState);
return asyncResult;
}
catch (Exception)
{
connection.Dispose();
throw;
}
}
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));
}
}
}
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);
}
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;
}
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;
}
}
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:
<elmah>
<errorLog type="Williablog.Core.Providers.SqlErrorLog, Williablog.Core"
connectionStringName="ErrorDB" />
</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.