Today I was playing with HTML tables in a ASP.NET C# MVC Web Application: since I had to implement some ajax-based parsing, sorting and filtering, I installed the awesome Tabulator JQuery plugin, which is among the few ones that supports all that.
Implementing it was rather easy, thanks to the great documentation and examples provided by the project's website. However, as soon as I was testing the filters, I struggled a bit to convince my server-side Controller to properly handle the auto-generated ajax URL used by the plugin to send the filters info issued by the client to the server:
1 |
/GetData?page=1&size=100&filters[0][field]=fieldName&filters[0][type]=number&filters[0][value]=3 |
This is a rather common notation syntax to pass "array-like" values within a GET request: the technique is usually called "square-bracket notation" and it's much similar to what you can do in loosely-typed scripting languages such as PHP and JavaScript, where you can define numeric and associative arrays in the following way:
1 2 |
filters[0][field] = "value"; filters[0][type] = "anotherValue"; |
... and so on: you don't have to worry about compilation-level errors there, as the type will be determined internally. Conversely, in strongly-typed languages such as ASP.NET's C# and Visual Basic.NET, these types need to be determined right from the definition.
The Issue
The aforementioned concept is strictly related to our initial problem: as a matter of fact, the binding logic used by MVC to convert the Query String of a GET Request into strongly-typed parameters also needs to be instructed properly. In other words, we cannot do this:
1 |
public ActionResult GetData(int page, int size, string filters) { /* ...TODO... */ } |
Because the default MVC property binder won't be able to do its job properly: the filters string will be always set to null, because there's nothing in the query string that can be treated like a string.
And the following won't work either:
1 |
public ActionResult GetData(int page, int size, object filters) { /* ...TODO... */ } |
We would just end up with an empty filters object, as the property binder won't know how to properly "box" the received parameter array(s).
(If you don't know what boxing is, read here for more info - otherwise, keep reading).
The Workaround(s)
As we most likely already know, all the query string parameter value(s) can be also retrieved directly from the Request.QueryString collection. However, it won't be a walk in the park, as they will be split up into many different key-values items (one for each inner value):
That's far from ideal.
We could try to normalize this outcome by parsing the QueryString in a different way: however, the HttpUtility.ParseQueryString() and the UriExtension.ParseQueryString() methods won't help us, as they both return a NameValueCollection object, which is basically a collection of string keys and string values... And our filters object is definitely not a string.
Our only option to get these values in a decent fashion would be to implement a custom (and rather complex) QueryString parser... and it won't be easy. Luckily enough, the MVC framework does provide much better alternatives to deal with our issue.
The Fix
For standard arrays of strings and/or numbers, the solution would be something like this:
1 |
public ActionResult GetData(int page, int size, IEnumerable<string> filters) { /* ...TODO... */ } |
However, in our specific scenario that won't work either: we're not dealing with a simple array of strings: our filters object is actually a two-dimensional array. To be even more precise, it's an array of name-value pairs.
Once we understand that, the fix is rather obvious:
1 |
public ActionResult GetData(int page, int size, IEnumerable<Dictionary<string,string>> filters) { /* ...TODO... */ } |
This will be accepted (and properly "boxed") by the MVC default model binder, without the need to implement a custom model binder and/or TypeConverter manually.
Unfortunately, there's not an easy way to achieve the same result using the standard HttpUtility.ParseQueryString() and the UriExtension.ParseQueryString() methods, meaning that we cannot properly handle these complex query strings outside one of our standard request-routing cycles, where we can use the powerful property-binding feature provided by the MVC framework.