Search

Friday, 29 April 2011

Accessing ASP.NET Session Data using Dynamics


We all know that in ASP.NET (up to and including version 3.5) accessing session data was done via an indexer on the HttpSessionState object. For example:

// C#
Session["MyInt"] = 12;
int myInt = (int)Session["MyInt"];
' Visual Basic
Session("MyInt") = 12
Dim myInt as Integer = CInt(Session("MyInt"))

The System.Dynamic namespace (introduced in .NET 4.0) gives us the ability to create objects whose members (properties, methods etc.) are not specifically coded; but instead are added dynamically as and when they are required. This means we should now be able to access session information via a dynamic object where, instead of an indexer, we use a dynamic property to specify the name of the session item.

We can already see examples of where this has been done elsewhere in the framework. Take, for example, ViewData in MVC:

// C#
// MVC 1.0 and 2.0
ViewData["MyString"] = "Foo";

// MVC 3.0
ViewBag.MyString = "Foo";
' Visual Basic
' MVC 1.0 and 2.0
ViewData("MyString") = "Foo"

' MVC 3.0
ViewBag.MyString = "Foo"

So how can we achieve the same with ASP.NET session data? Firstly, we need to create ourselves a dynamic object which is going wrap the current HttpSessionState object:

// C#
public sealed class SessionBag : DynamicObject
{
    private SessionBag()
    {
    }
}
' Visual Basic
Public NotInheritable Class SessionBag
    Inherits DynamicObject

    Private Sub New()

    End Sub

End Class

Note how the class inherits from DynamicObject. This is a base class for specifying dynamic behaviour at run time. Additionally, the constructor is set as private. The reason for this will become apparent later.

Next, we add a convenience property for accessing the current HttpSessionState object:

// C#
private HttpSessionStateBase Session
{
    get { return new HttpSessionStateWrapper(HttpContext.Current.Session); }
}
' Visual Basic
Private ReadOnly Property Session As HttpSessionStateBase
    Get
        Return New HttpSessionStateWrapper(HttpContext.Current.Session)
    End Get
End Property

We then override the TryGetMember() and TrySetMember() methods of DynamicObject. These methods define how our dynamic object should behave when a dynamic property is accessed. In this case we want it to retrieve and add items to the HttpSessionState respectively:

// C#
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    result = Session[binder.Name];
    return true;
}

public override bool TrySetMember(SetMemberBinder binder, object value)
{
    Session[binder.Name] = value;
    return true;
}
' Visual Basic
Public Overrides Function TryGetMember(binder As System.Dynamic.GetMemberBinder, ByRef result As Object) As Boolean
    result = Session(binder.Name)
    Return True
End Function

Public Overrides Function TrySetMember(binder As System.Dynamic.SetMemberBinder, value As Object) As Boolean
    Session(binder.Name) = value
    Return True
End Function

Additionally, we can override the TryGetIndex() and TrySetIndex() methods so our session data is still accessible using its index position:

// C#
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
    int index = (int)indexes[0];
    result = Session[index];
    return result != null;
}

public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
    int index = (int)indexes[0];
    Session[index] = value;
    return true;
}
' Visual Basic
Public Overrides Function TryGetIndex(binder As System.Dynamic.GetIndexBinder, indexes() As Object, ByRef result As Object) As Boolean
    Dim index As Integer = CInt(indexes(0))
    result = Session(index)
    Return Not result Is Nothing
End Function

Public Overrides Function TrySetIndex(binder As System.Dynamic.SetIndexBinder, indexes() As Object, value As Object) As Boolean
    Dim index As Integer = CInt(indexes(0))
    Session(index) = value
    Return True
End Function

Finally, we add a static variable for the current SessionBag object and a convenience property for accessing it. This ensures that only one SessionBag object is created and is why its constructor is private:

// C#
private static readonly SessionBag sessionBag;

static SessionBag()
{
    sessionBag = new SessionBag();
}

public static dynamic Current
{
    get { return sessionBag; }
}
' Visual Basic
Private Shared ReadOnly _sessionBag As SessionBag

Shared Sub New()
    _sessionBag = New SessionBag()
End Sub

Public Shared ReadOnly Property Current As Object
    Get
        Return _sessionBag
    End Get
End Property

Note that in C# the return type of the Current property is of type dynamic. This tells the C# compiler to use late binding on that object and allow members to be added dynamically. The same thing is achieved in Visual Basic by setting Option Strict to Off.

