共用方式為


WCF Extensibility – QueryStringConverter

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

Coming back to “proper” WCF extensibility, this week’s post is about the QueryStringConverter. This is actually a simple topic to be covered, as its purpose is quite specific (unlike other extensibility points seen before, which could be used for a wide variety of cases) – within WCF the QueryStringConverter is only used on endpoints which have the WebHttpBehavior applied to them. And even in those, only on operations which have parameters passed via the query strings (either operations with parameters marked with [WebGet] or a [WebInvoke] operation with a UriTemplate that explicitly binds some parameters to the query string). A QueryStringConverter is the piece which can convert between operation parameters and their representation in a query string. For example, for the operation below:

  1. [WebGet(UriTemplate = "/Movies?releaseYear={year}&genre={genre}")]
  2. Movie[] GetMovies(int year, string genre);

On the server side (the most common usage) the QueryStringConverter will convert the strings in the query string into the actual parameters which will be passed to the operation – in the case of the “genre” parameter it will be the same value; for the “year” parameter the value will be parsed as an integer and passed to the operation. On the client side (i.e., when using a WebHttpBinding/WebHttpBehavior-based client endpoint), the converter is responsible for converting the operation parameters into strings which will be passed in the query string when the call is being made.

The default QueryStringConverter used by the WebHttpBehavior supports natively several types, including all simple numeric types (Byte, SByte, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double, Decimal), Boolean, Char, Object, String, DateTime, DateTimeOffset, TimeSpan, Guid, Uri, and arrays of Byte (essentially, all the types which the DataContractSerializer considers to be “primitives”, with the exception of XmlQualifiedName). Enumeration types are also supported by default (the string representation of the enum values are used). Finally, there is also another set of types which are supported by the default QueryStringConverter – any one which declares a [TypeConverter] attribute with a type converter which can convert the type to and from strings (more on that below).

Public implementations in WCF

The QueryStringConverter class is concrete, so it can be used directly. There’s also a JsonQueryStringConverter class, which uses the DataContractJsonSerializer to convert between instances of non-primitive types and (its JSON-equivalent) strings.

Class definition

  1. public class QueryStringConverter
  2. {
  3.     public virtual bool CanConvert(Type type);
  4.     public virtual object ConvertStringToValue(string parameter, Type parameterType);
  5.     public virtual string ConvertValueToString(object parameter, Type parameterType);
  6. }

The QueryStringConverter class has three public (and virtual) methods. CanConvert is used when the runtime is being set up (called by WebHttpBehavior.ApplyDispatchBehavior for the server side or ApplyClientBehavior for the client side) for all parameter types in the operations, and if one of them return false, the service (or the channel factory) will throw when being opened. At the server side, when an incoming request arrives with query parameters, ConvertStringToValue will be called to convert it to the appropriate CLR type. At the client side, ConvertValueToString will be called to do the equivalent translation. Notice that it entails that if you’re writing a custom QueryStringConverter to be used on the server side only (most common scenario), you don’t need to override ConvertValueToString as it will never be called.

How to set the query string converter

Since the QueryStringConverter is only honored by the WebHttpBehavior, we need a class derived from that behavior to replace the QueryStringConverter used in the operation. WebHttpBehavior defines a virtual method, GetQueryStringConverter, which can be overridden to return a type derived from the default QueryStringConverter.

  1. class MyQueryStringConverter : QueryStringConverter { }
  2. public class MyWebHttpBehavior : WebHttpBehavior
  3. {
  4.     protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
  5.     {
  6.         return new MyQueryStringConverter();
  7.     }
  8. }

Notice that this method is called for every operation in the contract, so you can potentially have different converters per each operation.

Supporting additional types in query string parameters

Before going in the details, one question needs to be asked: why does one need to support additional types in query string parameters anyway? Per the REST principles, we should not pass complex types over GET requests, and if you are trying to do it you’re likely doing it wrong (or at least not in the spirit of REST). The URI (universal resource identifier) for a resource shouldn’t be identified by the resource itself, which is what most complex types represent. There may be some good reasons for that, though. A point can potentially be used as an identifier of a place in a grid (or in a plane in general), and being able to pass it in the query string is a nice option to have. Also, I’ve seen some non-idempotent operations are mapped as GET operations to work around cross-domain limitations which could benefit from an extended set of types (although I really, really dislike this, as it goes against all good that HTTP can give us, but I digress). And it’s possible, for performance reasons (less HTTP traffic), that we may want to use a GET operation to retrieve a collection of resources, instead of a single resource, and supporting things such as arrays of identifiers in an operation may make sense in some scenarios (even though it may go against the strict idea of resource). And with many questions in forums about supporting different types in query strings that, even if it may not be the most elegant solution, some people need it, and WCF offers a way to solve this problem.

So we can use the query string converter to support additional parameter types. So how to do about it? There are essentially two ways to go about this issue: create a type converter for your type, and decorate it with a [TypeConverter] attribute referencing the converter you created. Or create a subclass of QueryStringConverter and override the appropriate methods depending on where the converter will be used.

