-4

I want to be able to query and sort a Dictionary<int, MyObj> by any of the properties inside MyObj.

class MyObj
  {
    public string Symbol { get; set; }
    public string Name { get; set; }
    public int AtomicNumber { get; set; }
    public int Id { get; set; }

    public string toString()
    {
      return Symbol + " " + Name + " " + AtomicNumber + " " + Id;
    }

  }

  class Program
  {

    private static void AddToDictionary(Dictionary<int, MyObj> elements,
    string symbol, string name, int atomicNumber, int id)
    {
      MyObj theElement = new MyObj();

      theElement.Symbol = symbol;
      theElement.Name = name;
      theElement.AtomicNumber = atomicNumber;
      theElement.Id = id;

      elements.Add(key: theElement.Id, value: theElement);
    }

    private static Dictionary<int, MyObj> BuildDictionary()
    {
      var elements = new Dictionary<int, MyObj>();

      AddToDictionary(elements, "K", "Potassium", 19, 0);
      AddToDictionary(elements, "Ca", "Calcium", 20, 1);
      AddToDictionary(elements, "Sc", "Scandium", 21, 2);
      AddToDictionary(elements, "Ti", "Titanium", 22, 3);

      return elements;
    }

    static List<T> GetListOfProperty<T>(Dictionary<int, MyObj> colBlobs, string property)
    {
      Type t = typeof(MyObj);
      PropertyInfo prop = t.GetProperty(property);
      if (prop == null)
      {
        // throw new Exception(string.Format("Property {0} not found", f.Name.ToLower()));
        Console.WriteLine(string.Format("Property {0} not found", property));
        return new List<T>();
      }
      //still need to fix this 
      return colBlobs.Values
          .Select(blob => (T)prop.GetValue(blob))
          .OrderBy(x => x)
          .ToList();
    }


    static SortedDictionary<int, MyObj> GetListOfProperty2<T>(Dictionary<int, MyObj> colBlobs, string property)
    {

      // CODE?

      return sortedDict;
    }


    static void Main(string[] args)
    {
      Dictionary<int, MyObj> myColl = BuildDictionary();

      var res = GetListOfProperty<string>(myColl, "Name");
      foreach (var rr in res)
        Console.WriteLine(rr.ToString());
      //outputs : Which is only one property, the one selected
      //--------
      //Calcium
      //Potassium
      //Scandium
      //Titanium

      var res2 = GetListOfProperty2<string>(myColl, "Name");
      //want to output the whole dictionary
      //<1, {"Ca", "Calcium", 20,1}
      //<0, {"K", "Potassium", 19, 0}
      //<2, {"Sc", "Scandium", 21, 2}
      //<3, {"Ti", "Titanium", 22, 3}
    }
  }

Since it seems to be that it is unclear what I want. I added example output. I am pretty sure there is no way to make this question more clear.

