如何将一个对象序列化成查询字符串格式?

101

我如何将一个对象序列化为查询字符串格式?在谷歌上好像找不到答案。谢谢。

这是我将要作为示例序列化的对象。

public class EditListItemActionModel
{
    public int? Id { get; set; }
    public int State { get; set; }
    public string Prefix { get; set; }
    public string Index { get; set; }
    public int? ParentID { get; set; }
}

为什么不创建自己的函数来进行序列化呢? - James Black
你想得到这样的结果吗:Id=1&State=CA&Prefix=Mr...之类的吗?如果是的话,我同意@James的观点。 - Bob Kaufman
4
哇,那是唯一的方法吗?我以为 .NET 中有某种内置的东西。我的想法有点像 MVC 模型绑定器的反向操作。一定有这样的方法吧? - Benjamin
如果没有内置函数,你能给我一些提示如何编写一个吗? - Benjamin
3
Flurl是一个URL构建器/HTTP客户端,广泛使用对象表示名称-值对类似的东西(查询字符串、标头、URL编码表单值等)。SetQueryParams可以完全满足您的需求。如果您只需要URL构建器而不是所有的HTTP内容,则可以在此处找到它(https://www.nuget.org/packages/Flurl/)。[免责声明:我是这个项目的作者] - Todd Menier
显示剩余8条评论
14个回答

142

我99%确定没有内置的实用程序可以完成这项任务。这不是一个非常常见的任务,因为Web服务器通常不会使用URLEncoded键/值字符串进行响应。

你对混合反射和LINQ感觉如何?这个方法可以工作:

var foo = new EditListItemActionModel() {
  Id = 1,
  State = 26,
  Prefix = "f",
  Index = "oo",
  ParentID = null
};

var properties = from p in foo.GetType().GetProperties()
                 where p.GetValue(foo, null) != null
                 select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(foo, null).ToString());

// queryString will be set to "Id=1&State=26&Prefix=f&Index=oo"                  
string queryString = String.Join("&", properties.ToArray());

更新:

要编写一个返回任何一层对象的QueryString表示的方法,你可以这样做:

public string GetQueryString(object obj) {
  var properties = from p in obj.GetType().GetProperties()
                   where p.GetValue(obj, null) != null
                   select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString());

  return String.Join("&", properties.ToArray());
}

// Usage:
string queryString = GetQueryString(foo);

你也可以将它创建为一个扩展方法,而且几乎不需要额外的工作

public static class ExtensionMethods {
  public static string GetQueryString(this object obj) {
    var properties = from p in obj.GetType().GetProperties()
                     where p.GetValue(obj, null) != null
                     select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString());

    return String.Join("&", properties.ToArray());
  }
}

// Usage:
string queryString = foo.GetQueryString();

这很好。我正在尝试将其制作为一个接受动态参数的函数,但我认为我搞砸了动态LINQ选择语法。public string SerializeWithDynamicLINQ(dynamic Thing) { var Properties = Thing.GetType().GetProperties().ToArray(); return "&" + Properties.select("Property.Name") + "=" + HttpUtility.UrlEncode( Properties.select("Property").GetValue(Thing, null).ToString()); } 我也不知道如何在评论中编写代码块。 - Benjamin
@Benjamin:我更新了我的答案,以帮助你解决这个问题。 - Dave Ward
1
既然你说这不是一个很常见的任务,那么在不必硬编码所有RouteValueDictionary值的情况下传递大量表单值的替代方法是什么? - Doug Chamberlain
2
你可以通过将每个属性的值仅分配给一个临时变量来使其更有效率,例如使用 let value = p.GetValue(obj, null) - WhatIsHeDoing
2
这仅适用于服务器上的英文Culture Info。如果您在Windows上设置特定的datetime格式,则此方法将无法正常工作,因为您应该将CultureInfo Invariant设置为ToString参数。在float / double中具有特定小数分隔符的问题也是如此。 - Tomas Kubes

26

使用 Json.Net 可以更加轻松地实现,通过对键值对进行序列化和反序列化。

这是一个代码示例:

using Newtonsoft.Json;
using System.Web;

string ObjToQueryString(object obj)
{
     var step1 = JsonConvert.SerializeObject(obj);

     var step2 = JsonConvert.DeserializeObject<IDictionary<string, string>>(step1);

     var step3 = step2.Select(x => HttpUtility.UrlEncode(x.Key) + "=" + HttpUtility.UrlEncode(x.Value));

     return string.Join("&", step3);
}

