3

I'm trying to accept a query string of the form

?param[key1]=value1&param[key2]=value2

or alternatively

?where[column1]=value1&where[column2]=value1
&orderby[column1]=asc&orderby[column2]=desc

and convert it into a Dictionary in C# MVC 4 Web API so it can be accessed as param["key1"]. This is trivial to do in PHP, but I haven't been able to find any method of reproducing this in C#.

I know I can take an array via

?param=value1&param=value2

so that I could do something like

?where=column1&where=column2
&whereval=value1&whereval=value2
&orderby=column1&orderby=column2
&orderbycard=asc&orderbycard=desc

but that leaves issues of ordering and isn't nearly as useful for my purposes.

In the hopes that this isn't a limitation of the framework, how might I implement a dictionary-style conversion in C#, preferably so that I can take dictionaries in the parameters of functions?

To clarify: I'm not looking to convert a query string into an NVC, but rather to convert query string parameters into their own NVCs. This is NOT as simple as ParseQueryString() or the Request.QueryString object.

This question was closed last time on the grounds of being a duplicate of this question, so I've added clarification. I'd ask that anyone considering marking this as a duplicate consider what I'm asking here before doing so, as well as taking a serious look at my examples.

Community
  • 1
  • 1
Dan Ambrisco
  • 865
  • 7
  • 13
  • can you show the example in a url so that one can post an alternative solution..? – MethodMan Feb 01 '13 at 18:33
  • A url like `http://example.com/api/objects?where[id]=4&orderby[id]=asc`? – Dan Ambrisco Feb 01 '13 at 18:36
  • Can you expand on how you expect to access the params in the final result? Would a key be "where[column]" or would it just be "column"? Also, what's the expected value in either case? – Ahmad Mageed Feb 01 '13 at 20:29
  • I would expect to access a dictionary `where` with key `column1` as `where["column1"]` with the value being `value1`. – Dan Ambrisco Feb 01 '13 at 21:50

3 Answers3

4

There's no built in way to achieve that result. Below is my attempt at it, using a regex with named groups. It's possible to skip the split and deal with the ? and & characters within the regex itself, but I took this approach to keep the pattern cleaner.

string input = "?where[column]=value&where[column2]=value2&orderby[column]=asc&orderby[column2]=desc";
string pattern = @"(?<Type>[^[]+)\[(?<Key>[^]]+)\]=(?<Value>.+)";
var re = new Regex(pattern);
var dict = input.Split(new[] { "?", "&" }, StringSplitOptions.RemoveEmptyEntries)
                .Select(s => re.Match(s))
                .GroupBy(m => m.Groups["Type"].Value)
                .ToDictionary(g => g.Key,
                    g => g.ToDictionary(x => x.Groups["Key"].Value,
                                        x => x.Groups["Value"].Value));

foreach (var t in dict.Keys)
{
    Console.WriteLine("Type: " + t);
    foreach (var k in dict[t].Keys)
    {
        Console.WriteLine("{0}: {1}", k, dict[t][k]);
    }
}

Of course, this relies on you knowing the keys to check for before hand, e.g., dict["where"]["column"]

You might find the ContainsKey and TryGetValue methods helpful to check for the existence of a key:

string result;
if (dict["where"].TryGetValue("column", out result))
{
    Console.WriteLine(result);
}
Ahmad Mageed
  • 94,561
  • 19
  • 163
  • 174
0

you could try something like this

var queryString = "http://example.com/api/objects?where[id]=4&orderby[id]=asc?";
string[] arr = queryString.Split(new char[] { '&' });
int cntVar = 0; // start key
Dictionary<int, string> dictQuery;
dictQuery = arr.ToDictionary(d => ++cntVar);
MethodMan
  • 18,625
  • 6
  • 34
  • 52
0

I wanted to share with you what I came up with, based off of Ahmad Mageed work. (It was a great start by the way.) I needed the same functionality, but I needed it to be able to accept much more depth in the index chain.

This allows you to be able to read as many sub indexes as you need, without hard coding it to stop at 2 or 3 levels deep. So with this update you could read such a query string as so...

