views:

60

answers:

4

How to prepare the ASP.NET MVC Controllers to use Session and at the same time be testable, so in essence not use Session but rather use some Session abstraction? I am using Ninject so your examples could be based on that.

The problem is that the Session object is not available at all times in the controllers (like in ctor's) but I need to store something to the Session at application start (the global.asax.cs also does not have access to the Session).

A: 

There are a couple of ways - either use a custom filter attribute to inject a session value into a controller action, or create a session object with an interface that can be mocked and inject it into the constructor of the controller.

Below is an example of the custom filter.

public class ProfileAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

        filterContext.ActionParameters["profileUsername"] = "some session value";

        base.OnActionExecuting(filterContext);
    }
}`

and a way to use it in a controller:

[ProfileAttribute]
public ActionResult Index(string profileUsername)
{
    return View(profileUsername);
}

Which one you choose probably depends on how much you rely on session values, but either way is relatively testable.

briercan
This solution also suffers from the same problem as @Ladislav one. You have to decorate the controller with the attribute for this to work. I would rather have a developer-indenpedent, "centrally-managed" solution for accesing Session. So it seems that a solution with a mocked Session object and controller injection would be the right way to go.
mare
+2  A: 

You can't store anything to Session at application start. Session = client initiated interaction. You don't have Sessions for all clients at application start.

Controller usually do not interact with session directly - it makes controllerd dependent on session. Instead controller methods (actions) accepts parameters which are automatically filled from the session by creating custom ModelBinder. Simple example:

public class MyDataModelBinder : IModelBinder
{
  private const string _key = "MyData";

  public object BindModel(ControllerContext context, ModelBindingContext bindingContext)
  {
    MyData data = (MyData)context.HttpContext.Session[_key];

    if (data == null)
    {
      data = new MyData();
      context.HttpContext.Session[_key] = data;
    }

    return data;
  }
}

You will than register your binder in Application_Start (Global.asax):

ModelBinders.Binders.Add(typeof(Mydata), new MyDataModelBinder());

And you define your action like:

public ActionResult MyAction(MyData data)
{ ... }

As you can see the controller is in no way dependent on Session and it is fully testable.

Ladislav Mrnka
I guess I could "invent" a class that would hold the data I want to store in a Session and create a custom Model Binder for it but that would require me to add this class as a parameter to all the action methods where I want to use Session. Even if it is testable it kinda doesn't seem right, actually it's cumbersome, a developer would need to know that he has to add the SessionData (I've just come up with this name) class to the controller action where he wants to use Session and if he didn't know that, he would end up writing a method using the Session directly, which would be wrong.
mare
I think that my approach is cleaner because it provides only strongly typed required data to actions which really need them. But yes there is no control over developer code unless you are doing code review. I can imagine writing some custom rules to Code Analysis which will fire when Session is accessed directly in Controller to support my approach.
Ladislav Mrnka
Do you think it would work if I add the parameter to the constructor of the base controller from which all controllers derive?
mare
I don't think so. Model binding is related to deserialization and passing parameters to actions not to instancing controller itself.
Ladislav Mrnka
+1  A: 

Just use a mock framework to create a mock HttpSessionStateBase and inject it into the controller context. With Rhino Mocks, this would be created using MockRepository.PartialMock<HttpSessionStateBase>() (see below). The controller, during the test, will then operate on the mock Session.

var mockRepository = new MockRepository();
var controller = new MyController();
var mockHttpContext = mockRepository.PartialMock<HttpContextBase>();
var mockSessionState = mockRepository.PartialMock<HttpSessionStateBase>();
SetupResult.For(mockHttpContext.Session).Return(mockSessionState);
// Initialize other parts of the mock HTTP context, request context etc
controller.ControllerContext = 
    new ControllerContext(
        new RequestContext(
            mockHttpContext, 
            new RouteData()
        ), 
        controller
    );
bzlm
+1  A: 

If you want your class to be testable, don't use non-testable (external) components there. When you try to mock them, you just work around the bad design. Re-design you controllers instead. A class shouldn't rely on external/global objects. That's one of the reasons why IoC is used.

You have two choices to separate implementation/infrastructure details from your controllers:

  1. Minor abstraction

    public interface ISession
    {
       string GetValue(string name);
       void SetValue(string name, string value);
    }
    
  2. Domain abstraction.

    public interface IStateData
    {
        bool IsPresent { get; }
        int MyDomainMeaningfulVariable { get; set; }
    }
    

In the latter case, the interface adds semantics over session - strongly typed, well named property. This is just as using NHibernate domain entities instead of sqlreader["DB_COLUMN_NAME"].

Then, of course, inject HTTP implementation of the interface (e.g. using HttpContext.Current) into controllers.

Model binders are also a good way to go, just as action filters are. They're not just for form data.

queen3
The second option actually looks really really nice. Just as I would use a repository interface in controller constructor, I could add this as a parameter, then set up Ninject rules to provide concrete implementations.
mare