我使用这个,因为它可以直接用于DateTime属性。 - Ashley Kilgour
6
我喜欢它的简单性。对于平面对象来说非常棒,但是不适用于嵌套对象/列表。 - devklick
2
这也适用于System.Text.Json库,只需确保第2步中的IDictionary是<string,object>,然后在第3步中使用x.Value.ToString() - Jim Yarbro

21

在其他评论的好想法基础上,我创建了一个通用扩展方法.ToQueryString(),它可以用于任何对象。

public static class UrlHelpers
{
    public static string ToQueryString(this object request, string separator = ",")
    {
        if (request == null)
            throw new ArgumentNullException("request");

        // Get all properties on the object
        var properties = request.GetType().GetProperties()
            .Where(x => x.CanRead)
            .Where(x => x.GetValue(request, null) != null)
            .ToDictionary(x => x.Name, x => x.GetValue(request, null));

        // Get names for all IEnumerable properties (excl. string)
        var propertyNames = properties
            .Where(x => !(x.Value is string) && x.Value is IEnumerable)
            .Select(x => x.Key)
            .ToList();

        // Concat all IEnumerable properties into a comma separated string
        foreach (var key in propertyNames)
        {
            var valueType = properties[key].GetType();
            var valueElemType = valueType.IsGenericType
                                    ? valueType.GetGenericArguments()[0]
                                    : valueType.GetElementType();
            if (valueElemType.IsPrimitive || valueElemType == typeof (string))
            {
                var enumerable = properties[key] as IEnumerable;
                properties[key] = string.Join(separator, enumerable.Cast<object>());
            }
        }

        // Concat all key/value pairs into a string separated by ampersand
        return string.Join("&", properties
            .Select(x => string.Concat(
                Uri.EscapeDataString(x.Key), "=",
                Uri.EscapeDataString(x.Value.ToString()))));
    }
}

如果对象只包含基本类型或字符串,并且具有数组和通用列表类型的属性,则此方法也适用于它们。

试一试,欢迎评论:使用反射将对象序列化为查询字符串


8
为什么不把代码放在这里?它并不多。 - asakura89
只是一点语法糖如果 (request == null) throw new ArgumentNullException(nameof(request)); - Jan Skála

13

根据流行答案,我需要更新代码以支持数组。分享实现:

public string GetQueryString(object obj)
{
    var result = new List<string>();
    var props = obj.GetType().GetProperties().Where(p => p.GetValue(obj, null) != null);
    foreach (var p in props)
    {
        var value = p.GetValue(obj, null);
        var enumerable = value as ICollection;
        if (enumerable != null)
        {
            result.AddRange(from object v in enumerable select string.Format("{0}={1}", p.Name, HttpUtility.UrlEncode(v.ToString())));
        }
        else
        {
            result.Add(string.Format("{0}={1}", p.Name, HttpUtility.UrlEncode(value.ToString())));
        }
    }

    return string.Join("&", result.ToArray());
}

5

这对于嵌套对象也很有用。

public static class HttpQueryStrings
{
    private static readonly StringBuilder _query = new();

    public static string ToQueryString<T>(this T @this) where T : class
    {
        _query.Clear();

        BuildQueryString(@this, "");

        if (_query.Length > 0) _query[0] = '?';

        return _query.ToString();
    }

    private static void BuildQueryString<T>(T? obj, string prefix = "") where T : class
    {
        if (obj == null) return;

        foreach (var p in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (p.GetValue(obj, Array.Empty<object>()) != null)
            {
                var value = p.GetValue(obj, Array.Empty<object>());


                if (p.PropertyType.IsArray && value?.GetType() == typeof(DateTime[]))
                    foreach (var item in (DateTime[])value)
                        _query.Append($"&{prefix}{p.Name}={item.ToString("yyyy-MM-dd")}");

                else if (p.PropertyType.IsArray)
                    foreach (var item in (Array)value!)
                        _query.Append($"&{prefix}{p.Name}={item}");

                else if (p.PropertyType == typeof(string))
                    _query.Append($"&{prefix}{p.Name}={value}");

                else if (p.PropertyType == typeof(DateTime) && !value!.Equals(Activator.CreateInstance(p.PropertyType))) // is not default 
                    _query.Append($"&{prefix}{p.Name}={((DateTime)value).ToString("yyyy-MM-dd")}");

                else if (p.PropertyType.IsValueType && !value!.Equals(Activator.CreateInstance(p.PropertyType))) // is not default 
                    _query.Append($"&{prefix}{p.Name}={value}");


                else if (p.PropertyType.IsClass)
                    BuildQueryString(value, $"{prefix}{p.Name}.");
            }
        }
    }
}

