Search code examples
c#restgrapevine

How do I write a regular expression to route traffic with Grapevine when my request URL has a query string?


I'm using Grapevine and I want to route request traffic that has a query string, and I don't know regular expressions well enough to figure out why it's not working.

e.g.

http://localhost:1234/service/function?param1=1&param2=2

And I defined my RESTRoute like this:

[RESTRoute(Method = HttpMethod.GET, PathInfo = @"^/service/function\?\D+$")]
public void HandleFooRequestString(HttpListenerContext context)
{
    PrintRequest(context);
    this.SendTextResponse(context, "foo is a success!");
}

But traffic sent to the URL isn't going to that method. What am I doing wrong?


Solution

  • This is a pretty common question, and has spawned some changes to the Grapevine road map.

    Grapevine 3.1.0 +

    In the most recent version of Grapevine, the query string is stripped off of the URL prior to regular expression pattern matching, so this is no longer a problem.

    Additionally, routing is being updated in version 4 to match the Node/Express routing mechanism, so you can opt to not use regular expressions at all if you prefer. However, this version is still in the planning stages.

    Grapevine 3.0.x

    While it certainly isn't uncommon for GET requests to contain query string parameters, let's first make sure we are using proper URI design. To quote from O'Reilly's RESTful Web Services, page 233, under the header URI Design (emphasis mine):

    When designing URIs, use path variables to separate elements of a hierarchy, or a path through a directed graph. Example: /weblogs/myweblog/entries/100 goes from the general to the specific. From a list of weblogs, to a particular weblog, to the entries in that weblog, to a particular entry. Each path variable is in some sense "inside" the previous one.

    Use punctuation characters to separate multiple pieces of data at the same level of hierarchy. Use commas when the order of the items matters, as it does in latitude and longitude: /Earth/37.0,-95.2. Use semicolons when the order doesn't matter: /color-blends/red;blue.

    Use query variables only to suggest arguments being plugged into an algorithm, or when the other two techniques fail. If two URIs differ only in their query variables, it implies that they're the different sets of inputs into the same underlying algorithm.

    The big takeaway here is that our URIs, generally speaking, should only use the query string to pass arguments to an algorithm. If, indeed, we are expecting query string parameters in our methods, those should be validated by the method the request is routed to, and our PathInfo regular expressions should reflect the possibility of receiving such arguments.

    Example: When not to use query string

    Suppose you wanted to request user data given a specific numeric user id, let's say 632. This is a scenario where it might be tempting to use the query string, but it would be better not to.

    • Less Correct URI: /user?id=632
    • More Correct URI: /user/632

    The RESTRoute for the more-correct URI would then look like this:

    [RESTRoute(Method = HttpMethod.GET, PathInfo = @"^/user/\d+$")]
    public void GetUser(HttpListenerContext context)
    {
        var userid = context.RawUrl.GrabFirst(@"^/user/(\d+)$");
        // you got your user id, do something
    }
    

    Example: Correctly using the query string

    If you wanted to create a REST route that multiplied two integers together, then - setting aside that URI are suppose to represent resources, not operations on a resource - using a query string might be more suitable.

    • Probably Less Correct URI: /2/product/3
    • Probably More Correct URI: /product?x=2&y=3

    The RESTRoute for the probably-more-correct URI would then look like this:

    [RESTRoute(Method = HttpMethod.GET, PathInfo = @"^/product")]
    public void MultiplyTwoIntegers(HttpListenerContext context)
    {
        var x = context.Request.QueryString["x"];
        var y = context.Request.QueryString["y"];
    
        // Verify the inputs and do the math.
    }
    

    Note that the PathInfo regular expression omits the trailing $, which would normally indicate the end of the string, and we let the route method handle getting the parameters passed. If we really wanted to be a stickler, we could also write it like this:

    [RESTRoute(Method = HttpMethod.GET, PathInfo = @"^/product\?.+$")]
    

    Which would ensure that it at least looked like there might be some parameters coming in the query string, but isn't really necessary since we are going to do that checking anyway.