The first case isn’t specific to WCF, it comes from the component model namespace and was primarily intended for usage in XAML-based projects to convert a type to and from a text representation, but the converter is generic enough that it supports converting between any arbitrary pair of types. For the Point scenario listed above, a simple converter is shown below. By decorating the class Point with the [TypeConverter] attribute (and creating the converter itself), this type can now be used in query parameters for WCF endpoints which use the WebHttpBehavior.

  1. [ServiceContract]
  2. public class ChessService
  3. {
  4.     static ChessPiece[,] board = new ChessPiece[8, 8];
  5.  
  6.     [WebGet]
  7.     public ChessPiece GetPiece(Point point)
  8.     {
  9.         return board[point.X, point.Y];
  10.     }
  11. }
  12. [TypeConverter(typeof(PointTypeConverter))]
  13. public class Point
  14. {
  15.     public int X { get; set; }
  16.     public int Y { get; set; }
  17. }
  18. public class PointTypeConverter : TypeConverter
  19. {
  20.     static readonly Regex PointRegex = new Regex(@"\[(\d+),(\d+)\]");
  21.     public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  22.     {
  23.         return sourceType == typeof(string) ||
  24.             base.CanConvertFrom(context, sourceType);
  25.     }
  26.     public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  27.     {
  28.         string strValue = value as string;
  29.         if (strValue != null)
  30.         {
  31.             Match match = PointRegex.Match(strValue);
  32.             if (match.Success)
  33.             {
  34.                 return new Point
  35.                 {
  36.                     X = int.Parse(match.Groups[1].Value),
  37.                     Y = int.Parse(match.Groups[2].Value)
  38.                 };
  39.             }
  40.         }
  41.  
  42.         return base.ConvertFrom(context, culture, value);
  43.     }
  44.     public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
  45.     {
  46.         if (destinationType == typeof(string))
  47.         {
  48.             Point point = (Point)value;
  49.             return string.Format("[{0},{1}]", point.X, point.Y);
  50.         }
  51.         else
  52.         {
  53.             return base.ConvertTo(context, culture, value, destinationType);
  54.         }
  55.     }
  56. }

The type converter way works fine, but it’s not feasible in some cases. First, you need to decorate the type with an attribute, and this is often not possible – if you want to support arrays, dictionaries, or any built-in types which can’t be modified. Also, you need to create essentially a converter class for every type you want to support (since TypeConverter.CanConvertFrom does not specify which type it needs to convert to), so if you need to support multiple types the number of converters will increase. For these scenarios the alternative is to replace the query string converter itself with a subclass, which can handle the conversion. The main drawback of this approach is that you’ll now need also a subclass of WebHttpBehavior to replace the query string converter used by the behavior. Below is the same scenario for the Point class solved with the new approach.

  1. [ServiceContract]
  2. public class ChessService
  3. {
  4.     static ChessPiece[,] board = new ChessPiece[8, 8];
  5.  
  6.     [WebGet]
  7.     public ChessPiece GetPiece(Point point)
  8.     {
  9.         return board[point.X, point.Y];
  10.     }
  11. }
  12. public class Point
  13. {
  14.     public int X { get; set; }
  15.     public int Y { get; set; }
  16. }
  17. public class PointAwareQueryStringConverter : QueryStringConverter
  18. {
  19.     static readonly Regex PointRegex = new Regex(@"\[(\d+),(\d+)\]");
  20.     public override bool CanConvert(Type type)
  21.     {
  22.         return type == typeof(Point) || base.CanConvert(type);
  23.     }
  24.     public override object ConvertStringToValue(string parameter, Type parameterType)
  25.     {
  26.         if (parameterType == typeof(Point))
  27.         {
  28.             Match match = PointRegex.Match(parameter);
  29.             if (match.Success)
  30.             {
  31.                 return new Point
  32.                 {
  33.                     X = int.Parse(match.Groups[1].Value),
  34.                     Y = int.Parse(match.Groups[2].Value)
  35.                 };
  36.             }
  37.         }
  38.  
  39.         return base.ConvertStringToValue(parameter, parameterType);
  40.     }
  41. }
  42. public class ChessWebHttpBehavior : WebHttpBehavior
  43. {
  44.     protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
  45.     {
  46.         return new PointAwareQueryStringConverter();
  47.     }
  48. }

Which approach to use will really depend on the scenario. The second approach works for all types, but if you already have a converter used elsewhere (such as for XAML binding), then you can reuse the same converter for WCF HTTP endpoints as well. And if you like to define the endpoint in configuration (instead of via code), to use the second option you’ll need to create a behavior configuration extension for the new behavior as well.

Real world scenario: retrieving multiple resources in a single GET request

In the questions I’ve seen people asking about enabling new types for query string parameters, some of them were simply trying to pass array values in the query string parameters. As I mentioned before, one valid reason to do that is to selectively retrieve some resources from a service in a single request (to avoid multiple HTTP round-trips), and this would hopefully work in a simple way. This is a fairly simple scenario to implement, so I decided to go with it.