unixsnob
  • 1,685
  • 2
  • 19
  • 45
  • 3
    What do yo mean by "fails"? It looks like the method is supposed to return a list of dictionaries, but you're trying to just return a single one. I don't understand what you're trying to actually do. Dictionaries aren't sorted collections. If you want that, use a [SortedDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.sorteddictionary-2?view=netframework-4.8). – itsme86 Jul 05 '19 at 17:46
  • 1
    This can't work, because "x" has no Key and no Value. How should your dictionary look like? What is your expected key and the expected value? – Arthur S. Jul 05 '19 at 17:59
  • I don't see how ` .Select(blob => (T)prop.GetValue(blob))` does anything useful as `blob` is of type `KeyValue` but you are trying to get property for `MyObj`... – Alexei Levenkov Jul 05 '19 at 18:00
  • I love the fact that no one says how to do it instead. Just, this does not work, that does not work. I just pasted the whole code. The question is how do you structure a query on a dictionary of Objects to get a sorted Dictionary based on a property of the Objects. – unixsnob Jul 05 '19 at 18:05
  • @itsme86: SortedDictionary in. Still does not work. Fails == does not work. – unixsnob Jul 05 '19 at 18:07
  • 1
    @unixsnob The code you've posted does not compile for case you claim it work ("if I return list it works") and throws "Object does not match target type" as expected on `.Select(blob => (T)prop.GetValue(blob))` line when trying to convince that code to at least run... – Alexei Levenkov Jul 05 '19 at 18:08
  • @AlexeiLevenkov : well obviously as I am trying to return a dictionary. If I return a list of the values it works. The whole point of the question is HOW to do it with a dictionary. – unixsnob Jul 05 '19 at 18:09
  • Anyway, this question is going to get closed with no answer. I'll put another one just to keep the downvotes coming. – unixsnob Jul 05 '19 at 18:11
  • @unixsnob you should stop for a second and think what you are asking - signature of your method says `SortedDictionary` but you want to sort it get sorted by `string` property `Name`... It is *really* hard to see what you expect as result. – Alexei Levenkov Jul 05 '19 at 18:17
  • @AlexeiLevenkov I don't think this question should be closed. The provided code cold be made valid with a couple of changes. I would like to present an answer that works. –  Jul 05 '19 at 18:40
  • @AlexeiLevenkov: You are develivery not reading what the question says. It's literally on the first sentence! The string is for the property inside MyObj, not the Key. I want to preserve the pair and get a sorted Dictionary. So, if I want to sort by name property, I want a SortedDict of by property Name. – unixsnob Jul 05 '19 at 18:44
  • 1
    @HenrikHansen: Hi, I hope this is clear enough for you to submit an answer. I really don't get why people think it's so unclear. I just want to query and sort the dictionary by a property inside the object. So say, for example, get sorted pairs for `MyObj`'s AtomicNumber property > 20. – unixsnob Jul 05 '19 at 19:04
  • @unixsnob: Sure, its clear enough for me.But I can't answer as long as the question is on hold. –  Jul 05 '19 at 19:06
  • The question was edited by OP, so now it's pretty clear what OP wants to achieve. I would vote to make the question active. Ready to provide a solution – Dmitry Stepanov Jul 05 '19 at 20:08
  • unixsnob sorry. I got completely confused by code you have. Now it is clear that you are asking how to use [SortedDictionary with Comparer in C#](https://www.bing.com/search?q=c%23+sorteddictionary+comparer) - I'm voting to reopen the question so @Dmitry Stepanov can provide answer similar to https://stackoverflow.com/questions/2720009/how-to-use-custom-icomparer-for-sorteddictionary – Alexei Levenkov Jul 05 '19 at 20:08

2 Answers2

2

The problem with SortedDictionary is that it can only be sorted by Key, so you'll have to use OrderBy() in some way:

public static IOrderedEnumerable<KeyValuePair<K, T>> SortByMember<K, T>(this Dictionary<K, T> data, string memberName)
{
  Type type = typeof(T);
  MemberInfo info = type.GetProperty(memberName) ?? type.GetField(memberName) as MemberInfo;

  Func<KeyValuePair<K, T>, object> getter = kvp => kvp.Key;

  if (info is PropertyInfo pi)
    getter = kvp => pi.GetValue(kvp.Value);
  else if (info is FieldInfo fi)
    getter = kvp => fi.GetValue(kvp.Value);

  return data.OrderBy(getter);
}

This can handle both properties and fields, and if the member name is invalid, it defaults to sorting by key.

You can change that to not sort if member name is invalid by changing the return value:

public static IEnumerable<KeyValuePair<K, T>> SortByMember<K, T>(this Dictionary<K, T> data, string memberName)
{
  Type type = typeof(T);
  MemberInfo info = type.GetProperty(memberName) ?? type.GetField(memberName) as MemberInfo;

  if (info == null) return data;

  Func<KeyValuePair<K, T>, object> getter = null;

  if (info is PropertyInfo pi)
    getter = kvp => pi.GetValue(kvp.Value);
  else if (info is FieldInfo fi)
    getter = kvp => fi.GetValue(kvp.Value);

  return data.OrderBy(getter);
}

IMO it is wrong to return an empty dictionary if it fails to find the member. Alternatively you can throw an exception.

Use case:

  Dictionary<int, MyObj> myColl = BuildDictionary();

  var res = myColl.SortByMember("Name");

  foreach (var rr in res)
    Console.WriteLine(rr.Value);
  • I agree that returning an empty is not ideal, but in my case it makes sense as I use the dictionary to create UI emails to display results. – unixsnob Jul 06 '19 at 11:50
  • 1
    Well, it's just for a prototype and it's not really doing the actual db stuff. The actual data will come from some external system. – unixsnob Jul 06 '19 at 11:54
1

To achieve your goal you can use expression tree to build a compiled lambda and then use this lambda in the Linq OrderBy() method:

public static class ExpressionHelper 
{
    public static Func<T, object> GetMemberExpressionFunc<T>(string memberName)
    {
        var parameter = Expression.Parameter(typeof(T));
        Expression source = Expression.PropertyOrField(parameter, memberName);  
        Expression conversion = Expression.Convert(source, typeof(object));
        return Expression.Lambda<Func<T, object>>(conversion, parameter).Compile();
    }
}   

static void Main(string[] args)
{
    Dictionary<int, MyObj> myColl = BuildDictionary();

    string property = "AtomicNumber";    // or whatever property you want your dictionary ordered by
    var func = ExpressionHelper.GetMemberExpressionFunc<MyObj>(property);
    var ordered = myColl.OrderBy(x => func(x.Value));
}
Dmitry Stepanov
  • 2,776
  • 8
  • 29
  • 45
  • Nice and clean. Is there a reason to use `Expression.Convert()`? At first sight I don't see it? –  Jul 06 '19 at 09:36
  • 1
    @HenrikHansen, without `Expression.Convert()` you can order by some `string` properties, but in other cases (e.x. order by `int` properties) you will get an `ArgumentException – Dmitry Stepanov Jul 06 '19 at 09:41
  • @HenrikHansen, your solution is better. If `MyObj` has a `bool` prop then when I order the dictionary by it, I actually get an exception, but your solution works well in that case. – Dmitry Stepanov Jul 06 '19 at 09:55
  • When I try your code with a `bool` property it works fine, so the exception must be thrown elsewhere. –  Jul 06 '19 at 10:10
  • 1
    @HenrikHansen, yes, `bool` property is also fine. I just wrote wrong code when were experementing with and without `Expression.Convert()` – Dmitry Stepanov Jul 06 '19 at 10:15
  • @DmitryStepanov: This is what I need! Thanks. One addition, how would you go on about adding a `Select` condition to the lambda? For example, AtomicNumber > 20. – unixsnob Jul 06 '19 at 11:52
  • 1
    @unixsnob, Why can't you add `Where()` clause like this: `var ordered = myColl.Where(x => x.Value.AtomicNumber > 20).OrderBy(x => func(x.Value));` – Dmitry Stepanov Jul 06 '19 at 13:07
  • @DmitryStepanov: Thanks! I actually didn't actually know you could chain it there. I thought you had to put it inside the lambda! I have done SQL before, but this is my first time using `Linq`. – unixsnob Jul 06 '19 at 13:18