How I customised my fubuMVC routes with a url policy

I’m currently re-writing my existing application so that it uses fubuMVC and I shared my experiences in how easy it was to get started with fubuMVC – this is the second post in a series on fubuMVC in action.

I’m migrating the static pages for my website – the home, news, about pages – and I now have an opportunity to change a few things around.
One of the things I was doing when using ASP.NET MVC was to have a HomeController that had actions for these pages:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult About()
        {
            return View();
        }

        public ActionResult News()
        {
            return View();
        }
    }

This resulted in urls of “home/index”, “home/about”, “home/news”.

So the first thing I want to change is to ditch the “home” part of the url – for the first one, I’ve already declared my HomeController.FrontPage() method as the root url for my website in my FubuRegistry, so that is taken care of.
I now have the other two to tackle so I want these to be simply “/news” and “/about”.

Here comes the fubu power because I am now free to think of any convention I like to make this happen – once I’ve come up with one, I can declare that convention in my own FubuRegistry.

I decide that for my project, any action that has a method called “Root” will have a custom url that ignores the method and just uses the prefix name of the controller.

To enable this, I’m going to define a custom IUrlPolicy (to read more about policies, see Josh Arnold’s post on compositional patterns - policies).

The IUrlPolicy interface contract is:

  public interface IUrlPolicy
  {
    bool Matches(ActionCall call, IConfigurationObserver log);

    IRouteDefinition Build(ActionCall call);
  }

Being always test driven, I define my tests for the matching process first using Machine.Specifications :

    [Subject(typeof(EnsureControllersCanHaveARootUrlPolicy))]
    public class when_matching_against_a_non_root_action_call
    {
        Establish context = () =>
        {
            _actionCall = ActionCall.For<HomeController>(h => h.FrontPage());
            _log = new Mock<IConfigurationObserver>();
            _matches = true;

            _urlPolicy = new EnsureControllersCanHaveARootUrlPolicy();
        };

        Because of = () => _matches = _urlPolicy.Matches(_actionCall, _log.Object);

        It should_not_match = () => _matches.ShouldBeFalse();

        private static EnsureControllersCanHaveARootUrlPolicy _urlPolicy;
        private static bool _matches;
        private static ActionCall _actionCall;
        private static Mock<IConfigurationObserver> _log;
    }

    [Subject(typeof(EnsureControllersCanHaveARootUrlPolicy))]
    public class when_matching_against_a_root_action_call
    {
        Establish context = () =>
        {
            _actionCall = ActionCall.For<NewsController>(h => h.Root());
            _log = new Mock<IConfigurationObserver>();

            _urlPolicy = new EnsureControllersCanHaveARootUrlPolicy();
        };

        Because of = () => _matches = _urlPolicy.Matches(_actionCall, _log.Object);

        It should_match = () => _matches.ShouldBeTrue();

        private static EnsureControllersCanHaveARootUrlPolicy _urlPolicy;
        private static bool _matches;
        private static ActionCall _actionCall;
        private static Mock<IConfigurationObserver> _log;
    }

So here I’m writing specifications for my new class “EnsureControllersCanHaveARootUrlPolicy” and the first should not match on an action call to the HomeController’s FrontPage method.
The second should match on an action call to a new class, NewsController, and its new Root method that meets my convention.
Here’s the implementation for this:

    public class EnsureControllersCanHaveARootUrlPolicy : IUrlPolicy
    {
        public bool Matches(ActionCall call, IConfigurationObserver log)
        {
            return call.Method.Name.EndsWith("Root");
        }
    }

My next specification is for the building of the route definition when the policy executes:

    [Subject(typeof(EnsureControllersCanHaveARootUrlPolicy))]
    public class when_building_a_route_definition
    {
        Establish context = () =>
        {
            _actionCall = ActionCall.For<NewsController>(h => h.Root());

            _urlPolicy = new EnsureControllersCanHaveARootUrlPolicy();
        };

        Because of = () => 
            _routeDefinition = _urlPolicy.Build(_actionCall);

        It should build_the_route_from_the_prefix_of_the_controller = () =>
            _routeDefinition.Pattern.ShouldEqual("news");

        private static EnsureControllersCanHaveARootUrlPolicy _urlPolicy;
        private static bool _matches;
        private static ActionCall _actionCall;
        private static Mock<IConfigurationObserver> _log;
        private static IRouteDefinition _routeDefinition;
    }

