1

Adding links to resource representations is really easy to do within a Web API controller. Using the controller's Url property (which is of UrlHelper class) you can build the href of a link using route names, like this:

var href = Url.Link("RouteName", new { id = 123 });

The value of href is computed based on route configuration - in this case something like http://example.org/api/products/123. This is great because I can adjust route configurations without having to update all the places hrefs are generated. All links emitted conform to the new configurations.

But can this, or any similar route-based technique, be used to create templated hrefs? For example, to create a link from some resource to an arbitrary product using HAL's templated link property:

"_links": {
  "self": {
    "href": "http://example.org/api/productfinder"
  },
  "product": {
    "href": "http://example.org/api/products/{productId}",
    "templated": true
  }  
}

(I don't really have a resource called productfinder - just a contrived example for this question).

I tried setting the routeValues parameter to a string of the template, e.g.

var href = Url.Link("RouteName", new { id = "{productId}" });

This produces a URL-encoded string: http://example.org/api/products/%7BproductId%7D. I could use .Replace, but this approach fails for another reason. If the route configuration has type constraints e.g. {id:int} then the href becomes null.

I tried passing null to routeValues hoping I could then just append the template portion. Link() returned null, so href ended up being just the template portion. Plus this wouldn't work for complex routes like api/{this}/something/{that}{?q}

So I'm stuck. Can I use routes to generate templated hrefs? If so, how?

biscuit314
  • 2,384
  • 2
  • 21
  • 29

1 Answers1

2

Okay, it took me a while, but I've got something that works. The trick is to lookup the RouteTemplate from the configuration's route table. Each controller has a reference to the configuration. From there it was pretty easy.

I created an extension class to ApiController. In practice I will probably subclass ApiController but the idea is the same. Also, I may return the Uri object instead of using .ToString().

public static class ApiControllerExtensions
{
    public static string GetTemplatedHref(this ApiController controller, string routeName, string controllerName = "")
    {
        var routeTemplate = controller.Configuration.Routes[routeName].RouteTemplate;

        var baseUri = new Uri(controller.Request.RequestUri.GetLeftPart(UriPartial.Authority));
        var templatedHref = new Uri(baseUri, routeTemplate).ToString();

        return string.IsNullOrEmpty(controllerName) ? 
            templatedHref : 
            templatedHref.Replace("{controller}", controllerName.ToLower());
    }
}

Now, when inside a controller's method and I need a templated href I can do this:

var href = this.GetTemplatedHref("RouteName");

This works well with route attributes. It also works for pre-configured routes, e.g.:

config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

So I can supply the controller name and get the templated href like this:

var href = this.GetTemplatedHref("DefaultApi", "product");

Again in practice I'll modify and tweak, e.g. add options for absolute/relative, etc. But the core problem of getting templates using route names is now solved.

biscuit314
  • 2,384
  • 2
  • 21
  • 29