【问题标题】:How to send complex objects in GET to WEB API 2如何将 GET 中的复杂对象发送到 WEB API 2
【发布时间】:2015-10-13 09:32:59
【问题描述】:

假设你有以下代码

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;
}

我想知道是否有人知道可以将任何对象转换为 WEB API 2 控制器可以理解的查询字符串的库。

这是我想要的示例

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"

httpClient 的完整示例可能如下所示:

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;

目前,我使用一个版本(取自一个我再也找不到的问题,也许是How do I serialize an object into query-string format? ...)。

问题在于,除了简单的属性外,它不适用于其他任何东西。 例如,在 Date 上调用 ToString 不会给出 WEB API 2 控制器可解析的内容...

    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();
    }

【问题讨论】:

  • 为什么不将它作为 json 发布?
  • 因为 HTTP 动词在 REST 中具有语义。例如 POST /products 进行搜索很尴尬,因为您会认为这是一个创作
  • 序列化为 JSON 和 URL 编码?
  • @ChristopheBlin,正确我的意思是为什么不在 GET 请求中使用 JSON 有效负载,但无论如何这似乎不是一个好习惯。
  • @Jehof 我不反对在查询字符串中使用 JSON 有效负载,但是这不适用于开箱即用的 WEB API 2...

标签: asp.net-web-api dotnet-httpclient


【解决方案1】:

“将任何对象转换为查询字符串”似乎暗示对此有标准格式,但实际上并没有。所以你需要选择一个或自己滚动。由于可用的优秀库,JSON 似乎是显而易见的选择。

【讨论】:

  • 是的,但您对处理 any 对象类型的要求已满足。在服务器上反序列化是单行的,或者您可以使用自定义 ModelBinder 将其完全隐藏。
  • 感谢您的想法!您的解决方案有效,但我不想牺牲 URL 的可读性(即 { " : 将被 urlencoded,导致 URL 很难阅读)。而且您也对,我提出的解决方案不适用于任何对象跨度>
【解决方案2】:

由于之前似乎没有人处理过这个问题,所以这是我在项目中使用的解决方案:

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();
            }
        }
    }
}

这是要演示的单元测试:

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"));
        }
    }
}

我要感谢所有参与者,即使这不是你通常应该以问答形式做的事情:)

【讨论】:

  • 这似乎属于“自己动手”类别,与使用现有的序列化格式相比有很多限制。但如果它适用于您的目的,那就太酷了。 :)
  • @ToddMenier:你说得对。我的解决方案的唯一优点是与 json 编码的 url 相比,该 URL 仍然很好:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-01
  • 1970-01-01
  • 2018-01-22
  • 1970-01-01
  • 2017-06-19
  • 2016-06-12
  • 2014-12-19
相关资源
最近更新 更多