Ambiguous UriTemplates, Query Parameters and Integration Testing
When I woke up this morning I had not planned on the day unfolding like this, but an email from a colleague started me on a path and here is where it led.
If you create a service that uses UriTemplates with the WCF REST Starter Kit and you include Query String parameters in the UriTemplate you might end up with something like this
[OperationContract]
[WebGet(UriTemplate = "subscriptions?page={page}&pagesize={pagesize}", ResponseFormat = WebMessageFormat.Xml)]
ItemInfoList GetPageInXml(int page, int pagesize);
[WebGet(UriTemplate = "subscriptions")]
[OperationContract]
ItemInfoList GetItemsInXml();
This contract will result in a runtime exception when WCF validates the contract
System.InvalidOperationException was unhandled by user code Message="UriTemplateTable does not support multiple templates that have equivalent path as template 'subscriptions?format=json' but have different query strings, where the query strings cannot all be disambiguated via literal values. See the documentation for UriTemplateTable for more detail." Source="System.ServiceModel.Web"
What WCF is saying is that it does not have enough information to map a URI to a method on the contract and the Query Strings don’t help in this case.
So what should you do?
My advice is to avoid putting Query String parameters in the UriTemplate. You can still access them from the incoming request. To make it easier, I created a class called QueryString to help you.
QueryString will retreive arguments from the IncommingRequest object. It also provides overloads that allow you to specify default values and a bool flag to indicate if the parameter is required or not.
Example
Suppose you want to add paging support for a RESTful URI. I would need to allow the caller to specify the starting index and the page size. If they don’t specify either, they get some defaults like start at page 0 and use a page size of 5.
Because I don’t want to put the QueryString parameters in my contract it would look like this
[WebGet(UriTemplate = "subscriptions")]
[OperationContract]
List GetItemsInXml()
{
// Get the optional start index parameter
int startIndex = GetInt("start", // Name of Query Parameter
0); // Default value
// Get the optional page size parameter
int pageSize = GetInt("count", // Name of Query Parameter
5); // Default value
// Get the items - our db layer doesn't support paging in this case
List Items = GetItems();
// Support the paging here
return Items.Skip(startIndex).Take(pageSize).ToList();
}
// Call this service using a URI like this (query parameters are optional)
https://localhost/service.svc/subscriptions?start=5&count=10
Required Parameters
Sometimes you want to require a query string parameter. If the caller doesn't supply it, you want to return a status code of 400 - Bad Request. To do this you can do this
// Get a required parameter - no default
// If no supplied, the response code is set to 400 and an argument exception is thrown with the response body suppressed.
string tag = GetRequiredString("tag")
Integration Testing
This morning in a rush to help my colleague out, I quickly put this class together (without any tests) and sent it to him on email. Then I began to think to myself... How do I know that this class works? Though I had a test page, I didn't really know that it worked but I thought it did... After feeling guilty for a while, I decided to write some unit tests. Since this class makes liberal use of WCF Intrinsics like WebOperationContext.Current I found that I couldn't really test it without either (a) mocking the intrinsics (difficult) or (b) integration testing with the HTTP stack. So I opted for integration testing. Sure enough as I was testing I found major bugs in the code (could have guessed that). I also found that if you don't intentionally make the test fail, you could have buggy tests that always pass (had a couple of those as well).
Testing Failures by checking the HTTP Status Code
In my negative test cases (ones that should fail) I wanted to insure that the HTTP status code was 400 (Bad Request) and not something else like 500 (Internal Server Error). Though MSTest allows you to specify an [ExpectedException(...)], you cannot validate the status code from the attribute. To do this I had to use the following code.
// Should error on invalid count
[TestMethod()]
[ExpectedException(typeof(WebException))]
public void ShouldBadRequestOnInvalidCount()
{
bool required = false;
string tag = null;
string start = "3";
string count = "bar";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(CreateRequestUri(required, tag, start, count));
try
{
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
}
catch (WebException webex)
{
// Shoud return a 400
Assert.AreEqual(HttpStatusCode.BadRequest, ((HttpWebResponse)webex.Response).StatusCode);
throw;
}
}
At first I left off the ExceptedException attribute because I was catching the exception, but then you won't get a test failure if no exception is thrown so it is important to verify the status code and the ExpectedException.
Try it out
You are welcome to check this class out and give it a try - let me know what you think
Comments
- Anonymous
February 10, 2009
PingBack from http://www.anith.com/?p=8558