RenderAction with ASP.NET MVC 3 Sessionless Controllers

One of the new features of ASP.NET MVC 3 is a controller-level attribute to control the availability of session state. In the RC the attribute, which lives in the System.Web.SessionState namespace, is [ControllerSessionState]; for RTM ScottGu says it will be renamed simply [SessionState]. The attribute accepts a SessionStateBehavior argument, one of Default, Disabled, ReadOnly or Required. A question that came up during a Twitter discussion a few weeks back is how the different behaviors affect Html.RenderAction(), so I decided to find out.

The Setup

I started with an empty MVC 3 project and the Razor view engine. We’ll let a view model figure out what’s going on with our controller’s Session:

public class SessionModel
{
    public SessionModel(Controller controller, bool delaySession = false)
    {
        SessionID = delaySession ? "delayed" : GetSessionId(controller.Session);
        Controller = controller.GetType().Name;
    }

    public string SessionID { get; private set; }
    public string Controller { get; private set; }

    private static string GetSessionId(HttpSessionStateBase session)
    {
        try
        {
            return session == null ? "null" : session.SessionID;
        }
        catch (Exception ex)
        {
            return "Error: " + ex.Message;
        }
    }
}

The model is rendered by two shared views. Index.cshtml gives us some simple navigation and renders actions from our various test controllers:

@model SessionStateTest.Models.SessionModel
@{
    View.Title = Model.Controller;
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Host: @Model.Controller (@Model.SessionID)</h2>
<ul>
    <li>@Html.ActionLink("No Attribute", "Index", "Home")</li>
    <li>@Html.ActionLink("Exception", "Index", "Exception")</li>
    <li>@Html.ActionLink("Default", "Index", "DefaultSession")</li>
    <li>@Html.ActionLink("Disabled", "Index", "DisabledSession")</li>
    <li>@Html.ActionLink("ReadOnly", "Index", "ReadOnlySession")</li>
    <li>@Html.ActionLink("Required", "Index", "RequiredSession")</li>
</ul>
@{
    Html.RenderAction("Partial", "Home");
    Html.RenderAction("Partial", "Exception");
    Html.RenderAction("Partial", "DefaultSession");
    Html.RenderAction("Partial", "DisabledSession");
    Html.RenderAction("Partial", "ReadOnlySession");
    Html.RenderAction("Partial", "RequiredSession");
}

Partial.cshtml just dumps the model:

@model SessionStateTest.Models.SessionModel
<div>Partial: @Model.Controller (@Model.SessionID)</div>

Finally, we need a few test controllers which will all inherit from a simple HomeController:

public class HomeController : Controller
{
    public virtual ActionResult Index()
    {
        return View(new SessionModel(this));
    }

    public ActionResult Partial()
    {
        return View(new SessionModel(this));
    }
}

[ControllerSessionState(SessionStateBehavior.Default)]
public class DefaultSessionController : HomeController { }

[ControllerSessionState(SessionStateBehavior.Disabled)]
public class DisabledSessionController : HomeController { }

[ControllerSessionState(SessionStateBehavior.ReadOnly)]
public class ReadOnlySessionController : HomeController { }

[ControllerSessionState(SessionStateBehavior.Required)]
public class RequiredSessionController : HomeController { }

And finally, a controller that uses the SessionModel constructor’s optional delaySession parameter. This parameter allows us to test RenderAction‘s Session behavior if the host controller doesn’t use Session:

public class ExceptionController : HomeController
{
    public override ActionResult Index()
    {
        return View(new SessionModel(this, true));
    }
}

The Reveal

So what do we find? Well the short answer is that the host controller’s SessionStateBehavior takes precedence. In the case of Home, Default, ReadOnly, and Required, we have access to Session information in all rendered actions:
Sessionless Controller: RenderAction with SessionState

If the host controller is marked with SessionStateBehavior.Disabled, all the rendered actions see Session as null:
Sessionless Controller: RenderAction with Disabled SessionState

I see this is the key finding to remember: an action that depends on Session, even if its controller is marked with SessionStateBehavior.Required, will be in for a nasty NullRef surprise if it’s rendered by controller without. It would be nice if the framework either gave some sort of warning about this, or if they used a Null Object pattern instead of just letting Session return null.

Finally, things get really weird if a Session-dependent action is rendered from a host controller that doesn’t reference Session, even if SessionState is enabled:

Sessionless Controller Exception: Session state has created a session id, but cannot save it because the response was already flushed by the application.

It’s pretty clear the issue has something to do with where RenderAction() happens in the request lifecycle, but it’s unclear how to resolve it short of accessing Session in the host controller.

So there we have it…a comprehensive testing of sessionless controllers and RenderAction for the ASP.NET MVC 3 Release Candidate. Hopefully the inconsistencies of the latter two cases will be resolved or at least documented before RTM.

About these ads

One Response to “RenderAction with ASP.NET MVC 3 Sessionless Controllers”

  1. Haacked Says:

    Hey there. Thanks for the write-up. The last scenario you wrote about…

    “Finally, things get really weird if a Session-dependent action is rendered from a host controller that doesn’t reference Session, even if SessionState is enabled:”

    Definitely looks like a bug. Unfortunately it’s probably a bug in System.Web.dll and not System.Web.Mvc.dll. That means it may take a while before there’s a fix available as you’ll have to wait for the next .NET framework release.


Comments are closed.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: