19

I have a WCF REST 4.0 project based on the the WCF REST Service Template 40(CS). I'd like to expose simple service endpoint URLs without trailing slashes. For example:

  1. CarService.cs
  2. TruckService.cs

I look at the above URLs as resource requests (not directories), which is why I don't think trailing slashes are appropriate here.

Unfortunately, I can't seem to get the behavior I want because I am always redirected to /cars/ and /trucks/ with a trailing slash.

Here's how I've defined the "cars" route and service method - note that I have not included any slashes in any of the route or URI template definitions:

// Global.asax.cs
RouteTable.Routes.Add(new ServiceRoute("cars", new WebServiceHostFactory(), typeof(CarService)));

// CarService.cs
[WebGet(UriTemplate = "")]
public List<Car> GetCollection()
{
    return DataContext.GetAllCars();
}

Note that MVC does not work this way. With the MapRoute method I can route requests directly to http://www.domain.com/about without a redirect to /about/. How can I get the same behavior in WCF REST 4.0?

Will
  • 375
  • 1
  • 4
  • 13

7 Answers7

9

The primary issue that you're running into is that the current version of WCF REST causes a 307 redirect (to the "/") when you have an empty string for the UriTemplate in your WebGet attribute. As far as I know, there is no getting around this in the current version.

However, there are a couple of "middle ground" solution to your problem given that you want a solution that 1) allows you to differentiate services, and 2) have (relatively) short URIs.

First Solution You can put this in your global.asax file (per this example). You can do a service route for each service:

RouteTable.Routes.Add(new ServiceRoute("cars", new WebServiceHostFactory(), typeof(CarService)));
RouteTable.Routes.Add(new ServiceRoute("trucks", new WebServiceHostFactory(), typeof(TruckService)));

At this point you can populate your UriTemplate in each service:

[WebGet(UriTemplate = "all")]
CarPool GetAllCars();

[WebGet(UriTemplate = "{carName}")]
Car GetCar(string carName);

This will allow you URIs of:

www.domain.com/cars/all
www.domain.com/cars/123 or www.domain.com/cars/honda

similarly for trucks:

www.domain.com/trucks/all
www.domain.com/trucks/123 or www.domain.com/trucks/ford

Second Solution Use the service host from the REST Starter Kit (i.e., the WebServiceHost2Factory).

RouteTable.Routes.Add(new ServiceRoute("cars", new WebServiceHost2Factory(), typeof(CarService)));

This does not result in a 307 redirect when using the URIs that you're attempting to use above and thus, gives you exactly what you need. Though I realize that feels a little weird using that service host factory rather than the one that ships with WCF 4.

Steve Michelotti
  • 5,223
  • 1
  • 25
  • 30
  • 2
    Thanks for the info, Steve. I'd considered using your "all" suggestion already (it's a decent workaround), but I'm reluctant to give up on the canonical REST syntax I originally hoped for. I'm looking into the differences between the shipped WebServiceHostFactory and WebServiceHost2Factory, and what it is about the latter one that prevents the 307 redirect. If it's a minor change that I can grok, I'll go with that approach. – Will Oct 20 '10 at 04:50
  • Actually, I just tried using WebServiceHost2Factory and get the same redirect result. – Will Oct 20 '10 at 07:11
  • Oops - strike my second solution - you are correct in that the WebServiceHost2Factory results in the same behavior. (when I tested it, I did not have an empty UriTemplate). But the first solution will still work. – Steve Michelotti Oct 20 '10 at 11:24
  • @SteveMichelotti but this raise a problem, can you point out a soultion for this [Overriding existing Routes](http://stackoverflow.com/questions/17588458/serviceroute-overriding-existing-routes) . i am facing the same situation as this – Andrew Jul 11 '13 at 13:01
3

Try putting this in the Global.asax.cs

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        string rawUrl = HttpContext.Current.Request.RawUrl.ToLower();

        if (rawUrl.EndsWith("/cars"))
        {
            HttpContext.Current.RewritePath(rawUrl + "/");  // append trailing slash
        }
    }
Roman Y
  • 117
  • 1
  • 11
3

You need a UriTemplate, Try something like this:

 [ServiceContract()]
 public interface ICarService
 {

     [OperationContract]
     [WebGet(UriTemplate = "/Car")]
     CarPool GetAllCars();

     [OperationContract]
     [WebGet(UriTemplate = "/Car/{carName}")]
     Car GetCar(string carName);

 }
