When creating resources with the ASP.NET Web API (beta) it's important to be able to create correct hyperlinks (you know, if it doesn't have hyperlinks, it's not REST). These hyperlinks may link to other resources in the same API, so it's important to keep the links consistent. A client following such a link should hit the desired resource.

This post describes an refactoring-safe approach to creating hyperlinks using the Web API RouteCollection and Expressions.

The Problem

Obviously hyperlinks can be hard-coded, but since incoming requests are matched based on the Web API's RouteCollection, there's a risk that hard-coded links become disconnected from the API's incoming routes. In other words, hard-coding links is probably not a good idea.

For reference, the default route in the Web API looks like this:

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

A sample action fitting that route might look like this:

public Artist Get(string id)

where the Get method is defined by the ArtistController class.

Desired Outcome

In order to provide a refactoring-safe way to create links to e.g. the artist resource, the strongly typed Resource Linker approach outlined by José F. Romaniello can be adopted. The IResourceLinker interface looks like this:

public interface IResourceLinker
{
    Uri GetUri<T>(Expression<Action<T>> method);
}

This makes it possible to create links like this:

var artist = new Artist
{
    Name = artistName,
    Links = new[]
    {
        new Link
        {
            Href = this.resourceLinker.GetUri<ArtistController>(r =>
                r.Get(artistsId)).ToString(),
            Rel = "self"
        }
    },
    // More crap goes here...
};

In this example, the resourceLinker field is an injected instance of IResourceLinker.

Since the input to the GetUri method is an Expression, it's being checked at compile time. It's refactoring-safe because a refactoring tool will be able to e.g. change the name of the method call in the Expression if the name of the method changes.

Example Implementation

It's possible to implement IResourceLinker over a Web API RouteCollection. Here's an example implementation:

public class RouteLinker : IResourceLinker
{
    private Uri baseUri;
    private readonly HttpControllerContext ctx;
 
    public RouteLinker(Uri baseUri, HttpControllerContext ctx)
    {
        this.baseUri = baseUri;
        this.ctx = ctx;
    }
 
    public Uri GetUri<T>(Expression<Action<T>> method)
    {
        if (method == null)
            throw new ArgumentNullException("method");
 
        var methodCallExp = method.Body as MethodCallExpression;
        if (methodCallExp == null)
        {
            throw new ArgumentException("The expression's body must be a MethodCallExpression. The code block supplied should invoke a method.\nExample: x => x.Foo().", "method");
        }
 
        var routeValues = methodCallExp.Method.GetParameters()
            .ToDictionary(p => p.Name, p => GetValue(methodCallExp, p));
 
        var controllerName = methodCallExp.Method.ReflectedType.Name
            .ToLowerInvariant().Replace("controller", "");
        routeValues.Add("controller", controllerName);
 
        var relativeUri = this.ctx.Url.Route("DefaultApi", routeValues);
        return new Uri(this.baseUri, relativeUri);
    }
 
    private static object GetValue(MethodCallExpression methodCallExp,
        ParameterInfo p)
    {
        var arg = methodCallExp.Arguments[p.Position];
        var lambda = Expression.Lambda(arg);
        return lambda.Compile().DynamicInvoke().ToString();
    }
}

This isn't much different from José F. Romaniello's example, apart from the fact that it creates a dictionary of route values and then uses the UrlHelper.Route method to create a relative URI.

Please not that this is just an example implementation. For instance, the call to the Route method supplies the hard-coded string "DefaultApi" to indicate which route (from Global.asax) to use. I'll leave it as an exercise for the interested reader to provide a generalization of this implementation.


Comments

This is very helpful! I'm currently exploring a solution that injects links into representations via filter based on some sort of state machine for the resource, though I might be leading myself down a DSL path.
2012-04-17 23:07 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or Google Plus, or somewhere else with a permalink. Ping me with the link, and I may add it as a comment.

Published

Tuesday, 17 April 2012 14:46:42 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!