The goal of this post
It took me some time to figure out how to have WCF services running inside an MVC website so I want to show you that but at the same time I want to show a smart way you can use Linq to remove some unnecessary code and at the same time make the code more readable and clear.
The background story
I recently had to create a route constraint for my MVC code because of wanting to add some internal REST service for getting products via the WCF REST template. This does not work by default for the following reason, MVC needs a default route and the default route overrides the service route because it is too generic (catch-all) but let’s back up a little. First of all the way MVC routing works is internally it doesn’t work out of the box so you need to tell MVC how it should route incoming requests. Fortunately the MVC templates are setup with the basic (catch-all) routes needed to get something running quickly. Unfortunately I it doesn’t come with any advanced routes default and no constraints but we’ll get to that next.
The problem
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("*.ico");
routes.IgnoreRoute("*.js");
routes.MapRoute("Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }); // Parameter defaults
}
That is pretty much what ships with MVC. I did add two more routes one for my Product Service and one for my Registration Service. In .NET 4.0 this is done easily like below
routes.Add( new ServiceRoute("Register",
new WebServiceHostFactory(),
typeof(RegisterService)));
routes.Add( new ServiceRoute("Product",
new WebServiceHostFactory(),
typeof(ProductService)));
The service routes should be added last, after the “Default Route” otherwise it could break SEO. MVC routing would end up looking like
http://site.com/default?controller=somecontroller&action=someaction
At this point though routing to the actual service doesn’t work. Your controller factory tries to find a controller because the catch all (default) route will be run before the service routes and it can’t find the controller for product or register.
The solution
We need to exclude our services from the default route. To do this and maintain some flexibility I chose to create a custom constraint. I found some solid guidance over on Stephen Walters blog http://stephenwalther.com/blog/archive/2008/08/07/asp-net-mvc-tip-30-create-custom-route-constraints.aspx
Stephen has created a NotEqualConstraint that I want to use but Stephen considered only one constraint and I want multiple since I have multiple WCF services running. I modified Stephens code a little and ended up with the following class.
public class NotEqualConstraint : IRouteConstraint
{
private readonly string[] _match;
public NotEqualConstraint(params string[] match)
{
_match = match;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
var exists = false;
foreach (var s in _match) {
exists = String.Compare(values[parameterName].ToString(), s, true) != 0;
if (exists) {
break;
}
}
return exists;
}
}
Now I can switch the default route from the standard MVC example to the below code making use of an override of the MapRoute function.
routes.MapRoute("Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new { controller = new NotEqual("Register", "Product") }); // Constraints
Code works (what a bliss) but I am not too happy about the looping. It’s not clean enough for my taste.
The bonus
This basically has nothing to do with the post itself but is just a general tip on how to make good use of Linq2Objects. The above code can be simplified to the following using the Linq extension method Any.
public class NotEqualConstraint : IRouteConstraint
{
private readonly string[] _match;
public NotEqualConstraint(params string[] match)
{
_match = match;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return _match.Any(s => String.Compare(values[parameterName].ToString(), s, true) != 0);
}
}
Footnote
Some people take great pride in their many lines of code. I am the opposite and certainly so when it makes the code even cleaner.