patch to enable session variable logging with ELMAH

It’s pretty much just copy-paste of the server variables stuff and populating from the HttpContext.Session.  Works For Me 🙂

You can apply this after checking out a copy from http://code.google.com/p/elmah/source/checkout

EDIT 2011-11-02: due to the hassle of trying to find the patch after google groups removed the files (thankfully @raboof was able to get the files!), I’m just pasting it in this post since it’s relatively small.  Also, since this is against r694 which is pretty old, it’s likely the case that someone using it would need to manually apply the patch changes anyway.

Index: src/Elmah/Error.cs
===================================================================
— src/Elmah/Error.cs    (revision 694)
+++ src/Elmah/Error.cs    (working copy)
@@ -32,6 +32,7 @@
     using System.Xml;
     using Thread = System.Threading.Thread;
     using NameValueCollection = System.Collections.Specialized.NameValueCollection;
+    using System.Web.SessionState;
 
     #endregion
 
@@ -54,6 +55,7 @@
         private DateTime _time;
         private int _statusCode;
         private string _webHostHtmlMessage;
+        private NameValueCollection _sessionVariables;
         private NameValueCollection _serverVariables;
         private NameValueCollection _queryString;
         private NameValueCollection _form;
@@ -123,6 +125,7 @@
                 HttpRequest request = context.Request;
 
                 _serverVariables = CopyCollection(request.ServerVariables);
+                _sessionVariables = CopyCollection(context.Session);
                 _queryString = CopyCollection(request.QueryString);
                 _form = CopyCollection(request.Form);
                 _cookies = CopyCollection(request.Cookies);
@@ -264,6 +267,16 @@
         }
 
         /// <summary>
+        /// Gets a collection representing the session values
+        /// captured as part of diagnostic data for the error.
+        /// </summary>
+
+        public NameValueCollection SessionVariables
+        {
+            get { return FaultIn(ref _sessionVariables); }
+        }
+
+        /// <summary>
         /// Gets a collection representing the Web query string variables
         /// captured as part of diagnostic data for the error.
         /// </summary>
@@ -356,6 +369,23 @@
             return copy;
         }
 
+        private static NameValueCollection CopyCollection(HttpSessionState sessionState)
+        {
+            if (sessionState == null || sessionState.Count == 0)
+                return null;
+
+            NameValueCollection copy = new NameValueCollection(sessionState.Count);
+
+            foreach (string sessionKey in sessionState.Keys)
+            {
+                string sessionValue = (sessionState[sessionKey] ?? string.Empty).ToString();
+
+                copy.Add(sessionKey, sessionValue);
+            }
+
+            return copy;
+        }
+
         private static NameValueCollection FaultIn(ref NameValueCollection collection)
         {
             if (collection == null)
Index: src/Elmah/ErrorDetailPage.cs
===================================================================
— src/Elmah/ErrorDetailPage.cs    (revision 694)
+++ src/Elmah/ErrorDetailPage.cs    (working copy)
@@ -242,6 +242,14 @@
             RenderCollection(writer, error.ServerVariables,
                 "ServerVariables", "Server Variables");
 
+            //
+            // If we have session values available, include those
+            // in the output as well.
+            //
+
+            RenderCollection(writer, error.SessionVariables,
+                "SessionVariables", "Session Variables");
+
             base.RenderContents(writer);
         }
 
Index: src/Elmah/ErrorJson.cs
===================================================================
— src/Elmah/ErrorJson.cs    (revision 694)
+++ src/Elmah/ErrorJson.cs    (working copy)
@@ -116,6 +116,7 @@
             Member(writer, "statusCode", error.StatusCode, 0);
             Member(writer, "webHostHtmlMessage", error.WebHostHtmlMessage);
             Member(writer, "serverVariables", error.ServerVariables);
+            Member(writer, "sessionVariables", error.SessionVariables);
             Member(writer, "queryString", error.QueryString);
             Member(writer, "form", error.Form);
             Member(writer, "cookies", error.Cookies);
Index: src/Elmah/ErrorMailHtmlFormatter.cs
===================================================================
— src/Elmah/ErrorMailHtmlFormatter.cs    (revision 694)
+++ src/Elmah/ErrorMailHtmlFormatter.cs    (working copy)
@@ -285,6 +285,7 @@
         protected virtual void RenderCollections()
         {
             RenderCollection(this.Error.ServerVariables, "Server Variables");
+            RenderCollection(this.Error.SessionVariables, "Session Variables");
         }
 
         /// <summary>
