Untyped F# HTTP route defaults for ASP.NET Web API

Datetime:2016-08-22 22:00:10          Topic: F#           Share

In ASP.NET Web API, route defaults can be provided by a dictionary in F#.

When you define a route in ASP.NET Web API 2, you most likely use the MapHttpRoute overload where you have to supply default values for the route template:

public static IHttpRoute MapHttpRoute(
    this HttpRouteCollection routes,
    string name,
    string routeTemplate,
    object defaults)

The defaults arguments has the type object , but while the compiler will allow you to put any value here, the implicit intent is that in C#, you should pass an anonymous object with the route defaults. A standard route looks like this:

configuration.Routes.MapHttpRoute(
    "DefaultAPI",
    "{controller}/{id}",
    new { Controller = "Home", Id = RouteParameter.Optional });

Notice how the name of the properties ( Controller and Id ) (case-insensitively) match the place-holders in the route template ( {controller} and {id} ).

While it's not clear from the type of the argument that this is what you're supposed to do, once you've learned it, it's easy enough to do, and rarely causes problems in C#.

Flexibility

You can debate the soundness of this API design, but as far as I can tell, it attempts to strike a balance between flexibility and syntax easy on the eyes. It does, for example, enable you to define a list of routes like this:

configuration.Routes.MapHttpRoute(
    "AvailabilityYear",
    "availability/{year}",
    new { Controller = "Availability" });
configuration.Routes.MapHttpRoute(
    "AvailabilityMonth",
    "availability/{year}/{month}",
    new { Controller = "Availability" });
configuration.Routes.MapHttpRoute(
    "AvailabilityDay",
    "availability/{year}/{month}/{day}",
    new { Controller = "Availability" });
configuration.Routes.MapHttpRoute(
    "DefaultAPI",
    "{controller}/{id}",
    new { Controller = "Home", Id = RouteParameter.Optional });

In this example, there are three alternative routes to an availability resource, keyed on either an entire year, a month, or a single date. Since the route templates (e.g. availability/{year}/{month} ) don't specify an id place-holder, there's no reason to provide a default value for it. On the other hand, it would have been possible to define defaults for the custom place-holders year , month , or day , if you had so desired. In this example, however, there are no defaults for these place-holders, so if values aren't provided, none of the availability routes are matched, and the request falls through to the DefaultAPI route.

Since you can supply an anonymous object in C#, you can give it any property you'd like, and the code will still compile. There's no type safety involved, but using an anonymous object enables you to use a compact syntax.

Route defaults in F#

The API design of the MapHttpRoute method seems forged with C# in mind. I don't know how it works in Visual Basic .NET, but in F# there are no anonymous objects. How do you supply route defaults, then?

As I described in my article on creating an F# Web API project , you can define a record type:

type HttpRouteDefaults = { Controller : string; Id : obj }

You can use it like this:

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    "DefaultAPI",
    "{controller}/{id}",
    { Controller = "Home"; Id = RouteParameter.Optional }) |> ignore

That works fine for DefaultAPI , but it's hardly flexible. You must supply both a Controller and a Id value. If you need to define routes like the availability routes above, you can't use this HttpRouteDefaults type, because you can't omit the Id value.

While defining another record type is only a one-liner, you're confronted with the problem of naming these types.

In C#, the use of anonymous objects is, despite appearances, an untyped approach. Could something similar be possible with F#?

It turns out that the MapHttpRoute also works if you pass it an IDictionary<string, object> , which is possible in F#:

config.Routes.MapHttpRoute(
    "DefaultAPI",
    "{controller}/{id}",
    dict [
        ("Controller", box "Home")
        ("Id", box RouteParameter.Optional)]) |> ignore

While this looks more verbose than the previous alternative, it's more flexible. It's also stringly typed , which normally isn't an endorsement, but in this case is honest , because it's as strongly typed as the MapHttpRoute method. Explicit is better than implicit .

The complete route configuration corresponding to the above example would look like this:

config.Routes.MapHttpRoute(
    "AvailabilityYear",
    "availability/{year}",
    dict [("Controller", box "Availability")]) |> ignore
config.Routes.MapHttpRoute(
    "AvailabilityMonth",
    "availability/{year}/{month}",
    dict [("Controller", box "Availability")]) |> ignore
config.Routes.MapHttpRoute(
    "AvailabilityDay",
    "availability/{year}/{month}/{day}",
    dict [("Controller", box "Availability")]) |> ignore
config.Routes.MapHttpRoute(
    "DefaultAPI",
    "{controller}/{id}",
    dict [
        ("Controller", box "Home")
        ("Id", box RouteParameter.Optional)]) |> ignore

If you're interested in learning more about developing ASP.NET Web API services in F#, watch my Pluralsight course A Functional Architecture with F# .





About List