在C#中如何将匿名类型转换为键/值数组?

81

我有以下匿名类型:

new {data1 = "test1", data2 = "sam", data3 = "bob"}

我需要一个方法,能够将这个内容输入进去,并输出一组键值对到数组或字典中。

我的目标是在HttpRequest中使用它作为post数据,因此最终会将其连接成以下字符串:

"data1=test1&data2=sam&data3=bob"
9个回答

126

只需要稍微思考一下就可以完成。

var a = new { data1 = "test1", data2 = "sam", data3 = "bob" };
var type = a.GetType();
var props = type.GetProperties();
var pairs = props.Select(x => x.Name + "=" + x.GetValue(a, null)).ToArray();
var result = string.Join("&", pairs);

7
var dict = props.ToDictionary(x => x.Name, x => x.GetValue(a_source, null)) - Jordan
37
我们已经走到了这一步……我们可以将其简化为一行代码:var dict = a.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(a, null)); - nikib3ro
@kape123,确实。事实上,最新的.NET版本不再需要调用ToArray(),这很好。无论如何,当前的响应在SO上适合得很好,没有换行,所以我会保持原样。 - kbrimington
@kape123的答案非常有效,我目前还没有发现任何问题。 kbrimington,您能否(或者我应该)将其添加到答案中? - Xan-Kun Clark-Davis
@Xan-KunClark-Davis,kape的回答很好;然而,它实际上是相同的答案。这就是为什么我点赞他的评论而不是将其整合到我的回复中。现在,在最新的.NET框架上,我会将整个内容打包成扩展方法。这将提高可重用性和清晰度。 - kbrimington

64
如果您正在使用.NET 3.5 SP1或.NET 4,您可以(滥用)RouteValueDictionary来实现。它实现了IDictionary<string, object>,并具有接受object并将属性转换为键值对的构造函数。
然后轻松遍历键和值以建立您的查询字符串。

4
我使用“滥用”这个词是因为该类最初是为路由设计的(或者至少它的名称和命名空间暗示了这一点)。然而,该类并不包含任何特定于路由的功能,并且已经被用于其他功能(例如,在ASP.NET MVCHtmlHelper扩展方法中将匿名对象转换为字典以用作HTML属性)。 - GWB
我已经做了这个,但现在我需要从RouteValueDictionary返回到匿名对象,有什么想法吗? - Joshy
1
这甚至不算滥用...微软也这么做。例如,当您调用HtmlHelper.TextBox时...您应该传递一个匿名类型来设置属性值。这实际上会在Razor中导致绑定错误(例如尝试调用@Html.Partial(“〜/Shared/_PartialControl.cshtml”,new {id =“id”,value =“value”}),即使在局部视图中声明了@model dynamic,它也会抛出绑定错误。TextBox方法内部调用“public static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes)”,它返回一个RouteValueDictionary。所以就是这样。 - Triynko
@Triynko 这意味着微软也在滥用它。将来,如果他们可能改变功能以执行特定于路由的操作。我建议基于源代码创建自己的实现,并给它一个更合适的名称。 - Nick Coad
6
除非根据你的代码库,它将强制使用 System.Web 的依赖项,否则可以考虑使用。有点希望该类更通用化并位于其他命名空间中或更高层次。 - Nick Albrecht
显示剩余2条评论

29

这是在RouteValueDictionary中的实现方式:

  private void AddValues(object values)
    {
        if (values != null)
        {
            foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
            {
                object obj2 = descriptor.GetValue(values);
                this.Add(descriptor.Name, obj2);
            }
        }
    }

完整源代码在这里: http://pastebin.com/c1gQpBMG


我尝试使用pastebin上的代码,但Visual Studio提示说一些Dictionary方法未实现。我不得不进行显式转换为IDictionary。我只是将一些“this._dictionary”更改为“((IDictionary<string,object>)this._dictionary)” - Walter Stabosz

5

有一种内置的方法可以将匿名对象转换为字典:

HtmlHelper.AnonymousObjectToHtmlAttributes(yourObj)

它还返回 RouteValueDictionary。请注意它是静态的。

根据https://learn.microsoft.com/en-us/previous-versions/aspnet/mt171927(v%3Dvs.118)的描述,“将指定HTML属性中的下划线字符(_)替换为连字符(-)”,在某些情况下可能会成为问题。 - IulianT

3
using Newtonsoft.Json;
var data = new {data1 = "test1", data2 = "sam", data3 = "bob"};
var encodedData = new FormUrlEncodedContent(JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(data))

3
请给答案加一些解释,仅有代码的回答会浪费审阅者的时间,而且常常被误解,甚至可能被删除。 - Munim Munna

3