The complete code for the class is as follows:

// C#
public sealed class SessionBag : DynamicObject
{
    private static readonly SessionBag sessionBag;

    static SessionBag()
    {
        sessionBag = new SessionBag();
    }

    private SessionBag()
    {
    }

    private HttpSessionStateBase Session
    {
        get { return new HttpSessionStateWrapper(HttpContext.Current.Session); }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = Session[binder.Name];
        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        Session[binder.Name] = value;
        return true;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        int index = (int)indexes[0];
        result = Session[index];
        return result != null;
    }

    public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
    {
        int index = (int)indexes[0];
        Session[index] = value;
        return true;
    }

    public static dynamic Current
    {
        get { return sessionBag; }
    }
}
' Visual Basic
Public NotInheritable Class SessionBag
    Inherits DynamicObject

    Private Shared ReadOnly _sessionBag As SessionBag

    Shared Sub New()
        _sessionBag = New SessionBag()
    End Sub

    Private Sub New()

    End Sub

    Private ReadOnly Property Session As HttpSessionStateBase
        Get
            Return New HttpSessionStateWrapper(HttpContext.Current.Session)
        End Get
    End Property

    Public Overrides Function TryGetMember(binder As System.Dynamic.GetMemberBinder, ByRef result As Object) As Boolean
        result = Session(binder.Name)
        Return True
    End Function

    Public Overrides Function TrySetMember(binder As System.Dynamic.SetMemberBinder, value As Object) As Boolean
        Session(binder.Name) = value
        Return True
    End Function

    Public Overrides Function TryGetIndex(binder As System.Dynamic.GetIndexBinder, indexes() As Object, ByRef result As Object) As Boolean
        Dim index As Integer = CInt(indexes(0))
        result = Session(index)
        Return Not result Is Nothing
    End Function

    Public Overrides Function TrySetIndex(binder As System.Dynamic.SetIndexBinder, indexes() As Object, value As Object) As Boolean
        Dim index As Integer = CInt(indexes(0))
        Session(index) = value
        Return True
    End Function

    Public Shared ReadOnly Property Current As Object
        Get
            Return _sessionBag
        End Get
    End Property

End Class

An Example

Here is an example of our SessionBag class in action. It is a simple MVC application which generates random numbers and uses the ASP.NET session to output the current number and the last number to the browser:

//C#
public class RandomController : Controller
{
    private Random random;

    public RandomController()
    {
        random = new Random();
    }

    public ActionResult Index()
    {
        SessionBag.Current.LastNumber = SessionBag.Current.CurrentNumber;
        int number = random.Next();
        SessionBag.Current.CurrentNumber = number;
        return View();
    }
}
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Random</title>
</head>
<body>
    <div>
        <% using (Html.BeginForm())
           { %>
        <% if (SessionBag.Current.CurrentNumber != null)
           {%>
        Current number is
        <%: SessionBag.Current.CurrentNumber %>
        <br />
        <%} %>
        <% if (SessionBag.Current.LastNumber != null)
           {%>
        Last number was
        <%: SessionBag.Current.LastNumber %>
        <br />
        <%} %>
        <input type="submit" value="Generate" />
        <%} %>
    </div>
</body>
</html>
' Visual Basic
Public Class RandomController
    Inherits System.Web.Mvc.Controller

    Private _random As Random

    Public Sub New()
        _random = New Random()
    End Sub

    Public Function Index() As ActionResult
        SessionBag.Current.LastNumber = SessionBag.Current.CurrentNumber
        Dim number As Integer = _random.Next()
        SessionBag.Current.CurrentNumber = number
        Return View()
    End Function

End Class
<%@ Page Language="VB" Inherits="System.Web.Mvc.ViewPage" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title>Random</title>
</head>
<body>
    <div>
        <%: "" %>
        <% Using Html.BeginForm()
        %>
        <% If Not SessionBag.Current.CurrentNumber Is Nothing Then
        %>
        Current number is
        <%: SessionBag.Current.CurrentNumber %>
        <br />
        <%
        End If%>
        <% If Not SessionBag.Current.LastNumber Is Nothing Then
        %>
        Last number was
        <%: SessionBag.Current.LastNumber %>
        <br />
        <%
        End If%>
        <input type="submit" value="Generate" />
        <%
        End Using%>
    </div>
</body>
</html>

No comments:

Post a Comment