8

I need to pass some data to a PHP page on a server from my C# client using HttpWebRequest. The expected data according to the documentation is an array of arrays, something like this:

$postData = array(
    'label1' => 'myLabel',
    'label2' => array(
        'label2_1' => 3
        'label2_2' => array(
            'label2_2_1' => 3
        )
    )
);

The structure above is just an example. It can be very complicated and the structure itself is not constant.

In PHP there is a function named http_build_query which serializes these PHP nested arrays to a simple string, which can be sent as the data of a HTTP POST request. The problem is I need to call this PHP page from my C# application. I would like to represent these nested arrays either as nested Dictionary<string, object>s, or anonymous types.

How can I do that? What rules does http_build_query follow to produce its output string?

There is a very similar question Converting PHP array of arrays to C#, which does not solve my problem, unfortunately. The accepted answer recommends a solution for a fixed structure, the second one does not work at all.

2 Answers 2

17
+50

Well, there doesn't seem to be anything built-in to .NET to allow you to do this. However, if you want to re-implement the PHP behavior in .NET, you can either white-box implement it by taking a look at the PHP source code, or black-box implement it by reading the PHP documentation of http_build_query and testing the function out on various inputs.

I took a black-box approach and created the following class:

/// <summary>
///  Helps up build a query string by converting an object into a set of named-values and making a
///  query string out of it.
/// </summary>
public class QueryStringBuilder
{
  private readonly List<KeyValuePair<string, object>> _keyValuePairs
    = new List<KeyValuePair<string, object>>();

  /// <summary> Builds the query string from the given instance. </summary>
  public static string BuildQueryString(object queryData, string argSeperator = "&")
  {
    var encoder = new QueryStringBuilder();
    encoder.AddEntry(null, queryData, allowObjects: true);

    return encoder.GetUriString(argSeperator);
  }

  /// <summary>
  ///  Convert the key-value pairs that we've collected into an actual query string.
  /// </summary>
  private string GetUriString(string argSeperator)
  {
    return String.Join(argSeperator,
                       _keyValuePairs.Select(kvp =>
                                             {
                                               var key = Uri.EscapeDataString(kvp.Key);
                                               var value = Uri.EscapeDataString(kvp.Value.ToString());
                                               return $"{key}={value}";
                                             }));
  }

  /// <summary> Adds a single entry to the collection. </summary>
  /// <param name="prefix"> The prefix to use when generating the key of the entry. Can be null. </param>
  /// <param name="instance"> The instance to add.
  ///  
  ///  - If the instance is a dictionary, the entries determine the key and values.
  ///  - If the instance is a collection, the keys will be the index of the entries, and the value
  ///  will be each item in the collection.
  ///  - If allowObjects is true, then the object's properties' names will be the keys, and the
  ///  values of the properties will be the values.
  ///  - Otherwise the instance is added with the given prefix to the collection of items. </param>
  /// <param name="allowObjects"> true to add the properties of the given instance (if the object is
  ///  not a collection or dictionary), false to add the object as a key-value pair. </param>
  private void AddEntry(string prefix, object instance, bool allowObjects)
  {
    var dictionary = instance as IDictionary;
    var collection = instance as ICollection;

    if (dictionary != null)
    {
      Add(prefix, GetDictionaryAdapter(dictionary));
    }
    else if (collection != null)
    {
      Add(prefix, GetArrayAdapter(collection));
    }
    else if (allowObjects)
    {
      Add(prefix, GetObjectAdapter(instance));
    }
    else
    {
      _keyValuePairs.Add(new KeyValuePair<string, object>(prefix, instance));
    }
  }

  /// <summary> Adds the given collection of entries. </summary>
  private void Add(string prefix, IEnumerable<Entry> datas)
  {
    foreach (var item in datas)
    {
      var newPrefix = String.IsNullOrEmpty(prefix)
        ? item.Key
        : $"{prefix}[{item.Key}]";

      AddEntry(newPrefix, item.Value, allowObjects: false);
    }
  }

  private struct Entry
  {
    public string Key;
    public object Value;
  }

  /// <summary>
  ///  Returns a collection of entries that represent the properties on the object.
  /// </summary>
  private IEnumerable<Entry> GetObjectAdapter(object data)
  {
    var properties = data.GetType().GetProperties();

    foreach (var property in properties)
    {
      yield return new Entry()
                   {
                     Key = property.Name,
                     Value = property.GetValue(data)
                   };
    }
  }

  /// <summary>
  ///  Returns a collection of entries that represent items in the collection.
  /// </summary>
  private IEnumerable<Entry> GetArrayAdapter(ICollection collection)
  {
    int i = 0;
    foreach (var item in collection)
    {
      yield return new Entry()
                   {
                     Key = i.ToString(),
                     Value = item,
                   };
      i++;
    }
  }

  /// <summary>
  ///  Returns a collection of entries that represent items in the dictionary.
  /// </summary>
  private IEnumerable<Entry> GetDictionaryAdapter(IDictionary collection)
  {
    foreach (DictionaryEntry item in collection)
    {
      yield return new Entry()
                   {
                     Key = item.Key.ToString(),
                     Value = item.Value,
                   };
    }
  }
}

The code is pretty self-explanatory, but it accepts a dictionary, an array, or an object. If it's an top-level object, it serialized the properties. If it's an array, each element is serialized with the appropriate array index. If it's a dictionary, the key/values are serialized. Arrays and Dictionary-values that contain other arrays or dictionaries are flattened, similar to PHPs behavior.

For example, the following:

QueryStringBuilder.BuildQueryString(new
       {
         Age = 19,
         Name = "John&Doe",
         Values = new object[]
                  {
                    1,
                    2,
                    new Dictionary<string, string>()
                    {
                      { "key1", "value1" },
                      { "key2", "value2" },
                    }
                  },
       });

// 0=1&1=2&2%5B0%5D=one&2%5B1%5D=two&2%5B2%5D=three&3%5Bkey1%5D=value1&3%5Bkey2%5D=value2
QueryStringBuilder.BuildQueryString(new object[]
       {
         1,
         2,
         new object[] { "one", "two", "three" },
         new Dictionary<string, string>()
         {
           { "key1", "value1" },
           { "key2", "value2" },
         }
       }
  );

Generates:

Age=19&Name=John%26Doe&Values%5B0%5D=1&Values%5B1%5D=2&Values%5B2%5D%5Bkey1%5D=value1&Values%5B2%5D%5Bkey2%5D=value2

which is:

Age=19&Name=John%26Doe&Values[0]=1&Values[1]=2&Values[2][key1]=value1&Values[2][key2]=value2
Age=19
Name=John&Doe
Values[0]=1
Values[1]=2
Values[2][key1]=value1
Values[2][key2]=value2
Sign up to request clarification or add additional context in comments.

2 Comments

This is a gem! @MackieChan
This is golden indeed. You can edit with if (allowObjects && instance.GetType().IsClass && instance.GetType().Name != "String") and set allowObjects to true inside the Add function so you can have Dictionaries with objects.
3

Using NameValueCollection you could do this:

private string ToQueryString(NameValueCollection queryData)
{
    var array = (from key in queryData.AllKeys
        from value in queryData.GetValues(key)
        select string.Format(CultureInfo.InvariantCulture, "{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(value)))
        .ToArray();
    return "?" + string.Join("&", array);
}

1 Comment

How can I nest NameValueCollection inside a NameValueCollection? This is not going to work, I am afraid.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.