columns[0][data]=0&columns[0][name]=&columns[0][searchable]=true  
&columns[0][orderable]=true&columns[0][search][value]=  
&columns[0][search][regex]=false&columns[1][data]=1  
&columns[1][name]=&columns[1][searchable]=true  
&columns[1][orderable]=true&columns[1][search][value]=  
&columns[1][search][regex]=false&columns[2][data]=2  
...  
&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false

Here is my updated version of that part of the code.

public IDictionary ConvertQueryString(string sQry)
{
    string pattern = @"(?<Prop>[^[]+)(?:\[(?<Key>[^]]+)\])*=(?<Value>.*)";
    // The brackets seem to be encoded as %5B and %5D
    var qry = HttpUtility.UrlDecode(sQry);

    var re = new Regex(pattern);
    var dict = qry.Split(new[] { "?", "&" }, StringSplitOptions.RemoveEmptyEntries)
               .Select(s => re.Match(s)).Where(g => g.Success)
               .GroupBy(m => m.Groups["Prop"].Value)
               .ToDictionary<IGrouping<string, Match>, string, object>(
                    g => g.Key, 
                    g => GetKey(g, 0));
    return dict;
}

private object GetKey(IGrouping<string, Match> grouping, int level)
{
    var count = grouping.FirstOrDefault().Groups["Key"].Captures.Count;
    // If the level is equal to the captures, then we are at the end of the indexes
    if (count == level)
    {
        var gValue = grouping.Where(gr => gr.Success).FirstOrDefault();
        var value = gValue.Groups["Value"].Value;
        return value;
    }
    else
    {
        return grouping.Where(gr => gr.Success)
                .GroupBy(m => m.Groups["Key"].Captures[level].Value)
                .ToDictionary<IGrouping<string, Match>, string, object>(
                        a => a.Key, 
                        a => GetKey(a, level + 1));
    }
}

I wanted to go a little further than just building a nested dictionary object, I wanted to actually build objects based on the passed string. I needed this for the new release of jQuery DataTables, it's actually what produces that nasty looking query string above. It was a quick write so it is a little crude and probably a little error prone as I have not had time yet to harden it.

// Call it like so.
MyClass object = new MyClass();
WalkDictionary(dict, object);
// At this point the object will be filled out for you.

public class MyClass
{
    public List<cColumns> columns { get; set; }
    public cSearch search { get; set; }
}
public class cColumns
{
    public int? data { get; set; }
    public string name { get; set; }
    public bool? searchable { get; set; }
    public bool? orderable { get; set; }
    public cSearch search { get; set; }
}
public class cSearch
{
    public string value { get; set; }
    public bool? regex { get; set; }
}

I only put the basic Types that I needed. So, if you need/want more, then you will have to add them.

private void WalkDictionary(IDictionary dict, object obj)
{
    foreach (string key in dict.Keys)
    {
        Type t = obj.GetType();
        if (t.IsGenericType
            && typeof(List<>) == t.GetGenericTypeDefinition())
        {
            Type[] typeParameters = t.GetGenericArguments();
            foreach (DictionaryEntry item in dict)
            {
                var lstPropObj = Activator.CreateInstance(typeParameters[0]);
                WalkDictionary((IDictionary)item.Value, lstPropObj);
                ((IList)obj).Add(lstPropObj);
            }
            return;
        }
        PropertyInfo prop = obj.GetType().GetProperty(key);
        if (prop == null)
            continue;
        if (dict[key] is IDictionary)
        {
            //Walk
            var objProp = prop.GetValue(obj);
            if (objProp == null)
                objProp = Activator.CreateInstance(prop.PropertyType);
            WalkDictionary(dict[key] as IDictionary, objProp);
            prop.SetValue(obj, objProp);
        }
        else if (prop.PropertyType.IsAssignableFrom(typeof(int)))
        {
            int val = Convert.ToInt32(dict[key]);
            prop.SetValue(obj, val);
        }
        else if (prop.PropertyType.IsAssignableFrom(typeof(bool)))
        {
            bool val = Convert.ToBoolean(dict[key]);
            prop.SetValue(obj, val);
        }
        else if (prop.PropertyType.IsAssignableFrom(typeof(string)))
        {
            string val = Convert.ToString(dict[key]);
            prop.SetValue(obj, val);
        }
    }
}
John C
  • 1,761
  • 2
  • 20
  • 30