使用该解决方案的示例:

string queryString = new
{
    date = new DateTime(2020, 1, 1),
    myClass = new MyClass
    {
        FirstName = "john",
        LastName = "doe"
    },
    myArray = new int[] { 1, 2, 3, 4 },
}.ToQueryString();

4
也许这个通用的方法会有所帮助:
    public static string ConvertToQueryString<T>(T entity) where T: class
    {
        var props = typeof(T).GetProperties();

        return $"?{string.Join('&', props.Where(r=> r.GetValue(entity) != null).Select(r => $"{HttpUtility.UrlEncode(r.Name)}={HttpUtility.UrlEncode(r.GetValue(entity).ToString())}"))}";
    }

它是否适用于具有其属性的嵌套属性? - Konrad Viltersten

3
这是我的解决方案:
public static class ObjectExtensions
{
    public static string ToQueryString(this object obj)
    {
        if (!obj.GetType().IsComplex())
        {
            return obj.ToString();
        }

        var values = obj
            .GetType()
            .GetProperties()
            .Where(o => o.GetValue(obj, null) != null);

        var result = new QueryString();

        foreach (var value in values)
        {
            if (!typeof(string).IsAssignableFrom(value.PropertyType) 
                && typeof(IEnumerable).IsAssignableFrom(value.PropertyType))
            {
                var items = value.GetValue(obj) as IList;
                if (items.Count > 0)
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        result = result.Add(value.Name, ToQueryString(items[i]));
                    }
                }
            }
            else if (value.PropertyType.IsComplex())
            {
                result = result.Add(value.Name, ToQueryString(value));
            }
            else
            {
                result = result.Add(value.Name, value.GetValue(obj).ToString());
            }
        }

        return result.Value;
    }

    private static bool IsComplex(this Type type)
    {
        var typeInfo = type.GetTypeInfo();
        if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            // nullable type, check if the nested type is simple.
            return IsComplex(typeInfo.GetGenericArguments()[0]);
        }
        return !(typeInfo.IsPrimitive
          || typeInfo.IsEnum
          || type.Equals(typeof(Guid))
          || type.Equals(typeof(string))
          || type.Equals(typeof(decimal)));
    }
}

我在集成测试中使用这个扩展程序,它完美地发挥了作用 :)


3
public static class UrlHelper
{
    public static string ToUrl(this Object instance)
    {
        var urlBuilder = new StringBuilder();
        var properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
        for (int i = 0; i < properties.Length; i++)
        {
            urlBuilder.AppendFormat("{0}={1}&", properties[i].Name, properties[i].GetValue(instance, null));
        }
        if (urlBuilder.Length > 1)
        {
            urlBuilder.Remove(urlBuilder.Length - 1, 1);
        }
        return urlBuilder.ToString();
    }
}

这会破坏日期数值。 - undefined

2

这只是上述内容的另一种变体,但我想利用模型类中现有的DataMember属性,因此在GET请求的url中只发送要序列化的属性到服务器。

    public string ToQueryString(object obj)
    {
        if (obj == null) return "";

        return "?" + string.Join("&", obj.GetType()
                                   .GetProperties()
                                   .Where(p => Attribute.IsDefined(p, typeof(DataMemberAttribute)) && p.GetValue(obj, null) != null)
                                   .Select(p => $"{p.Name}={Uri.EscapeDataString(p.GetValue(obj).ToString())}"));
    }

1

这是我写的一些代码,可以满足你的需求。

    public string CreateAsQueryString(PageVariables pv) //Pass in your EditListItemActionModel instead
    {
        int i = 0;
        StringBuilder sb = new StringBuilder();

        foreach (var prop in typeof(PageVariables).GetProperties())
        {
            if (i != 0)
            {
                sb.Append("&");
            }

            var x = prop.GetValue(pv, null).ToString();

            if (x != null)
            {
                sb.Append(prop.Name);
                sb.Append("=");
                sb.Append(x.ToString());
            }

            i++;
        }

        Formating encoding = new Formating();
        // I am encoding my query string - but you don''t have to
        return "?" + HttpUtility.UrlEncode(encoding.RC2Encrypt(sb.ToString()));  
    }

1
这也可以很容易地采用object - ChaosPandion
@TheGeekYouNeed 谢谢!我会试一下。我很惊讶居然没有内置的功能。这个方法也能捕获继承属性吗? - Benjamin
本杰明,我想是这样的 - 我不完全记得,因为我写这段代码已经有一段时间了,但当我看到你的问题时,我想起来了。 - TheGeekYouNeed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接