16

I have read many posts on Session-scoped data in MVC, but I am still unclear where is the right place to include a custom Session wrapper into the solution.

I want to get the Username of the current user from the IPrincipal, load additional information about that User and store it in the Session. Then I want to access that User data from the Controller and the View.

None of the following approaches seem to fit what I want to do.

Option 1 : Access the Session collection directly

Everyone seems to agree this is a bad idea, but honestly it seems like the simplest thing that works. However, it doesn't make the User available to the view.

public class ControllerBase : Controller {
   public ControllerBase() : this(new UserRepository()) {}
   public ControllerBase(IUserRepository userRepository) {
      _userRepository = userRepository;
   }
   protected IUserRepository _userRepository = null;
   protected const string _userSessionKey = "ControllerBase_UserSessionKey";
   protected User {
      get { 
         var user = HttpContext.Current.Session[_userSessionKey] as User;
         if (user == null) {
            var principal = this.HttpContext.User;
            if (principal != null) {
               user = _userRepository.LoadByName(principal.Identity.Name);
               HttpContext.Current.Session[_userSessionKey] = user;
            }
         }
         return user;
      }
   }
}

Option 2: Injecting the Session into the class constructor forum post

This option seems pretty good, but I am still not sure how to attach it to the Controller and the View. I could new-it-up in the Controller, but shouldn't it be injected as a dependency?

public class UserContext {
   public UserContext() 
       : this(new HttpSessionStateWrapper(HttpContext.Current.Session), 
              new UserRepository()) { } 

   public UserContext(HttpSessionStateBase sessionWrapper, IUserRepository userRepository) { 
      Session = sessionWrapper;
      UserRepository = userRepository; 
   } 

   private HttpSessionStateBase Session { get; set; }
   private IUserRepository UserRepository{ get; set; }

   public User Current { 
      get {
         //see same code as option one
      }
   }
}

Option 3 : Use Brad Wilson's StatefulStorage class

In his presentation Brad Wilson features his StatefulStorage class. It is a clever and useful set of classes which include interfaces and uses constructor injection. However, it seems to lead me down the same path as Option 2. It uses interfaces, but I couldn't use the Container to inject it because it relies on a static factory. Even if I could inject it, how does it get passed to the View. Does every ViewModel have to have a base class with a setable User property?

Option 4 : Use something similar to the Hanselman IPrincipal ModelBinder

I could add the User as a parameter to the Action method and use a ModelBinder to hydrate it from the Session. This seems like a lot of overhead to add it everywhere it is needed. Plus I would still have to add it to the ViewModel to make it available to the View.

public ActionResult Edit(int id, 
   [ModelBinder(typeof(IPrincipalModelBinder))] IPrincipal user)
{ ... }

I feel like I am overthinking this, but it also seems like there should be an obvious place to do this sort of thing. What am I missing?

Community
  • 1
  • 1
jedatu
  • 4,053
  • 6
  • 48
  • 60
  • 2
    +1 good question. I would like to know the answer too. I saw Brad Wilson's video which looked pretty good, but i too was unsure of how it fit into DI. – RPM1984 Feb 20 '11 at 23:24

2 Answers2

13

My approach to Session:

Cover Session with interface:

public interface ISessionWrapper
{
    int SomeInteger { get; set; }
}

Implement interface using HttpContext.Current.Session:

public class HttpContextSessionWrapper : ISessionWrapper
{
    private T GetFromSession<T>(string key)
    {
        return (T) HttpContext.Current.Session[key];
    }

    private void SetInSession(string key, object value)
    {
        HttpContext.Current.Session[key] = value;
    }

    public int SomeInteger
    {
        get { return GetFromSession<int>("SomeInteger"); }
        set { SetInSession("SomeInteger", value); }
    }
}

Inject into Controller:

public class BaseController : Controller
{
    public ISessionWrapper SessionWrapper { get; set; }

    public BaseController(ISessionWrapper sessionWrapper)
    {
        SessionWrapper = sessionWrapper;
    }
}

Ninject dependency:

Bind<ISessionWrapper>().To<HttpContextSessionWrapper>()

You can pass some commonly used information using ViewData when you want to use it in master page and using view model in specific views.

LukLed
  • 31,452
  • 17
  • 82
  • 107
  • I like this approach but would go further to use ISessionWrapper from IUserRepository's implementation, and just use IUserRepository in the controller. – CRice Feb 21 '11 at 04:05
  • @CRice: If you want to use one interface in controller, I think it is better to pack ISessionWrapper and IUserRepository into one interface calles `IUserService` have two properties `interface IUserService { IUserRepository UserRepository { get; set; } ISessionWrapper SessionWrapper { get; set; }` and inject it into controller. – LukLed Feb 21 '11 at 06:34
  • 2
    I am going with this answer because it got me past something I was hung up on. I was trying to wrap the Session and call the Repository in the same class. What I needed is in the comment above, a third "service" class that consumes the Wrapper and the Repository. This allows the session Wrapper and the Repository to have a single responsibility, and the third class brings them together to create the strongly typed implementation. – jedatu Feb 21 '11 at 14:07
3

I would strongly recommend passing anything you need in the view down via the controller. That way, the decision on exactly what data the view should render stays with the controller. In order to make that as easy as possible, creating an abstract ViewModelWithUserBase class that has a settable User property really isn't a bad idea. An option is to create an interface IViewModelWithUser, and re-implement the User property every time (or combine with the base class, but you would have the option to re-implement instead of inheriting the base class if that makes things easier in some corner cases).

As far as populating this property, it can probably be done easily with an action filter. Utilizing the OnActionExecuted method you can test if the model passed to the view implements your base class (or interface), and then fill the property with the correct IPrincipal object if appropriate. This has the advantage that since action filters aren't executed in unit tests, you can use the HttpContext.Current.Session dependent code from your option 1 in your action filter, and still have a testable interface on the controller.

Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • I like this intriguing option and it has received several up-votes. However it does not let me use the User data in the controller, because the User property on the ViewModel isn't populated until after the controller's Action method is completed. – jedatu Feb 21 '11 at 08:00