As we were working through the design of the UriTemplate API, we had three broad classes of scenarios in mind. One class involves creating URI's that conform to a common pattern, which our implementation supports via the BindByName and BindByPosition methods. You use these methods when you need to create the URI for a new request or build a URI that someone else will later dereference via an embedded link. I blogged some sample code that shows how this works in a previous post.
A second, more interesting class of scenarios involves determining whether a specific URI matches a given pattern. If the URI matches the pattern, we can treat the URI as structured data and parse it according to the pattern which describes its structure. This functionality is implemented by the Match and MatchSingle API's on System.UriTemplate.
Addressing the second class of scenarios (which involve a single URI and a single pattern at a time) opens the door to the third class of scenarios, which involves matching a candidate URI against a set of templates and taking some arbitrary action based on which subset of the templates matched the candidate URI. This is implemented in the System.UriTemplateTable class which forms the basis of our URI dispatch engine, the discussion of which will probably get punted to a later post.
Many aspects of the final design were influenced by the second and third classes -- as much if not more so than the simple case of Bind().
A bit about template syntax
When you write code like the following:
UriTemplate t = new UriTemplate( "literal/{var}?k1={v1}" );
you are defining a pattern expression over the path and query components of the URI. The path component is divided into segments, where each segment in the template can be one of three tokens:
- A literal segment. Just what you'd think -- the corresponding segment in the URI must match this literal string. The match is done case-insensitively against the URL-escaped template string (canonicalization and escaping for template strings follow the logic of System.Uri).
- A named variable segment -- the name of the segment is surrounded with curly braces. Variables can match whole segments only, and the individual names of the variables within the template must be unique. During Match(), we use the variable name to create a key/value pair where the key is the name of the variable and the value is the unescaped value of the corresponding segment in the URI being matched.
- Path wildcard segments. We allow a special token (written as *) to appear at the end of the template as a stand in for 'the rest of the path'
The query component is written as an unordered sequence of name/value pairs. The left-hand side (the key) must be a literal value and cannot be templatized. The right-hand side can be a literal value or a named variable in {curlyBraces}.
Bind and Match are inverses
One basic operating assumption you can make about UriTemplate is that the Match() operation is the logical inverse of Bind(). If you create a URI from a template and a lexical environment using BindXXX(), you can assume that calling Match on the same template with the URI you just created will result in a successful match and create the same lexical environment you started with:
Uri baseAddress = new Uri( "http://localhost:81" );string artist = "Led Zeppelin";string album = "Four"; UriTemplate template =new UriTemplate("music/{artist}/{album}?format={format}" );Uri boundUri = template.BindByPosition( baseAddress, artist, album, "rss" ); //boundUri:// http://localhost:81/music/Led%20Zeppelin/Four?format=rssUriTemplateMatch match = template.Match( baseAddress, boundUri );Debug.Assert( match != null );Debug.Assert( match.BoundVariables["artist"] == artist );Debug.Assert( match.BoundVariables["album"] == album );Debug.Assert( match.BoundVariables["format"] == "rss" );
Although it's technically a weak invariant[1], it's a handy rule of thumb for reasoning about the intent of the design.
UriTemplateMatch
The return value of a successful match is an instance of System.UriTemplateMatch (a failed match will return null). Inside of UriTemplateMatch are a number of useful constructs:
public class UriTemplateMatch{public UriTemplateMatch (){} //The starting point for the template matchpublic Uri BaseUri{ get; }//The full candidate URI (as it came in off the wire -> untouched)public Uri RequestUri{ get; } //the set of path segments, starting after EndpointUri -> decodedpublic Colletion<string> RelativePathSegments { get; }//a name/value view of the entire query string -> VALUES are decodedpublic NameValueCollection QueryParameters { get; }//a name/value view of the template variables -> decodedpublic NameValueCollection BoundVariables { get; }//the template that was matched -> whatever you passed into the ctor of UriTemplatepublic UriTemplate MatchedTemplate { get; }//the unmatched part of the path//(for wildcard templates, e.g. {foo}/bar/*) -> untouchedpublic Collection<string> WildcardSegments{ get; }}
The member most relevant to this discussion is BoundVariables, which contains a name/value view of values obtained by extruding the candidate URI through the template and associating each variable in the template with its corresponding URI segment value. WildcardSegments and RelativePathSegments are quite nice because they give you just the parts of the URI that are relevant to your app (saves you from having to do unnatural acts -- like grepping through the URI to find a file extension, which is basically what you have to do on the platform today...).
I think that's probably enough rambling for one post -- I'll come back to Match() when I talk about UriTemplateTable in the next post.
--------
[1] The only time this invariant breaks is if the values you pass to BindXXX() contain slashes. Escaped or unescaped, slashes are considered evil by System.Uri for security reasons and are never escaped on the wire. As such, a call to Bind() with values containing slashes will result in an output URI with more segments that the template it came from, which will cause the subsequent Match() to fail. This gives rise to my URI canonicalization haiku:
oy, freaking slashes
real or escaped, they all suck.
I blame Roy Fielding...
Just kidding, Roy. :)