Index: src/Elmah/ErrorXml.cs
===================================================================
— src/Elmah/ErrorXml.cs    (revision 694)
+++ src/Elmah/ErrorXml.cs    (working copy)
@@ -146,6 +146,7 @@
                 switch (reader.LocalName)
                 {
                     case "serverVariables" : collection = error.ServerVariables; break;
+                    case "sessionVariables": collection = error.SessionVariables; break;
                     case "queryString"     : collection = error.QueryString; break;
                     case "form"            : collection = error.Form; break;
                     case "cookies"         : collection = error.Cookies; break;
@@ -236,6 +237,7 @@
             if (writer == null) throw new ArgumentNullException("writer");
 
             WriteCollection(writer, "serverVariables", error.ServerVariables);
+            WriteCollection(writer, "sessionVariables", error.SessionVariables);
             WriteCollection(writer, "queryString", error.QueryString);
             WriteCollection(writer, "form", error.Form);
             WriteCollection(writer, "cookies", error.Cookies);

on Quake Live – special Holiday CTF matches with the Silent Night map

yeah, yeah, sshot-fest

They’re certainly interesting matches – instead of the usual limit of 16 players (at the most) in a CTF match, these are 32 each – the maps are HUGE, so it doesn’t feel crowded at all (it just takes forever to get to the enemy base!)

It’s a cute map, lots of holiday stuff around it.

image

image

 

image

A map of, well, the map is included in your home base to help give you an idea of the scope:

image

Lots of trees:

 

image

Center Ring, complete with huge candy canes

image

Closer look:

image

Our base:

image

More candy canes!

image

Come back, Rackspace!

We’re hosted on Rackspace, and our production setup (along with, apparently, tons of Rackspace) is currently offline.  Their 800 support number (800-961-4454) just gives a busy signal, and their live chat interface is offline.

Quite the rash of outages these days, scattered across the place.

use Expression<Func> and not Func when adding abstraction layers for linq-to-sql or linq-to-entities

I was doing some testing on the staging site of an asp.net web app I’m working on and notice some of the pages taking an inordinately long amount of time to load.  Since it’s easy to run and problems tend to jump out quickly, I ran the SQL Server Profiler.  Indeed, some queries were jumping out at me as they were taking ~16 seconds (!) to run.  Looking at the queries, it was pretty clear why they were taking so long – they lacked any where clause – they were getting all the rows of the table – about 250k rows in that table – medium-sized, but enough that fetching the whole thing from a remote machine takes a little while.

I used IntelliTrace in VS 2010 and just turned on the ADO.NET category to see who the offender was, and was kind of surprised.

So, rewind a couple weeks to me doing some various refactorings.  There were some places I was fetching rows by PK and just used .Single(row => row.PK == someId) like you do.  I was fine with Single and the exceptions it throws in the non-1 cases, but when it was hit by a co-worker during some testing, it wasn’t clear to them what the cause was, so I figured I’d do a little wrapper to make it more clear.

private static T GetIdentity<T>(Table<T> table, Func<T, bool> identitySearch)
    where T : class
{
    var matchingIdentities = table.Where(identitySearch).ToList();
    switch (matchingIdentities.Count)
    {
        case 1:
            return matchingIdentities[0];

        case 0:
            throw new ApplicationException(
                String.Format("Database problem: Failed to find matching row from {0}",
                              table.ToString()));

        default:
            throw new ApplicationException(
                String.Format("Database problem: Found {0} matching rows from {1}",
                              matchingIdentities.Count, table.ToString()));
    }
}

(Yeah, I know, don’t use ApplicationException, yadda yadda yadda)
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Then my “fetch by PK” methods just turned into things like:

public SomeItem GetAppraiser(int someItemId)
{
    return GetIdentity(this.SomeItems, row => row.ID == someItemId);
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Since the runtime behavior (specifically, the lambda/func that gets passed in) didn’t change, I didn’t expect to have a runtime behavior in the SQL query generated.  So much so that I didn’t test it to make sure (doh!)

To test this in isolation, we make a console app and:

  • add in a linq-to-sql class for one of the asp.net tables (membership)
  • add in a linq-to-entities class for another of the asp.net tables (users)
  • convert the “non-method” version to .Where(clause).Single()
  • convert the GetIdentity method to just .Where(clause).Single() to make it easier to compare with the “inline” / “non-method” version
  • convert the first param of GetIdentity to IQueryable<T> so it works for both our linq-to-sql and linq-to-entities tests
  • add a “this” in front of the IQueryable<T> first param and move the method to a static class so we can use it as an extension method
  • rename GetIdentity to GetIdentityFunc
  • add a new GetIdentityExpression that only “wraps” the clause method param with Expression<T>

This results in a Program.cs that looks like:

using System;
using System.Linq;
using System.Linq.Expressions;

namespace GetIdentityTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var userIdToFind = new Guid("0c332b31-7c10-42c2-a33f-5f1fac59a291");
            using (var linqToSql = new DataClasses1DataContext())
            {
                var row1 = linqToSql.aspnet_Memberships.Where(row => row.UserId == userIdToFind).Single();
                var row2 = linqToSql.aspnet_Memberships.GetIdentityExpression(row => row.UserId == userIdToFind);
                var row3 = linqToSql.aspnet_Memberships.GetIdentityFunc(row => row.UserId == userIdToFind);
            }
            using (var linqToEntities = new SomeEntities())
            {
                var row1 = linqToEntities.aspnet_Users.Where(row => row.UserId == userIdToFind).Single();
                var row2 = linqToEntities.aspnet_Users.GetIdentityExpression(row => row.UserId == userIdToFind);
                var row3 = linqToEntities.aspnet_Users.GetIdentityFunc(row => row.UserId == userIdToFind);
            }
        }
    }

    public static class Extensions
    {
        public static T GetIdentityExpression<T>(this IQueryable<T> table, Expression<Func<T, bool>> identitySearch)
            where T : class
        {
            return table.Where(identitySearch).Single();
        }

        public static T GetIdentityFunc<T>(this IQueryable<T> table, Func<T, bool> identitySearch)
            where T : class
        {
            return table.Where(identitySearch).Single();
        }
    }
}

With the “inline” .Where(clause).Single() and the GetIdentityExpression versions, we got the expected where clause:

image

But with GetIdentityFunc, we don’t get that where clause – it asks for the entire table and filters on the client side.  Apparently not being an Expression, linq-to-sql can’t “see into” it, so it has to just pass it off.

image

Given how the runtime query generation works, I guess this shouldn’t have surprised me, but it’s quite the little gotcha 🙂

Side note: when you run this test, you’ll only see 5 ADO.NET events, not 6, because the Expression version for linq-to-sql doesn’t do a call, but instead apparently just returns the cached version.  Neat, although a little surprising that Entities didn’t do the same (it repeats the ‘top 2’ query), although I’m guessing (or at least hoping) there’s some setting I could change to have it do so.

image