Tuesday, November 26, 2013

Avoiding Duplicate Query Parameters in Web API

If you pass parameters to your Web API using queries, you would have a method like:
public IEnumerable<string> Get([FromUri] string p1)...
And you'll call it like:
.../Values?p1=a
All of this works perfectly fine until someone passes you duplicate parameters:
.../Values?p1=a&p1=b
In this case your Web API method, instead of getting the first value "a" or the last one "b", will have "(Collection)". Of course, you can redefine your method and make it to accept arrays, then validate the arrays, check if they are not empty, have mo than one element, and so on. But if you need only one unique value and ignore all the rest ones, Web API provides you with a nice but not very obvious solution. To do this you need to redefine the way the Web API parses query parameters. And the responsible ones for this functionality are the ValueProvider classes. There are different providers, but we need QueryStringValueProvider, and we simply replace it with our own implementation. 1. Replacing QueryStringValueProvider with our implementation (should be done anywhere in WebApiConfig):
config.Services.Remove (typeof (ValueProviderFactory), config.Services.GetValueProviderFactories().First (f => f is QueryStringValueProviderFactory));
config.Services.Add (typeof(ValueProviderFactory), new QueryStringUniqueValueProviderFactory());
2. Our implementation of QueryStringValueProvider providing only unique values:
using System.Globalization;
using System.Collections.Generic;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ValueProviders;
using System.Web.Http.ValueProviders.Providers;

public class QueryStringUniqueValueProviderFactory : QueryStringValueProviderFactory
{
 private const string RequestLocalStorageKey = "{755EDBD6-46CD-4B44-8162-31D8CF111155}";

 public override IValueProvider GetValueProvider(HttpActionContext actionContext)
 {
  object provider;
  var storage = actionContext.Request.Properties;

  // Only parse the query string once-per request
  if (!storage.TryGetValue(RequestLocalStorageKey, out provider))
  {
   provider = new QueryStringUniqueValueProvider(actionContext, CultureInfo.InvariantCulture);
   storage[RequestLocalStorageKey] = provider;
  }

  return (IValueProvider)provider;
 }
}


public class QueryStringUniqueValueProvider : NameValuePairsValueProvider
{
 public QueryStringUniqueValueProvider(HttpActionContext actionContext, CultureInfo culture)
  : base(GetUniqueQueryNameValuePairs(actionContext), culture)
 {
 }


 private static IEnumerable<KeyValuePair<string, string>> GetUniqueQueryNameValuePairs(HttpActionContext actionContext)
 {
  var pairs= actionContext.ControllerContext.Request.GetQueryNameValuePairs();
  var returnedKeys= new HashSet<string>();
  foreach (var pair in pairs)
  {
   if (returnedKeys.Contains (pair.Key))
    continue;
   returnedKeys.Add (pair.Key);
   yield return pair;
  }
 }
}
And that's it! One place, no mess in your code with arrays validation, quick and neat.