0

I need to order a collection of data dynamically. I have this working at the top level:

//where prop is a string passed in, ex: "ShoeSize"
_clowns
    .AsEnumerable()
    .OrderBy(x => x.GetType().GetProperty(prop)?.GetValue(x))
    .Select(x => x.Id)
    .ToList();

And that works so long as I only need to order by some property of Clowns. But what if I need to order Clowns by a property of their Car? I think I'm close, but can't clear the gap:

//for prop = "Car.ClownCapcity"
var queryBuilder = _clowns;
var orderProp = prop;
if (prop.Contains(".")){
    string[] props = prop.Split(".");
    foreach(string oneProp in props){
        if (props.Last() != oneProp){
            //this line is wrong for sure
            queryBuilder.Include(x => x.GetType().GetProperty(oneProp));
        } else {
            orderProp = oneProp;
        }
    }
}
queryBuilder.AsEnumerable()
    .OrderBy(x => x.GetType().GetProperty(orderProp)?.GetValue(x))
    .Select(x => x.Id)
    .ToList();

This doesn't work because I cannot build up queryBuilder, reassigning at the Include doesn't work because the return type is different. I also haven't figured out how to dynamically go deeper inside the final OrderBy.

Is there a decent way to do this in Linq, or should I go build a SQL string?

There are security concerns, those can be handled elsewhere and aren't terribly relevant to this question


Update

Progress! Got it to work two-levels deep, but only when I explicitly know it's two-levels deep. I haven't figured out the abstraction yet.

if (prop.Contains(".")){
    string[] props = prop.Split(".");
    string includeProp = props.FirstOrDefault();
    string orderProp = props.LastOrDefault();
    return _Clowns
        .Include(includeProp)
        .AsEnumerable()
        .OrderBy(x => {
            var y = x.GetType().GetProperty(formattedProp)?.GetValue(x);

            return y?.GetType().GetProperty(orderProp)?.GetValue(y);
        })
        .Select(x => x.Id)
        .ToList();
}
Randy Hall
  • 7,716
  • 16
  • 73
  • 151

1 Answers1

-1

Answering with my own solution. Please give me a better answer if I'm way over-complicating this.

if (prop.Contains(".")){
    string[] props = prop.Split(".");
    string includePath = prop.remove(prop.LastIndexOf("."));

    IQueryable<ClownModel> queryBuilder = _clowns.Include(includePath);

    
    return queryBuilder.
        .AsEnumerable()
        .OrderBy(x => {
            object y = x;
            foreach (var prop in props)
            {
                //avoid unnecessary depth checks if a null is found
                if (y == null)
                {
                    break;
                }
                //if we're dealing with a collection
                if (y?.GetType().Namespace == "System.Collections.Generic")
                {
                    //gets a HashMap<ModelType>
                    var o = (dynamic)y;
                    //can't access this HashMap with [0] or .First() though?
                    foreach(object m in o)
                    {
                        //cheat and iterate
                        y = m;
                        break;
                    }
                }
                y = y?.GetType().GetProperty(prop)?.GetValue(y);
            }
            return y;
        })
        .Select(x => x.Id)
        .ToList();
}

My initial workup was wrong, it was still only working for 2-level deep. This works for any levels, and accounts for collections of properties along the way. Intriguingly, the Include function can take a dot-notation string (super handy), but not the OrderBy.

So pass in the Include, then loop through each property level inside the OrderBy, taking the first record if it's a collection.

Bing-bam-boom, you got a baby pachyderm. Back to you, Trent.

Randy Hall
  • 7,716
  • 16
  • 73
  • 151