Using ServiceRoute With Existing MVC Routes

I’m sitting here at TechEd working on my demos for tomorrow’s presentation and I briefly got stuck on my route implementation.  The specific issues wasn’t anything big – I just had a more specific route path declared below a more general one, so my specific route was never matching.  but it did remind me of this post that I was planning to write about getting MVC routes and WCF service routes to play together in the same application.  This came out a recent experience working with a customer to troubleshoot some issues that he was having on our Web API bits.

The code that was the focus of investigation registered a default MVC route followed by a service route.

public static void RegisterRoutes(RouteCollection routes)
{ 
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 
  routes.MapRoute( 
    "Default", // Route name 
    "{controller}/{action}/{id}", // URL with parameters 
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
  ); 
  routes.MapServiceRoute("searchservice"); 
}

The default route was used for all of the basic UI workflows, and the service route was for, well, the WCF service.  Additionally, the service was called from script using the following jquery:

$.post("/searchservice", jsondata, iis.searchproducts.raisecallback, "json"); 

Now, looking at this, you may be thinking, “well of course there’s a problem – the default route is going to match all inbound URLs and try to fit them into it’s controller/action/id taxonomy” – and you would be absolutely right.  For inbound requests, the routing engine matches {host}/searchservice to the default route and fails when it tries to find a controller named searchservice.

So then if that’s the only problem, we should be able to fix it by simply moving the registration code for searchservice above the default route registration rule, right?  Well, not quite.  As you likely know, the URL routing engine is used to process inbound URLs as well as to generate URLs on the outbound side.  This shows up immediately if you change the order of the route registrations in the following view code:

@Html.ActionLink("Edit Quote", "Edit", 
  "Quotes", new { id = 5 }, null) 

While our search service will still work correctly because it is given the first pass at inbound URLs, we’ll never actually get to the form that uses the service because the action link to the form will be generated as follows:

http://localhost/searchservice?action=Edit&controller=Quotes&id=5

As such, we’ve got a bit of a catch 22 –

  1. If you put your service route above your default route, inbound route matching works fine (remember that the routing engine iterates the route collection from top to bottom when matching) because the service route pattern ends up looking like “[your prefix]/{*pathInfo} – however, service route will  also match on the outbound side – creates a problem when generating links.  It’s also worth nothing that adding an ignore for searchservice doesn’t help because it blocks you on both inbound and outbound.
  2. If you put your service route below your default route, your inbound routing will fail when you try and call the service because the default route is trying to find a controller named searchservice.

The solution is to place service route below the default route and apply a custom route constraint on the mvc route to exclude routes that contain the name of your service prefix.  The route constraint I used was simply the inverse of Guy Burstein’s custom route constraint.

public class NotInValuesConstraint : IRouteConstraint 
{ 
  public NotInValuesConstraint(params string[] values) { 
    _values = values; 
  } 

  private string[] _values; 

  public bool Match(HttpContextBase httpContext, 
    Route route, 
    string parameterName, 
    RouteValueDictionary values, 
    RouteDirection routeDirection) { 
      string value = values[parameterName].ToString(); 
      return !_values.Contains(value, StringComparer.CurrentCultureIgnoreCase); 
  } 
} 

Including this constraint on my default route enables it to match everything except for requests starting with ‘searchservice’.  This makes both inbound and outbound handling of URLs work.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        , new { controller =  new NotInValuesConstraint(new[]{"searchservice"}) } 
    );

    routes.MapServiceRoute<ProductsResource>("searchservice");
}

About Howard Dierking

I like technology...a lot...
This entry was posted in HTTP, WCF, Web. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Sriwantha Attanayake

    RouteCollection does not have a method called MapServiceRoute() where is it coming from?

  • Robert

    Turns out that it works excellent with WebSockets as ServiceRoute in MVC 4. Thank you for this post. It made my day.

  • http://twitter.com/denisndwiga Denis Ndwiga

    hi,
    How can i get the services {wcf} that i have defined using the above technique to run under medium trust level 

  • http://twitter.com/denisndwiga Denis Ndwiga

    This thing really got me thinking. thanks alot

  • Anonymous

    @f22dbd11d9da2f4d8bc8c6a80390a363:disqus Great catches – this is the problem when I “code” in WLW :)  Fortunately, the code that I had sent to the customer was consistent with the things you caught.

    Also, I recently learned of another possible fix for this problem from Phil Haack that doesn’t require the custom route constraint.  I need to verify in this sample and will write a follow up post.

  • Dennis

    One more thing… I think you need to also move the routes.MapServiceRoute below your default route, right?

    Thanks, Dennis

  • Dennis

    Howard – thanks for the tip.  I tried it and it worked, but the constraint is case-sensitive, so it might be better to change the comparison to _values.Contains(value, StringComparer.CurrentCultureIgnoreCase)?

  • http://www.facebook.com/people/David-Andrews/38418527 David Andrews

    Funny. I just watched your webinar on msdn and was trying to do the opposite: add a wcf route to an existing MVC app :) .. or the other way around. lol. Either way, great presentation.