When the route definition is required, I want this policy to make sure that “controller” is stripped from the name of the handling type (in this case NewsController) and that the name is in lower case.

Here’s the full implementation of the policy:

    public class EnsureControllersCanHaveARootUrlPolicy : IUrlPolicy
    {
        public bool Matches(ActionCall call, IConfigurationObserver log)
        {
            return call.Method.Name.EndsWith("Root");
        }

        public IRouteDefinition Build(ActionCall call)
        {            
            var newRoute = call.HandlerType.Name.Replace("Controller", string.Empty).ToLower();

            var routeDefinition = call.ToRouteDefinition();
            routeDefinition.Append(newRoute);
            return routeDefinition;
        }
    }

The next thing to do is to create a new view model in the NewsController and hook it up to my old news spark view that I’m migrating across. Here’s the news controller:

    public class NewsController
    {
        public NewsViewModel Root()
        {
            return new NewsViewModel();
        }
    }

    public class NewsViewModel
    {
    }

In my spark view, I ensure that the page is using this new output view model:

<viewdata model="Xerces.Web.Core.Home.NewsViewModel" />

Last thing to do is declare this policy in my own fubu registry :

    public class XercesFubuRegistry : FubuRegistry
    {
        public XercesFubuRegistry()
        {
            IncludeDiagnostics(true);

            Applies.ToThisAssembly();

            Actions.IncludeClassesSuffixedWithController();

            Routes.HomeIs<HomeController>(c => c.FrontPage())
                .IgnoreControllerNamespaceEntirely()
                .UrlPolicy<EnsureControllersCanHaveARootUrlPolicy>();  <-- new line

            this.UseSpark();

            Views.TryToAttachWithDefaultConventions();

            Output.ToJson.WhenCallMatches(action => action.Returns<AjaxResponse>());
        }
    }

I already have the start url of my web application pointed to the “_fubu/actions” url so that it goes straight to diagnostics, so I now debug the application to examine my new action set up:

urlpolicy

I now have a new action registered for NewsController.Root() that points to my news.spark view and has a route of “news”. In the bottom left corner is the hover on the news link, showing that it points to “/news”.

As you can see it is very easy to customise your MVC application and have it work exactly how you want with the powerful hooks that fubuMVC has provided.
In this convention I had decided to have a HomeController.FrontPage(), NewsController.Root(), AboutController.Root() – this is my personal preference so they are isolated and quite easy to remember that they will generate a root url.

However, let us look at an alternative just to show how customisable these rules are.. what if I had wanted to stick to my original ASP.NET MVC layout where the HomeController had methods called News() and About()?

If I say that my new convention is any method on the HomeController that is not FrontPage() has the method name used as the root url, this should give me the same output.
I would end up with something like:

    public class EnsureHomeControllerCanGenerateRootUrlsPolicy : IUrlPolicy
    {
        public bool Matches(ActionCall call, IConfigurationObserver log)
        {
            return call.HandlerType == typeof(HomeController)
                && call.Method.Name.ToLower() != "frontpage";
        }

        public IRouteDefinition Build(ActionCall call)
        {
            var newRoute = call.Method.Name.ToLower();
            var routeDefinition = call.ToRouteDefinition();
            routeDefinition.Append(newRoute);
            return routeDefinition;
        }
    }

This time I would be using the HandlerType of the action call and if it is the HomeController and the action call method is not the FrontPage method, then it would be eligible to build a route definition and it would use the lowercase name for that.

So that’s how I customised my routes with a url policy – oh and did I say that fubuMVC is great?!



1 comments:

  1. Josh September 1, 2011 at 3:43 PM

    Hey, Justin. Glad to see other people are grokking our IUrlPolicy stuff!

    Of course, the Routes expression build does give you a TON of flexibility but sometimes it's easier to express in an IUrlPolicy and/or the default policy won't handle edge cases.

    I usually recommend my handlers convention for people getting started with Fubu -- it tends to show off the composition abilities of the framework: http://lostechies.com/josharnold/2011/07/26/handlers-a-useful-fubumvc-convention/

    (Available via NuGet as well ;))