Wednesday, August 20, 2008

Taking the HttpContext out of the MVC Framework Controller

I really like the MVC Framework. I'm currently working on my third commercial project using it and I just love the flexibility it gives me, especially the opportunity to refactor the framework itself.

Here's an example: one of the things I dislike is the way the default Controller class is overly coupled to the HttpContext. HttpContext in itself is a problematic hangover from ASP.NET and to have it bound into the Controller to such an extent is a design mistake IMHO.

OK, so what do I mean? Take this action code from a controller:

public ActionResult Search()
{
    const int pageSize = 20;

    var criteria = new ArticleSearchCriteria();
    validatingBinder.UpdateFrom(criteria, Request.Form);

    var articles = articleRepository
        .GetAll()
        .ThatMatch(criteria)
        .ToPagedList(PageNumber, pageSize);

    return View("Search", View.Data
        .WithArticleSearchCriteria(criteria)
        .WithArticles(articles));
}

You see the reference to Request.Form there? The HttpRequest (and HttpResponse and HttpContext) are all exposed as properties of the base Controller class. In order to test that code I have to mock the HttpContext which is a real PITA.

Here's an alternative that I've started using. An IHttpContextService interface:

public interface IHttpContextService
{
    HttpContextBase Context { get; }
    HttpRequestBase Request { get; }
    HttpResponseBase Response { get; }
    NameValueCollection FormOrQuerystring { get; }
}

I can now just use Dependency injection in my controller to get a reference to an implementation of IHttpContextService. Note I don't care here how it is implemented:

public class ArticleController : ControllerBase
{
    private readonly IRepository<Article> articleRepository;
    private readonly IHttpContextService httpContextService;
    private readonly IValidatingBinder validatingBinder;

    public ArticleController(
        IRepository<Article> articleRepository, 
        IHttpContextService httpContextService,
        IValidatingBinder validatingBinder)
    {
        this.articleRepository = articleRepository;
        this.httpContextService = httpContextService;
        this.validatingBinder = validatingBinder;
    }

    public ActionResult Search()
    {
        const int pageSize = 20;

        var criteria = new ArticleSearchCriteria();
        validatingBinder.UpdateFrom(criteria, httpContextService.FormOrQuerystring);

        var articles = articleRepository
            .GetAll()
            .ThatMatch(criteria)
            .ToPagedList(PageNumber, pageSize);

        return View("Search", GroupLibView.Data
            .WithArticleSearchCriteria(criteria)
            .WithArticles(articles));
    }
}

And I can set up my IoC container to give me any implementation of IHttpContextService I want. Now because the IHttpContextService is provided by the IoC container, any dependencies that its implementation may require can also be provided by the IoC container which opens up all kinds of interesting opportunities.

Here's my current HttpContextService:

using System.Collections.Specialized;
using System.Web;

namespace Suteki.Common.Services
{
    public class HttpContextService : IHttpContextService
    {
        public HttpContextBase Context
        {
            get
            {
                return new HttpContextWrapper2(HttpContext.Current);
            }
        }

        public HttpRequestBase Request
        {
            get
            {
                return Context.Request;
            }
        }

        public HttpResponseBase Response
        {
            get
            {
                return Context.Response;
            }
        }

        public NameValueCollection FormOrQuerystring
        {
            get
            {
                if(Request.RequestType == "POST")
                {
                    return Request.Form;
                }
                return Request.QueryString;
            }
        }
    }
}

7 comments:

Stefan Lieser said...

Hello Mike,

I did the same for the Session object and blogged about it here (in german of course): http://www.lieser-online.de/blog/?p=82

Cheers,
Stefan

P.S. See you on ALT.NET UK!!

Mike Hadlow said...

Hi Stefan,

Thanks for that. I think we're on the same wavelength.

Yes, looking forward to ALT.NET UK :)

Ken Egozi said...

You just have to love Monorail for using IHttpStuffAdapters everywhere ... ;)

Chris Missal said...

Can't you just pass in your criteria through the Action without pulling it in through the Request? This would be easier to test and you wouldn't have to mock the HttpContext at all. I'm still kind of new to MVC so maybe I missed a previous blog post...

Mike Hadlow said...

Chris,

You are right, I could have used the binding stuff in MvcContrib and simply had the criteria as the argument for my test.

The main issue I have with that approach is that I like to have my validation rules in property setters, which is the obvious place for them. I've written a custom binder, the validatingBinder in the example which can handle the validation exceptions generated by the property setters. In this example I haven't shown the exception handling but if you get the suteki shop code there are plenty of examples there.

You might argue that I would have been better off implementing my own controller base class and putting the custom binding logic in that, but I haven't quite got that far yet. I really need to look closer at how the MvcContrib ConventionController does.

But it doesn't alter the point that it's often useful to be able to access the HttpContext in a controller and this method gives a far more IoC freindly design.

Chris Missal said...

I totally agree that "it's often useful to be able to access the HttpContext in a controller and this method gives a far more IoC friendly design."

On our MVC project we're trying to avoid validation on the properties. However, our properties are more suited toward real world domain objects (Order, Address, etc). We feel that these properties shouldn't contain validation since the validation is basically business rules. You SearchCriteria object though... I can buy the validation going in those properties.

Ben Foster said...

Thanks Mike. I would also do the same with IPrincipal for accessing the current User (httpContext.User).