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](http://solutionizing.files.wordpress.com/2010/12/sessionless-renderaction-home.png)

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.

Code Review with Git Patches and Outlook via PowerShell