To start things off, here’s a simple service for which one can retrieve products. With the optimization option, besides the “canonical” GetProduct operation, we also have a GetProducts one. Their implementation are trivial.

  1. public class Product
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public double Price { get; set; }
  6. }
  7.  
  8. [ServiceContract]
  9. public class Service
  10. {
  11.     [WebGet]
  12.     public Product GetProduct(int id)
  13.     {
  14.         return ProductRepository.GetProducts().Where(p => p.Id == id).FirstOrDefault();
  15.     }
  16.  
  17.     [WebGet]
  18.     public Product[] GetProducts(int[] ids)
  19.     {
  20.         return ProductRepository.GetProducts().Where(p => ids.Contains(p.Id)).ToArray();
  21.     }
  22. }

Trying to use this service with the default WebHttpBehavior will cause this validation error to be thrown when the service is being opened:

System.InvalidOperationException: Operation 'GetProducts' in contract 'Service' has a query variable named 'ids' of type 'System.Int32[]', but type 'System.Int32[]' is not convertible by 'QueryStringConverter'.  Variables for UriTemplate query values must have types that can be converted by 'QueryStringConverter'.

Since we cannot update the int[] class to use the type converter, we need to go with the new QueryStringConverter implementation. But

And before I go any further, here goes the usual disclaimer – this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Some known issues with the code are that for strings, any values in the array which contain commas will be split (it should be implemented in a CSV-style, where if you have commas in your data you can wrap the strings in quotes). For simplicity sake it also doesn’t have a lot of error handling which a production-level code would.

The new converter will use a simple algorithm to split the input from the query string at the ‘,’ character, and then use the original logic from the base QueryStringConverter class to convert the array elements. Notice that since this is to be used on the server only we don’t need to override ConvertValueToString.

  1. public class ArrayQueryStringConverter : QueryStringConverter
  2. {
  3.     public override bool CanConvert(Type type)
  4.     {
  5.         if (type.IsArray)
  6.         {
  7.             return base.CanConvert(type.GetElementType());
  8.         }
  9.         else
  10.         {
  11.             return base.CanConvert(type);
  12.         }
  13.     }
  14.  
  15.     public override object ConvertStringToValue(string parameter, Type parameterType)
  16.     {
  17.         if (parameterType.IsArray)
  18.         {
  19.             Type elementType = parameterType.GetElementType();
  20.             string[] parameterList = parameter.Split(',');
  21.             Array result = Array.CreateInstance(elementType, parameterList.Length);
  22.             for (int i = 0; i < parameterList.Length; i++)
  23.             {
  24.                 result.SetValue(base.ConvertStringToValue(parameterList[i], elementType), i);
  25.             }
  26.  
  27.             return result;
  28.         }
  29.         else
  30.         {
  31.             return base.ConvertStringToValue(parameter, parameterType);
  32.         }
  33.     }
  34. }

Now we need to define a subclass of WebHttpBehavior to replace the converter, and the implementation is simple. Also, since the original scenario where I saw this problem was a JavaScript client, the new behavior also changes the default outgoing response format to Json (the default is XML) so that it doesn’t have to be specified in every operation.

  1. public class ArrayInQueryStringWebHttpBehavior : WebHttpBehavior
  2. {
  3.     WebMessageFormat defaultOutgoingResponseFormat;
  4.     public ArrayInQueryStringWebHttpBehavior()
  5.     {
  6.         this.defaultOutgoingResponseFormat = WebMessageFormat.Json;
  7.     }
  8.  
  9.     public override WebMessageFormat DefaultOutgoingResponseFormat
  10.     {
  11.         get
  12.         {
  13.             return this.defaultOutgoingResponseFormat;
  14.         }
  15.         set
  16.         {
  17.             this.defaultOutgoingResponseFormat = value;
  18.         }
  19.     }
  20.  
  21.     protected override QueryStringConverter GetQueryStringConverter(OperationDescription operationDescription)
  22.     {
  23.         return new ArrayQueryStringConverter();
  24.     }
  25. }

And that’s pretty much it. We can now test the service and it should work just fine.

  1. static void Main(string[] args)
  2. {
  3.     string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  4.     ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  5.     host.AddServiceEndpoint(typeof(Service), new WebHttpBinding(), "").Behaviors.Add(new ArrayInQueryStringWebHttpBehavior());
  6.     host.Open();
  7.     Console.WriteLine("Host opened");
  8.  
  9.     WebClient c = new WebClient();
  10.     Console.WriteLine(c.DownloadString(baseAddress + "/GetProduct?id=3"));
  11.  
  12.     c = new WebClient();
  13.     Console.WriteLine(c.DownloadString(baseAddress + "/GetProducts?ids=1,3,5"));
  14. }

One final notice about WCF and REST endpoints: the new WCF Web APIs should make this scenario simpler by using formatters.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    August 09, 2011
    Hi Carlos,I have a WCF service which is consumed by a client as a webreference but he is not able to send value type members(x) to server as he is not setting "xSpecified" property exposed for value type member(x). I can not make it "isRequired" to true as it will affect HTTP POST clients. Is there any better way to handle it other than client setting specified property(xSpecified)?Is it a good practice for marking IsRequired property to true for all value type member in DataContract?