Click here to Skip to main content
15,887,267 members
Articles / Web Development / ASP.NET
Tip/Trick

Handle Session in ASP.NET MVC

Rate me:
Please Sign up or sign in to vote.
4.87/5 (11 votes)
9 Jun 2015CPOL2 min read 39.1K   22   1
Three different approaches to handle Session access in MVC applications.

Introduction

The proposed solutions solve the problem of accessing Session in controller and therefore allowing testing it.

Background

I have faced the issue with handling Session access in ASP.NET MVC that allows me to write unit test against controller logic. As I am TDD and IOC/DI focused, my first choice was just inject proper dependency. By going through "Pro ASP.NET MVC 5" by Adam Freeman, I have learnt another interesting approach.

Standard Solution

In the moment, I wanted to access Session in ASP.NET I have just called the session of current http request and have the value. Simple and clean. Let's assume that we have standard ASP.NET MVC application. Nothing fancy in it. We have decided to store user preferences regarding page background color and font style. Those data will be stored in the current session.

This is our model:

HTML
public class UserSettingsModel
{
    public string FontName { get; set; }

    public Color Background { get; set; }
}

and UserSettingsController :

HTML
public class UserSettingsController : Controller
{
        public ActionResult Index()
        {
            var service = GetService();
            var model = service.GetSettings();
            return View();
        }

        public ActionResult Save(UserSettingsModel model)
        {
            var service = GetService();
            service.Update(model);
            return View();
        }

        private UserSettingsService GetService()
        {
            var service = (UserSettingsService)Session["Settings"];
            if (service == null)
            {
                service = new UserSettingsService();
                Session["Settings"] = service;
            }
            return service;
        }
    }

In Session, we store our UserSettingsService class:

HTML
public class UserSettingsService
{
    private string _fontName = "Arial";
    private Color _background = Color.White;

    public void Update(UserSettingsModel model)
    {
        _fontName = model.FontName;
        _background = model.Background;
    }

    public UserSettingsModel GetSettings()
    {
        var model = new UserSettingsModel()
        {
            FontName = _fontName,
            Background = _background
        };
        return model;
    }
}

Obviously, writing any unit test against Controller will involve struggle with mocking HttpContext. We just need to get rid of dependency from Controller. We can do this in two ways. First, by using Dependency Injection resolved by container. Second introducing Dependency Injection by Model Binder.

Model Binder Solution

In this approach, we force Models Binder to deliver UserSettingsService on Controller level as Action parameter. To do this, we have to implement IModelBinder in our new UserSettingsServiceModelBinder class.

HTML
public class UserSettingsServiceModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        UserSettingsService service = null;
        if (controllerContext.HttpContext.Session != null)
        {
            service = (UserSettingsService)controllerContext.HttpContext.Session["Settings"];
        }

        if (service == null && controllerContext.HttpContext.Session != null)
        {
            service = new UserSettingsService();
            controllerContext.HttpContext.Session["Settings"] = service;
        }
        return service;
    }
}

To make it work, we also have to register it in Global.asax.cs to let it know our application, that if any action will need to bind request to model of type UserSettingsService, just use UserSettingsServiceModelBinder class to handle this:

HTML
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    ModelBinders.Binders.Add(typeof(UserSettingsService), new UserSettingsServiceModelBinder());

    BundleConfig.RegisterBundles(BundleTable.Bundles);
    AuthConfig.RegisterAuth();
}

Now, we can get rid of direct call to session in actions in UserSettingsController and this way making them testable and not dependent of HttpContext.

HTML
public class UserSettingsController : Controller
  {
     public ActionResult Index(UserSettingsService service)
     {
         var model = service.GetSettings();
         return View();
     }

     public ActionResult Save(UserSettingsService service, UserSettingsModel model)
     {
         service.Update(model);
         return View();
     }
 }

UserSettingsService can be mocked now action methods can be tested in isolation.

Dependency Injection Solution

More convenient for me is injecting dependency into UserSettingsController constructor as below:

HTML
public class UserSettingsController : Controller
{
    private readonly IUserSettingsService _service;

    public UserSettingsController(IUserSettingsService service)
    {
        Contract.Requires(service == null, "User settings service must be injected.");
        _service = service;
    }

    public ActionResult Index()
    {
        var model = _service.GetSettings();
        return View();
    }

    public ActionResult Save(UserSettingsModel model)
    {
        _service.Update(model);
        return View();
    }
}
HTML
public interface IUserSettingsService
{
    UserSettingsModel GetSettings();

    void Update(UserSettingsModel model);
}

Also, we are not storing in Session entire service class but just model.

HTML
public class UserSettingsService : IUserSettingsService
   {
       private readonly UserSettingsModel _settings;

       public UserSettingsService()
       {
           var settings = (UserSettingsModel)HttpContext.Current.Session["Settings"];
           if (settings == null)
           {
               settings = new UserSettingsModel();
               HttpContext.Current.Session["Settings"] = settings;
           }
           _settings = settings;
       }

       public UserSettingsModel GetSettings()
       {
           return _settings;
       }

       internal void Update(UserSettingsModel model)
       {
           _settings.Background = model.Background;
           _settings.FontName = model.FontName;
       }
   }

Of course, we have to register IUserSettingsService in Dependency Injection container of our choice, but this will not be covered by this tip.

As we can see, we can perform tests on actions in full isolation and mock service on class declaration. One more advantage of this solution is flexibility. If there is request to store user settings information instead of Session in database, we can replace in Dependency Injection container reference to other UserSettingsService that will access database.

HTML
public class UserSettingsService : IUserSettingsService
    {
        private readonly IUserSettingsReporitory _repository;
 
        public UserSettingsService(IUserSettingsReporitory repository)
        {
            Contract.Requires(repository == null, "User settings repository must be injected.");
            _repository = repository;
        }
 
        public UserSettingsModel GetSettings()
        {
            return _repository.GetSettings();
        }
 
        internal void Update(UserSettingsModel model)
        {
            _repository.Update(model.Background, model.FontName);
        }
    }

For the sake of simplicity, implementation of IUserSettingsRepository is omitted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugPossible Bug? Pin
Bruno Franchini14-Jun-19 8:37
Bruno Franchini14-Jun-19 8:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.