When building client applications that need to connect to a HTTP API, sooner or later you are going to get involved in constructing a URL based on a API Root and some parameters. Often enough when looking at client libraries I see lots of ugly string concatenation and conditional logic to account for empty parameter values and trailing slashes. And there there is the issue of encoding. Several years ago a IETF specification (RFC 6570) was released that described a templating system for URLs and I created a library that implements the specification. Here is how you can use it to make constructing even the most crazy URLs as easy as pie.
Path Parameters
The simplest example is where you have a base URI and you need to update a parameter in the URL path segment,
[Fact]
publicvoidUpdatePathParameter(){
var url = newUriTemplate("http://example.org/{tenant}/customers")
.AddParameter("tenant", "acmé")
.Resolve();
Assert.Equal("http://example.org/acm%C3%A9/customers", url);}
This is a really trivial case that could mostly be handled with a string replace. However, a string replace wouldn’t take care of percent-encoding delimiters and unicode characters in the parameter value.
Under the covers there is a URITemplate class that can have parameters added to it and a Resolve method. I have created a simple fluent inferface using extension methods to make it convenient quickly create and resolve a template..
Query Parameters
A slightly more complex example would be adding a query string parameter.
[Fact]
publicvoidUpdatePathParameter(){
var url = newUriTemplate("http://example.org/{tenant}/customers")
.AddParameter("tenant", "acmé")
.Resolve();
Assert.Equal("http://example.org/acm%C3%A9/customers", url);}
This style of template can be problematic when there are optional query parameters and a parameter does not have a value. A better way of defining query parameters is like this,
[Fact]
publicvoidQueryParametersTheNewWay(){
var url = newUriTemplate("http://example.org/customers{?active}")
.AddParameter("active", "true")
.Resolve();
Assert.Equal("http://example.org/customers?active=true", url);
}
when you don’t want to provide any value at all, the template parameter will be removed.
[Fact]
publicvoidQueryParametersTheNewWayWithoutValue(){
var url = newUriTemplate("http://example.org/customers{?active}")
.AddParameters(null)
.Resolve();
Assert.Equal("http://example.org/customers", url);
}
In this last example I used a slightly different extension method that takes a single object and uses it’s properties as key-value pairs. This makes it easy to set multiple parameters.
[Fact]
public void ParametersFromAnObject()
{
var url = new UriTemplate("http://example.org/{environment}/{version}/customers{?active,country}")
.AddParameters(new
{
environment = "dev",
version = "v2",
active = "true",
country = "CA"
})
.Resolve();
Assert.Equal("http://example.org/dev/v2/customers?active=true&country=CA", url);
}
Lists and Dictionaries
Where URI Templates start to really shine as compared to simple string replaces and concatenation is when you start to use lists and dictionaries as parameter values.
In the next example we use a list id values that are stored in an array to specify a property value.
[Fact]
public void ApplyParametersObjectWithAListofInts()
{
var url = new UriTemplate("http://example.org/customers{?ids,order}")
.AddParameters(new
{
order = "up",
ids = new[] {21, 75, 21}
})
.Resolve();
Assert.Equal("http://example.org/customers?ids=21,75,21&order=up", url);
}
We can use dictionaries to define both the query parameter name and value,
[Fact]
public void ApplyDictionaryToQueryParameters()
{
var url = new UriTemplate("http://example.org/foo{?coords*}")
.AddParameter("coords", new Dictionary<string, string>
{
{"x", "1"},
{"y", "2"},
})
.Resolve();
Assert.Equal("http://example.org/foo?x=1&y=2", url);
}
We can also use lists to define a set of path segments.
[Fact]
public void ApplyFoldersToPathFromStringNotUrl()
{
var url = new UriTemplate("http://example.org{/folders*}{?filename}")
.AddParameters(new
{
folders = new[] { "files", "customer", "project" },
filename = "proposal.pdf"
})
.Resolve();
Assert.Equal("http://example.org/files/customer/project?filename=proposal.pdf", url);
}
Parameters can be anywhere
Parameters are not limited to path segments and query parameters. You can also put parameters in the host name.
[Fact]
public void ParametersFromAnObjectFromInvalidUrl()
{
var url = new UriTemplate("http://{environment}.example.org/{version}/customers{?active,country}")
.AddParameters(new
{
environment = "dev",
version = "v2",
active = "true",
country = "CA"
})
.Resolve();
Assert.Equal("http://dev.example.org/v2/customers?active=true&country=CA", url);
}
You can even replace the entire base URL.
[Fact]
public void ReplaceBaseAddress()
{
var url = new UriTemplate("{+baseUrl}api/customer/{id}")
.AddParameters(new
{
baseUrl = "http://example.org/",
id = "22"
})
.Resolve();
Assert.Equal("http://example.org/api/customer/22", url);
}
However, by default URI template will escape all delimiter characters in parameters, so the slashes in the base address would come out percent-encoded. By adding the + operator to the front of the baseUrl parameter we can instruct the resolution algorithm to not escape characters in the parameter value.
Partial Resolution
A recently added feature to the library is the ability to only resolve parameters that have been passed and leave the other parameters untouched. This is useful sometimes when you want to resolve base address and version parameters on application startup, but then want to add other parameters later.
Constructing Urls the easy way!
[Fact]
public void PartiallyParametersFromAnObjectFromInvalidUrl()
{
var url = new UriTemplate("http://{environment}.example.org/{version}/customers{?active,country}",resolvePartially:true)
.AddParameters(new
{
environment = "dev",
version = "v2"
})
.Resolve();
Assert.Equal("http://dev.example.org/v2/customers{?active,country}", url);
}
And there is so much more…
The URI Template specification contains many more syntax options that I have not covered. Many of them you may never use. However, it is nice to know that if you ever run into some API that uses some strange formatting, there is a reasonable chance that URI templates can support it.
Although the templating language is fairly sophisticated, it was specifically designed to be fast to process. The resolution algorithm can be performed by walking the template characters just once and performing substitutions along the way.
Where to find it
All the source code for the project can be found on Github and there is a nuget package available. The library is built to support .Net35, .Net45 and there is a portable version that supports Win Phone 8, 8.1, WinRT and mono on Android and iOS.
Image Credit: Templates https://flic.kr/p/5Xc9sq