Recently I blogged about WCF REST services with no svc file and no config. In this post I also discussed the pros/cons of WCF services as compared to using MVC controller actions for web services and I made the case that, in many instances, WCF REST services is better than using the MVC infrastructure because WCF provides:
- a more RESTful API with less work
- a convenient automatic help page to assist consumers of your service
- automatic format selection (i.e., xml/json) depending on HTTP headers
In any case, using both WCF REST services and MVC-style web services are both quite convenient – and you don't have to choose. You can use both. In fact, you can use both inside the same web application project. However, a recent question on StackOverflow illustrates that you have to be careful how you set up your routing in order to make WCF REST services work inside MVC applications.
If you have a look at the question on StackOverflow, you'll see the routes were originally set up like this:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.Add(new ServiceRoute("Person", new WebServiceHostFactory(), typeof(PersonService))); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults );
The service route was for the WCF Service (using no *.svc file) and the last route was the standard MVC route. In most of the cases this works fine. Navigation to the root correctly lands on the HomeController's Index method. Navigation to "/Person/help" correctly goes to the WCF REST help page and the WCF REST service can be invoked just fine. However, the action links were not correct. When formulating a simple action link like this:
<%: Html.ActionLink("Home", "Index", "Home")%>
The resulting URL being produced was: "/Person?action=Index&controller=Home" which was clearly not correct. It's hitting the first route and thinking it should just put "Person" for the first segment. It then cannot match the "controller" or "action" tokens so it just puts them in the query string. It will attempt to go to the service but the URI is incorrect so, if it's a WCF REST service, you'll just get the default help page.
Of course, we know that the order matters for routing so what if we put the MVC route first and the service route second? In this case, we run into a different problem. If the first URI segment is "Person" then it will try to match that as the controller name and you'll get a 404 or, if you're using a controller factory, your IoC container won't be able to find a controller named "PersonController".
So how can we get the WCF REST service route and the MVC routes to play nice with each other? The simple answer: use a route contraint:
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults new { controller = "^(?!Person).*" } ); routes.Add(new ServiceRoute("Person", new WebServiceHostFactory(), typeof(PersonService)));
Notice on line #5 above, we add a route constraint so that it will try to use the MVC route first as long as the first segment (where the "controller" token is positioned) is not the "Person" string which matches where we want our WCF REST service to be. If it is "Person", then it will fall through to the service route and correctly point to our WCF REST service.
One other interesting note when using service routes in MVC applications – anytime you're going beyond the default MVC routes, I highly recommend you unit test your routes. This can be done very easily with the TestHelper that comes with MvcContrib. It makes it trivial to unit test routes like this:
[TestMethod] public void default_path_should_be_able_to_match_controller() { "~/".Route().ShouldMapTo<HomeController>(); }
The unfortunate caveat here is that you cannot use this when you're combining service routes. If you do, you'll get this exception when you initialize your routes at the start of the unit test by invoking the static RegisterRoutes() method: "System.InvalidOperationException: System.InvalidOperationException: 'ServiceHostingEnvironment.EnsureServiceAvailable' cannot be invoked within the current hosting environment. This API requires that the calling application be hosted in IIS or WAS." Fortunately, we can still use Phil Haack's Route Debugger:
With the URL of "/Person/23", it correctly matches my service route for my WCF service.
WCF REST and MVC are both great and they should be used together when appropriate.