Monday, 4 May 2015

Restival Part 2: All Aboard The Routemaster

Hello, and welcome to the second instalment of Restival: The Great .NET ReST Showdown (part 1 is here if you missed it)  So far, our API defines a single very simple method – “hello”. Making a GET request to /hello will return { "Message" : "Hello, World!" }, and making a GET request to /hello/chris will return { "Message" : "Hello, Chris!" }

The code we're discussing here is on GitHub as release v0.0.1. This release supports /hello/{name}, which demonstrates routing and parameter binding. I've deliberately not implemented "Hello, World" at /hello yet,   because I want to do that by using the various frameworks' conventions for specifying default parameter values and that logically can't happen until you've defined your routes. Even at this incredibly early stage, there's some interesting design decisions going on.

Routing and Route Binding

Routing controls how your app will map incoming HTTP requests to methods - it's the set of rules that say "when you get a request that looks like X, run method Y on class Z"

Nancy has a really lightweight routing syntax inspired by Sinatra - by inheriting from NancyModule, you get access to a RouteBuilder, a sort of dictionary that maps routes to anonymous methods, for each supported HTTP verb (DELETE, GET, HEAD, OPTIONS, POST, PUT and PATCH) - to add a route, you supply the pattern to match and the method implementation:

public class HelloModule : NancyModule {
    public HelloModule() {
        Get["/hello/{name}"] = parameters => new Greeting(parameters.name);
    }
}

Note the Nancy convention whereby we use an underscore to indicate "we're not using this variable for anything" in handlers that don't actually use their parameter dictionary. It's also worth noting that Nancy's lightweight syntax won't stop you defining multiple handlers for the same route - but this can lead to non-deterministic behaviour, so don't do it :)

WebAPI uses an explicit routing table that's configured during application startup - in WebAPI, there's a call to WebApiConfig.Register(GlobalConfiguration.Configuration) in Application_Start, and routes are mapped by specifying the name, the URL pattern and the defaults to use for that route. (If you're familiar with routing in ASP.NET MVC, WebAPI uses a very similar routing configuration, but with the 'action' mapped to the HTTP verb instead of to a path segment.)

config.Routes.MapHttpRoute(
    "Hello",   // route name
    "hello/{name}", // route template
    new { Controller = "Hello" } // route defaults
);

OpenRasta and ServiceStack are both far more explicit about the relationship between resources, routes and handlers. OpenRasta uses a fluent configuration interface to declare your resources (i.e. the bits of data we're interested in), your URIs (routes), handlers (the bits of code that actually do the work), and contracts (which handle things like serialization and content types)

public class Configuration : IConfigurationSource {
    public void Configure() {
        using (OpenRastaConfiguration.Manual) {
            ResourceSpace.Has.ResourcesOfType<Greeting>()
                AtUri("/hello/{name}")
                .HandledBy<HelloHandler>()
                .AsJsonDataContract();
        }
    }
}

Finally, ServiceStack requires you to explicitly define requests (DTOs representing the incoming request data), services (analogous to handlers in our other frameworks) and responses. This is far more verbose than the other frameworks, but providing these abstraction layers between every aspect of your ReST API and your underlying codebase gives you more flexibility to evolve your API independently of the underlying business logic. You map your routes to your request DTOs using the Route attribute, and inherit from ServiceStack.Service when implementing your handlers. ServiceStack maps HTTP verbs onto service method names - HelloService.Get(Hello dto), HelloService.Post(Hello dto), etc. - but also supports a catch-all Any() method which will map incoming requests regardless of the request verb.

[Route("/hello")]
[Route("/hello/{name}")]
public class Hello {
    public string Name { get; set; }
}

public class HelloResponse {
    public string Message { get; set; }
}

public class HelloService : Service {
  public HelloResponse Any(Hello dto) {
    var greeting = new Greeting(dto.Name);
    var response = new HelloResponse() { Message = greeting.Message };
    return (response);
  }
}

So there you go. /hello/{name} takes one line in NancyFX, a couple of lines in OpenRasta and WebAPI, and three entire classes in ServiceStack. Before you draw any conclusions, though, try pointing a browser at the root URL of each API implementation.

Nancy gives you this rather splendid 404 page - complete with Tumbeast:

image

Running under IIS, WebAPI and OpenRasta both interpret GET / as a directory browse request, and give you the all-too-familiar IIS 7.5 HTTP error screen:

image

But the pay-off for the extra boilerplate required by ServiceStack is this rather nice API documentation page, describing the services and encoding formats supported by the API and providing WSDL files for adding our API as a service endpoint. Now, we're not actually using any of that yet... but as our API grows, it's going to be interesting to see how much extra work the other frameworks require to do things that ServiceStack provides for free. (Or for $800 per developer, depending on what you're doing with it.)

image

Now, it's important to remember that we're trying to reflect the conventions and idioms of our chosen frameworks here. You could, without too much difficulty, implement the request/service/response pattern favoured by ServiceStack on any of the other frameworks, or to get your ServiceStack services to return raw entities instead of mapping them into Response objects - but if you're trying to make framework A behave like framework B, you might as well just switch to framework B and be done with it.

In the next episode, we're going to make GET /hello return "Hello, World!", and in the process look at how to define default values for our route parameters in each of our frameworks. Until then, happy hacking!

No comments: