2

Let's say that you have the following code

public class MyClass {
   public double Latitude {get; set;}
   public double Longitude {get; set;}
}
public class Criteria
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public MyClass MyProp {get; set;}
}

[HttpGet]    
public Criteria Get([FromUri] Criteria c)
{
  return c;
}

I'd like to know if someone is aware of a library that could transform any object into query string that is understood by a WEB API 2 Controller.

Here is an example of what I'd like

SerializeToQueryString(new Criteria{StartDate=DateTime.Today, EndDate = DateTime.Today.AddDays(1), MyProp = new MyProp{Latitude=1, Longitude=3}}); 
=> "startDate=2015-10-13&endDate=2015-10-14&myProp.latitude=1&myProp.longitude=3"

A full example with httpClient might look like :

new HttpClient("http://localhost").GetAsync("/tmp?"+SerializeToQueryString(new Criteria{StartDate=DateTime.Today, EndDate = DateTime.Today.AddDays(1), MyProp = new MyProp{Latitude=1, Longitude=3}})).Result;

At the moment, I use a version (taken from a question I do not find again, maybe How do I serialize an object into query-string format? ...).

The problem is that it is not working for anything else than simple properties. For example, calling ToString on a Date will not give something that is parseable by WEB API 2 controller...

    private string SerializeToQueryString<T>(T aObject)
    {
        var query = HttpUtility.ParseQueryString(string.Empty);
        var fields = typeof(T).GetProperties();
        foreach (var field in fields)
        {
            string key = field.Name;
            var value = field.GetValue(aObject);
            if (value != null)
                query[key] = value.ToString();
        }
        return query.ToString();
    }
Community
  • 1
  • 1
Christophe Blin
  • 1,687
  • 1
  • 22
  • 40
  • 1
    why don´t you POST it as json? – Jehof Oct 13 '15 at 09:38
  • 2
    because HTTP verbs have a semantic in REST. For example POST /products to do a search is awkward as you would think this is a creation – Christophe Blin Oct 13 '15 at 12:09
  • Serialize to JSON and URL-encode? – Todd Menier Oct 13 '15 at 12:42
  • @ChristopheBlin, correct what I mean is why don´t you use a JSON Payload in your GET Request, but anyway it seems that this is not a good practise. – Jehof Oct 13 '15 at 12:51
  • @Jehof I am not against using JSON payload in the query string, however this is not working with WEB API 2 out of the box... – Christophe Blin Oct 13 '15 at 13:42
  • @ChristopheBlin Right. Serializing to query strings is not an capability built into Web API 2. POST does not necessarily imply creation. POST is a perfectly valid option. The downside is you cannot take advantage of cached responses with a POST method. – Darrel Miller Oct 13 '15 at 23:53
  • @ChristopheBlin I think the problem is your `"routing"`. Here is my sample, It work perfectly https://github.com/hungdoan2/community-shared-sample/tree/master/stackoverflow/33099089_TestRoute – Hung Doan Oct 14 '15 at 08:51
  • @hungdoan my controller is working fine (just as yours). The problem is to call it from another c# program. I have edited the question to be more clear about this – Christophe Blin Oct 14 '15 at 09:08
  • Have you considered an approach where you have a separate API that you POST the query parameters to in order to create a 'Query' resource. Using the Id of the query resource coming back you then make your GET calls. This approach will allow queries that are larger than url limits and you can cache the GET responses too. – Ian Mercer Oct 14 '15 at 15:59
  • @IanMercer indeed, this is what I did in another context but this has serious limitations (i.e you need to store the query somewhere and this can be quite an annoyance with load balancing ...). See my "rool your own" solution at the end of this post – Christophe Blin Apr 04 '16 at 09:11

2 Answers2

2

"Transform any object to a query string" seems to imply there's a standard format for this, and there just isn't. So you would need to pick one or roll your own. JSON seems like the obvious choice due to the availability of great libraries.

Todd Menier
  • 37,557
  • 17
  • 150
  • 173
  • True, but your requirement of handling *any* object type is satisfied. Deserializing on the server is a one-liner, or you can hide it entirely with a custom `ModelBinder`. – Todd Menier Oct 14 '15 at 14:15
  • Thanks for your ideas ! Your solution works but I do not want to sacrifice URL readability (i.e { " : are going to be urlencoded, resulting in pretty hard to read URL). And you are also right that the solution I propose does not work with any object – Christophe Blin Oct 14 '15 at 15:21
0

Since it seems no one has dealt with the problem before, here is the solution I use in my project :

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Web;

namespace App
{
    public class QueryStringSerializer
    {
        public static string SerializeToQueryString(object aObject)
        {
            return SerializeToQueryString(aObject, "").ToString();
        }

        private static NameValueCollection SerializeToQueryString(object aObject, string prefix)
        {
            //!\ doing this to get back a HttpValueCollection which is an internal class
            //we want a HttpValueCollection because toString on this class is what we want in the public method
            //cf http://stackoverflow.com/a/17096289/1545567
            var query = HttpUtility.ParseQueryString(String.Empty); 
            var fields = aObject.GetType().GetProperties();
            foreach (var field in fields)
            {
                string key = string.IsNullOrEmpty(prefix) ? field.Name : prefix + "." + field.Name;
                var value = field.GetValue(aObject);
                if (value != null)
                {
                    var propertyType = GetUnderlyingPropertyType(field.PropertyType);
                    if (IsSupportedType(propertyType))
                    {
                        query.Add(key, ToString(value)); 
                    }
                    else if (value is IEnumerable)
                    {
                        var enumerableValue = (IEnumerable) value;
                        foreach (var enumerableValueElement in enumerableValue)
                        {
                            if (IsSupportedType(GetUnderlyingPropertyType(enumerableValueElement.GetType())))
                            {
                                query.Add(key, ToString(enumerableValueElement));
                            } 
                            else
                            {
                                //it seems that WEB API 2 Controllers are unable to deserialize collections of complex objects...
                                throw new Exception("can not use IEnumerable<T> where T is a class because it is not understood server side");  
                            }                            
                        }
                    }
                    else
                    {
                        var subquery = SerializeToQueryString(value, key);
                        query.Add(subquery);
                    }

                }
            }
            return query;
        }

        private static Type GetUnderlyingPropertyType(Type propType)
        {
            var nullablePropertyType = Nullable.GetUnderlyingType(propType);
            return nullablePropertyType ?? propType;
        }

        private static bool IsSupportedType(Type propertyType)
        {
            return SUPPORTED_TYPES.Contains(propertyType) || propertyType.IsEnum;
        }

        private static readonly Type[] SUPPORTED_TYPES = new[]
        {
            typeof(DateTime),
            typeof(string),
            typeof(int),
            typeof(long),
            typeof(float),
            typeof(double)
        };

        private static string ToString(object value)
        {
            if (value is DateTime)
            {
                var dateValue = (DateTime) value;
                if (dateValue.Hour == 0 && dateValue.Minute == 0 && dateValue.Second == 0)
                {
                    return dateValue.ToString("yyyy-MM-dd");
                }
                else
                {
                    return dateValue.ToString("yyyy-MM-dd HH:mm:ss");
                }
            }
            else if (value is float)
            {
                return ((float) value).ToString(CultureInfo.InvariantCulture);
            }
            else if (value is double)
            {
                return ((double)value).ToString(CultureInfo.InvariantCulture);
            }
            else /*int, long, string, ENUM*/
            {
                return value.ToString();
            }
        }
    }
}

Here is the unit test to demonstrate :

using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Framework.WebApi.Core.Tests
{
    [TestClass]
    public class QueryStringSerializerTest
    {
        public class EasyObject
        {
            public string MyString { get; set; }
            public int? MyInt { get; set; }
            public long? MyLong { get; set; }
            public float? MyFloat { get; set; }
            public double? MyDouble { get; set; }            
        }

        [TestMethod]
        public void TestEasyObject()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new EasyObject(){MyString = "string", MyInt = 1, MyLong = 1L, MyFloat = 1.5F, MyDouble = 1.4});
            Assert.IsTrue(queryString.Contains("MyString=string"));
            Assert.IsTrue(queryString.Contains("MyInt=1"));
            Assert.IsTrue(queryString.Contains("MyLong=1"));
            Assert.IsTrue(queryString.Contains("MyFloat=1.5"));
            Assert.IsTrue(queryString.Contains("MyDouble=1.4"));
        }

        [TestMethod]
        public void TestEasyObjectNullable()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new EasyObject() {  });
            Assert.IsTrue(queryString == "");
        }

        [TestMethod]
        public void TestUrlEncoding()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new EasyObject() { MyString = "&=/;+" });
            Assert.IsTrue(queryString.Contains("MyString=%26%3d%2f%3b%2b"));            
        }

        public class DateObject
        {
            public DateTime MyDate { get; set; }            
        }

        [TestMethod]
        public void TestDate()
        {
            var d = DateTime.ParseExact("2010-10-13", "yyyy-MM-dd", CultureInfo.InvariantCulture);
            var queryString = QueryStringSerializer.SerializeToQueryString(new DateObject() { MyDate = d });
            Assert.IsTrue(queryString.Contains("MyDate=2010-10-13"));
        }

        [TestMethod]
        public void TestDateTime()
        {
            var d = DateTime.ParseExact("2010-10-13 20:00", "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
            var queryString = QueryStringSerializer.SerializeToQueryString(new DateObject() { MyDate = d });
            Assert.IsTrue(queryString.Contains("MyDate=2010-10-13+20%3a00%3a00"));
        }


        public class InnerComplexObject
        {
            public double Lat { get; set; }
            public double Lon { get; set; }
        }

        public class ComplexObject
        {
            public InnerComplexObject Inner { get; set; }    
        }

        [TestMethod]
        public void TestComplexObject()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new ComplexObject() { Inner = new InnerComplexObject() {Lat = 50, Lon = 2} });
            Assert.IsTrue(queryString.Contains("Inner.Lat=50"));
            Assert.IsTrue(queryString.Contains("Inner.Lon=2"));
        }

        public class EnumerableObject
        {
            public IEnumerable<int> InnerInts { get; set; }
        }

        [TestMethod]
        public void TestEnumerableObject()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new EnumerableObject() { 
                InnerInts = new[] { 1,2 }
            });
            Assert.IsTrue(queryString.Contains("InnerInts=1"));
            Assert.IsTrue(queryString.Contains("InnerInts=2"));
        }

        public class ComplexEnumerableObject
        {
            public IEnumerable<InnerComplexObject> Inners { get; set; }
        }

        [TestMethod]
        public void TestComplexEnumerableObject()
        {
            try
            {
                QueryStringSerializer.SerializeToQueryString(new ComplexEnumerableObject()
                {
                    Inners = new[]
                    {
                        new InnerComplexObject() {Lat = 50, Lon = 2},
                        new InnerComplexObject() {Lat = 51, Lon = 3},
                    }
                });
                Assert.Fail("we should refuse something that will not be understand by the server");
            }
            catch (Exception e)
            {
                Assert.AreEqual("can not use IEnumerable<T> where T is a class because it is not understood server side", e.Message);
            }
        }

        public enum TheEnum : int
        {
            One = 1,
            Two = 2
        }


        public class EnumObject
        {
            public TheEnum? MyEnum { get; set; }
        }

        [TestMethod]
        public void TestEnum()
        {
            var queryString = QueryStringSerializer.SerializeToQueryString(new EnumObject() { MyEnum = TheEnum.Two});
            Assert.IsTrue(queryString.Contains("MyEnum=Two"));
        }
    }
}

I'd like to thank all the participants even if this is not something that you should usually do in a Q&A format :)

Christophe Blin
  • 1,687
  • 1
  • 22
  • 40
  • This seems to fall under the "roll your own" category and has lots of limitations compared to using an existing serialization format. But if it works for your purposes, cool. :) – Todd Menier Oct 14 '15 at 14:18
  • @ToddMenier : You are absolutely right. The only advantage of my solution is that the URL is still nice compared to a json encoded url :) – Christophe Blin Oct 14 '15 at 15:24