Shiraz Bhaiji
  • 64,065
  • 34
  • 143
  • 252
  • 1
    Yes, but I want both a CarService and a TruckService, so I would have to add two routes in Global.asax.cs, right? And to differentiate between them, the URLs would be very verbose: e.g. http://www.domain.com/carservice/cars and http://www.domain.com/truckservice/trucks. I'd prefer to have multiple service classes *and* short URLs: e.g. http://www.domain.com/cars and http://www.domain.com/trucks – Will Oct 19 '10 at 21:06
  • 1
    You are correct that I can have slashless URLs if I was willing to list all my service methods in a *single* service class, however. But that could be hundreds of methods for a large API. – Will Oct 19 '10 at 21:10
2

Older question but here's how I solved the problem with a WCF4 REST service (using the RouteTable in Global.asax to add ServiceRoutes). IIS7 is configured so that by the time the service is invoked I have an empty relative path so the handling method's UriTemplate is empty like Will's Car example. I used a rewrite rule in the service's web.config file to add a "/" if needed. It always matches the path then checks the original URI ({REQUEST_URI}) to see if it contains a path without a trailing "/".

    <rewrite>
        <rules>
            <!--
            This rule will append a "/" after "/car" if
            the client makes a request without a trailing "/".
            ASP however must have a trailing "/" to find
            the right handler.
            -->
            <rule name="FixCarPath" stopProcessing="true">
                <match url=".*" />
                <conditions>
                    <add input="{REQUEST_URI}" pattern="/car\?" />
                </conditions>
                <action type="Rewrite" url="{PATH_INFO}/" />
            </rule>
        </rules>
    </rewrite>
2

A bit more reusable:

public class Global : NinjectHttpApplication
{

    protected override void OnApplicationStarted()
    {
        base.OnApplicationStarted();
        RegisterRoutes();
    }

    private void RegisterRoutes()
    {
        RouteTable.Routes.Add(new ServiceRoute("login", new NinjectWebServiceHostFactory(), typeof(LoginService)));
        RouteTable.Routes.Add(new ServiceRoute("incidents", new NinjectWebServiceHostFactory(), typeof(IncidentService)));
        SetRoutePrefixes();
    }
    //This is a workaround for WCF forcing you to end with "/" if you dont have a urlTemplate and redirecting if you dont have
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        string rawUrl = HttpContext.Current.Request.RawUrl.ToLower();


        if (_routePrefixes.Any(rawUrl.EndsWith))
        {
            HttpContext.Current.RewritePath(rawUrl + "/");  // append trailing slash
        }
    }


    private static List<string> _routePrefixes; 
    private static void SetRoutePrefixes()
    {
        _routePrefixes = new List<string>();
        foreach (var route in RouteTable.Routes)
        {
            var r = route as Route;
            var routePrefix = r.Url.Split('/').First();
            _routePrefixes.Add(routePrefix);
        }
    }
Rui Jarimba
  • 11,166
  • 11
  • 56
  • 86
2

I was dealing with this exact problem and ran across this snippet in the MS online docs:

By default, routing does not handle requests that map to an existing physical file on the Web server. For example, a request for http://server/application/Products/Beverages/Coffee.aspx is not handled by routing if a physical file exists at Products/Beverages/Coffee.aspx. Routing does not handle the request even if it matches a defined pattern, such as {controller}/{action}/{id}.

I realized that my route pattern matched the directory my service was hosted in. It appears that a directory is treated the same as a physical file, and route patterns that match a directory are ignored as well. So following the documentation, I set the RouteExistingFiles property to "true" on the RouteCollection. My service now seems to be routing the requests correctly and I've been able to keep the REST syntax that I love so very very much.

jwill212
  • 320
  • 4
  • 3
1

Try changing your code in the Global.asax from...

Routes.Add(new ServiceRoute("cars", new WebServiceHostFactory(), typeof(CarService))); RouteTable.Routes.Add(new ServiceRoute("trucks", new WebServiceHostFactory(), typeof(TruckService)));

...to...

WebServiceHostFactory factory = new WebServiceHostFactory();

Routes.Add(new ServiceRoute("cars", factory, typeof(CarService))); RouteTable.Routes.Add(new ServiceRoute("trucks", factory, typeof(TruckService)));

DanC
  • 51
  • 1