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:
public class UserSettingsModel
{
public string FontName { get; set; }
public Color Background { get; set; }
}
and UserSettingsController
:
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:
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.
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:
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
.
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:
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();
}
}
public interface IUserSettingsService
{
UserSettingsModel GetSettings();
void Update(UserSettingsModel model);
}
Also, we are not storing in Session
entire service class but just model.
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.
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.