Posts Tagged ‘MVC’

Zugriff auf die Session in ASP.NET MVC Controllern

Wednesday, May 7th, 2008

Bei Verwendung des ASP.NET MVC Frameworks besteht meist die Notwendigkeit im Controller auf die HTTP Session zuzugreifen um dort den Zustand abzulegen. Eine naive Implementierung sieht z.B. so aus:

public RenderViewResult Warenkorb() {
    const string key = "warenkorb";
    if (HttpContext.Session[key] == null) {
        HttpContext.Session.Add(key, "");
    }
    string warenkorb = (string)HttpContext.Session[key];

    ViewData[key] = warenkorb;
    return RenderView("Warenkorb", ViewData);
}

Dass diese Implementierung nicht die beste ist erkennt man (spätestens) wenn man versucht die Methode Warenkorb zu testen:

[TestFixture]
public class HomeControllerTests
{
    private HomeController homeController;

    [SetUp]
    public void Setup() {
        homeController = new HomeController();
    }

    [Test]
    public void Action_Warenkorb() {
        RenderViewResult renderViewResult = homeController.Warenkorb();
        Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb"));
    }
}

Ergebnis bei Testausführung: System.NullReferenceException beim Zugriff auf den HttpContext. Dieser wird vom ASP.NET MVC Framework zur Laufzeit bereitgestellt, nicht jedoch wenn wir nur den Controller per Konstruktor instanziieren.

Nun kann man natürlich hingehen und mit Hilfe eines Mock Frameworks die Infrastruktur des Controllers bereitstellen. Die Tests sind dann jedoch sehr aufwendig, schlecht lesbar und dazu noch sehr zerbrechlich:

[SetUp]
public void Setup() {
    homeController = new HomeController();

    mocks = new MockRepository();
    HttpContextBase httpContext = mocks.DynamicMock<HttpContextBase>();
    session = mocks.DynamicMock<HttpSessionStateBase>();
    SetupResult.For(httpContext.Session).Return(session);
    ControllerContext controllerContext = new ControllerContext(
        httpContext, new RouteData(), homeController);
    homeController.ControllerContext = controllerContext;
}

[Test]
public void Action_Warenkorb() {
    using (mocks.Record()) {
        Expect.Call(session["warenkorb"]).Return("5 Äpfel");
    }
    using (mocks.Playback()) {
        RenderViewResult renderViewResult = homeController.Warenkorb();
        Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb"));
    }
}

Viel eleganter lässt sich das Problem lösen in dem wir dem Controller im Konstruktor ein IStateProvider Objekt übergeben mit dessen Hilfe der Controller auf die Session zugreift. Der State Provider lässt sich im Test leicht durch ein Mock Objekt ersetzen:

public interface IStateProvider<T>
{
    T GetState(Controller controller);

    void SetState(Controller controller, T state);
}
 
Eine Implementierung des IStateProvider Interface könnte dann z.B. so aussehen:
 
public class StateProvider<T> : IStateProvider<T> where T : new()
{
    private const string StateKey = "state";

    public T GetState(Controller controller) {
        if (controller.HttpContext.Session[StateKey] == null) {
            SetState(controller, new T());
        }
        return (T)controller.HttpContext.Session[StateKey];
    }

    public void SetState(Controller controller, T state) {
        if (controller.HttpContext.Session[StateKey] == null) {
            controller.HttpContext.Session.Add(StateKey, state);
        }
        else {
            controller.HttpContext.Session[StateKey] = state;
        }
    }
}
 
Im Test wird der State Provider durch ein Mock Objekt ersetzt. Der Test ist auf das wesentliche reduziert, gut lesbar und leicht verständlich:
 
[SetUp]
public void Setup() {
    mocks = new MockRepository();
    stateProvider = mocks.DynamicMock<IStateProvider<string>>();
    homeController = new HomeController(stateProvider);
}

[Test]
public void Action_Warenkorb() {
    using (mocks.Record()) {
        Expect.Call(stateProvider.GetState(homeController)).Return("5 Äpfel");
    }
    using (mocks.Playback()) {
        RenderViewResult renderViewResult = homeController.Warenkorb();
        Assert.That(renderViewResult.ViewName, Is.EqualTo("Warenkorb"));
    }
}
 
Zum Instanziieren der Controller verwende ich Castle Windsor und das MvcContrib Projekt. Eine gute Beschreibung wie die Initialisierung in der global.asax erfolgt findet man bei Mike Hadlow. Damit der Controller instanziiert werden kann muss der Typ StateProvider<> in Windsor bekannt gemacht werden. Andernfalls kann der Konstruktorparameter nicht erzeugt werden.
 
container.AddComponentWithLifestyle("stateProvider",
  typeof(IStateProvider<>), typeof(StateProvider<>),
  LifestyleType.Transient);
 
Technorati-Tags: ,