Untyped F# HTTP route defaults for ASP.NET Web API by Mark Seemann
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#.