虽然现在已经晚了,但我想为了更加稳健的解决方案而补充一些内容。 我在这里看到的一些方法存在一些问题(比如它们可能无法正确地处理 DateTime)。因此,我建议首先将其转换为 JSON(Newtonsoft Json.Net):

var data = new {data1 = "test1", data2 = "sam", data3 = "bob"};

var result = string.Join("&",
            JsonConvert.DeserializeObject<Dictionary<string, string>>(
            JsonConvert.SerializeObject(data))
            .Select(x => $"{x.Key}={x.Value}")
        );

1

@kbrimington的解决方案是一个不错的扩展方法 - 在我的情况下返回一个HtmlString

    public static System.Web.HtmlString ToHTMLAttributeString(this Object attributes)
    {
        var props = attributes.GetType().GetProperties();
        var pairs = props.Select(x => string.Format(@"{0}=""{1}""",x.Name,x.GetValue(attributes, null))).ToArray();
        return new HtmlString(string.Join(" ", pairs));
    }

我正在使用它将任意属性放入Razor MVC视图中。我最初使用RouteValueDictionary和循环结果的代码,但这种方法更加简洁。


7
这个功能已经存在于代码库中(至少现在是这样):HtmlHelper.AnonymousObjectToHtmlAttributes - Andrew

1
我做了类似这样的事情:

public class ObjectDictionary : Dictionary<string, object>
{
    /// <summary>
    /// Construct.
    /// </summary>
    /// <param name="a_source">Source object.</param>
    public ObjectDictionary(object a_source)
        : base(ParseObject(a_source))
    {

    }

    /// <summary>
    /// Create a dictionary from the given object (<paramref name="a_source"/>).
    /// </summary>
    /// <param name="a_source">Source object.</param>
    /// <returns>Created dictionary.</returns>
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="a_source"/> is null.</exception>
    private static IDictionary<String, Object> ParseObject(object a_source)
    {
        #region Argument Validation

        if (a_source == null)
            throw new ArgumentNullException("a_source");

        #endregion

        var type = a_source.GetType();
        var props = type.GetProperties();

        return props.ToDictionary(x => x.Name, x => x.GetValue(a_source, null));
    }
}

1
在@GWB的建议基础上,使用RouteValueDictionary,我编写了这个递归函数来支持嵌套匿名类型,通过添加其父级键的前缀来处理这些嵌套参数。
public static string EncodeHtmlRequestBody(object data, string parent = null) {
    var keyValuePairs = new List<string>();
    var dict = new RouteValueDictionary(data);

    foreach (var pair in dict) {
        string key = parent == null ? pair.Key : parent + "." + pair.Key;
        var type = pair.Value.GetType();
        if (type.IsPrimitive || type == typeof(decimal) || type == typeof(string)) {
            keyValuePairs.Add(key + "=" + Uri.EscapeDataString((string)pair.Value).Replace("%20", "+"));
        } else {
            keyValuePairs.Add(EncodeHtmlRequestBody(pair.Value, key));
        }
    }

    return String.Join("&", keyValuePairs);
}

使用示例:

var data = new {
    apiOperation = "AUTHORIZE",
    order = new {
        id = "order123",
        amount = "101.00",
        currency = "AUD"
    },
    transaction = new {
        id = "transaction123"
    },
    sourceOfFunds = new {
        type = "CARD",
        provided = new {
            card = new {
                expiry = new {
                    month = "1",
                    year = "20"
                },
                nameOnCard = "John Smith",
                number = "4444333322221111",
                securityCode = "123"
            }
        }
    }
};

string encodedData = EncodeHtmlRequestBody(data);

encodedData 变成了:

"apiOperation=AUTHORIZE&order.id=order123&order.amount=101.00&order.currency=AUD&transaction.id=transaction123&sourceOfFunds.type=CARD&sourceOfFunds.provided.card.expiry.month=1&sourceOfFunds.provided.card.expiry.year=20&sourceOfFunds.provided.card.nameOnCard=John+Smith&sourceOfFunds.provided.card.number=4444333322221111&sourceOfFunds.provided.card.securityCode=123"

希望这可以帮助到其他遇到类似情况的人。
编辑:如DrewG所指出的,这不支持数组。要正确实现对带有匿名类型的任意嵌套数组的支持是很困难的,并且由于我使用的API都没有接受数组(我甚至不确定是否有标准化的方式将它们序列化为表单编码),如果您需要支持它们,我会留给你们。

注意,这段代码无法正确处理数组。会导致堆栈溢出。 需要类似以下的代码: if (type.IsArray) { var arr = pair.Value as string[]; if (arr != null) { foreach (var s in arr) { keyValuePairs.Add((key + "[]=" + s).Replace(" ", "+")); } } } - DrewG

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