81

I have the following anonymous type:

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

I need a method that will take this in, and output key value pairs in an array or dictionary.

My goal is to use this as post data in an HttpRequest so i will eventually concatenate in into the following string:

"data1=test1&data2=sam&data3=bob"
Chris Kooken
  • 32,730
  • 15
  • 85
  • 123

9 Answers9

126

This takes just a tiny bit of reflection to accomplish.

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);
kbrimington
  • 25,142
  • 5
  • 62
  • 74
  • 7
    var dict = props.ToDictionary(x => x.Name, x => x.GetValue(a_source, null)) – Jordan Feb 19 '15 at 19:29
  • 37
    We got this far... we can make it a one liner: `var dict = a.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(a, null));` – nikib3ro Mar 21 '15 at 12:07
  • @kape123, indeed. In fact, the latest .NET versions no longer require the call to `ToArray()`, which is nice. At any rate, the response as it stands fits nicely in SO without word wrapping, so I'll leave it as it is. – kbrimington Mar 21 '15 at 23:11
  • The answer from @kape123 is working really well and I can't find any problems with it (so far). kbrimington, can you (or should I) add this to the answer? – Xan-Kun Clark-Davis Jan 18 '18 at 01:00
  • @Xan-KunClark-Davis, kape's answer is fine; however, it is really the same answer. That is why I upvoted his comment rather than integrate it into my response. Nowadays, on the latest .NET Frameworks, I would take the whole thing and bundle it up as an extension method. That would improve both reusability and clarity. – kbrimington Apr 06 '18 at 17:41
64

If you are using .NET 3.5 SP1 or .NET 4, you can (ab)use RouteValueDictionary for this. It implements IDictionary<string, object> and has a constructor that accepts object and converts properties to key-value pairs.

It would then be trivial to loop through the keys and values to build your query string.

GWB
  • 2,575
  • 2
  • 23
  • 28
  • 4
    I say "abuse" because the class was originally designed for routing (or at least its name and namespace imply this). However, it contains no routing-specific functionality and is already used for other features (like converting anonymous objects to dictionaries for HTML attributes in the ASP.NET MVC `HtmlHelper` extension methods. – GWB Aug 14 '10 at 04:30
  • I have done exactly this but now i need to go from the RouteValueDictionary back to the anomous object, any thoughts ?? – Joshy Dec 18 '13 at 00:38
  • 1
    This isn't even an abuse of it.. Microsoft does it. For example, when you call HtmlHelper.TextBox... you're supposed to pass an anonymous type to set the attribute values. That will actually cause binding error in Razor (e.g. try calling @Html.Partial("~/Shared/_PartialControl.cshtml", new {id="id",value="value"}), and it will throw a binding error, even with @model dynamic declared in the partial view. The TextBox method internally calls "public static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes)", which is returning a RouteValueDictionary. So there you go. – Triynko Jul 12 '15 at 17:54
  • @Triynko that just means Microsoft is abusing it as well. In the future if they may change the functionality to do something that is specific to routing. I would recommend making your own implementation based off the source and give it a more suitable name. – Nick Coad Aug 20 '15 at 05:36
  • @GWB, I say the opposite. The `RouteValueDictionary` was abused to be in `System.Web` instead of being in `System.Net` or `System.Net.Http`, in [corefx](https://github.com/dotnet/corefx). – Shimmy Weitzhandler Nov 02 '15 at 12:00
  • 6
    Except that depending on your code base, it would force a dependency on System.Web that might not otherwise be needed. Kind of wish the class was more genericised and sat higher up/in a different namespace. – Nick Albrecht Nov 15 '15 at 03:37
  • Not in .NET 4.7.1 or .NET Standard. – Shimmy Weitzhandler Dec 06 '17 at 05:20
29

Here is how they do it in 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);
            }
        }
    }

Full Source is here: http://pastebin.com/c1gQpBMG

  • I tried to use the code from pastebin and Visual Studio was saying that a bunch of the Dictionary methods were not implemented. I had to do an explicit cast to IDictionary. I just switched a couple of the "this._dictionary" to "((IDictionary)this._dictionary)" – Walter Stabosz Oct 05 '11 at 13:17
5

There is a built-in method of converting anonymous objects to dictionaries:

HtmlHelper.AnonymousObjectToHtmlAttributes(yourObj)

It also returns RouteValueDictionary. Note that it's static

Xymanek
  • 1,357
  • 14
  • 25
  • according to https://learn.microsoft.com/en-us/previous-versions/aspnet/mt171927(v%3Dvs.118) "Replaces underscore characters (_) with hyphens (-) in the specified HTML attributes" in some cases this might be a problem. – IulianT Jun 24 '20 at 15:43
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))
Konstantin Salavatov
  • 4,370
  • 2
  • 25
  • 24
  • 3
    Please add some explanation to the answer, code-only answers waste reviewer's time and is often misunderstood, can even get deleted. – Munim Munna May 08 '18 at 20:02
3

It is too late but anyway I would add this for a more robust solution. The ones I see here have some kind of problems (like they wouldn't work right with say DateTime). For that reason, I suggest first converting to a 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}")
        );
Cetin Basoz
  • 22,495
  • 3
  • 31
  • 39
1

I did something like this:

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));
    }
}
Jordan
  • 9,642
  • 10
  • 71
  • 141
1

Building on @GWB's suggestion of using a RouteValueDictionary, I wrote this recursive function to support nested anonymous types, prefixing those nested parameters by their parents' keys.

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

Example usage:

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 becomes:

"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"

Hope this helps someone else in a similar situation.

Edit: As DrewG pointed out, this doesn't support arrays. To properly implement support for arbitrarily nested arrays with anonymous types would be non-trivial, and as none of the APIs I've used have accepted arrays either (I'm not sure there's even a standardised way of serialising them with form encoding), I'll leave that to you folks if you need to support them.

Extragorey
  • 1,654
  • 16
  • 30
  • Heads up, this does not handle arrays properly. Will stack overflow. need something like this if (type.IsArray) { var arr = pair.Value as string[]; if (arr != null) { foreach (var s in arr) { keyValuePairs.Add((key + "[]=" + s).Replace(" ", "+")); } } } – DrewG Dec 21 '17 at 18:20
1

@kbrimington's solution makes a nice extension method - my my case returning a 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));
    }

I'm using it to drop arbitrary attributes into a Razor MVC view. I started with code using RouteValueDictionary and looping on the results but this is much neater.

Andiih
  • 12,285
  • 10
  • 57
  • 88
  • 7
    This already exists in the box (at least, it does now): `HtmlHelper.AnonymousObjectToHtmlAttributes` – Andrew Feb 17 '14 at 16:27