12

I'm attempting to use a variable inside of a LINQ select statement.

Here is an example of what I'm doing now.

using System;
using System.Collections.Generic;
using System.Linq;
using Faker;

namespace ConsoleTesting
{
internal class Program
{
    private static void Main(string[] args)
    {
        List<Person> listOfPersons = new List<Person>
        {
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person(),
            new Person()
        };

        var firstNames = Person.GetListOfAFirstNames(listOfPersons);

        foreach (var item in listOfPersons)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine();
        Console.ReadKey();
    }


    public class Person
    {
        public string City { get; set; }
        public string CountryName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person()
        {
            FirstName = NameFaker.Name();
            LastName = NameFaker.LastName();
            City = LocationFaker.City();
            CountryName = LocationFaker.Country();
        }

        public static List<string> GetListOfAFirstNames(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfCities(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfCountries(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }

        public static List<string> GetListOfLastNames(IEnumerable<Person> listOfPersons)
        {
            return listOfPersons.Select(x => x.FirstName).Distinct().OrderBy(x => x).ToList();
        }
    }
}
}

I have a Some very not DRY code with the GetListOf... Methods

i feel like i should be able to do something like this

public static List<string> GetListOfProperty(
IEnumerable<Person> listOfPersons, string property)
        {
            return listOfPersons.Select(x =>x.property).Distinct().OrderBy(x=> x).ToList();
        }

but that is not vaild code. I think the key Might Relate to Creating a Func

if That is the answer how do I do that?

Here is a second attempt using refelection But this is also a no go.

        public static List<string> GetListOfProperty(IEnumerable<Person> 
listOfPersons, string property)
        {
            Person person = new Person();
            Type t = person.GetType();
            PropertyInfo prop = t.GetProperty(property);
            return listOfPersons.Select(prop).Distinct().OrderBy(x => 
x).ToList();
}

I think the refection might be a DeadEnd/red herring but i thought i would show my work anyway.

Note Sample Code is simplified in reality this is used to populate a datalist via AJAX to Create an autocomplete experience. That object has 20+ properties and I can complete by writing 20+ methods but I feel there should be a DRY way to complete this. Also making this one method also would clean up my controller action a bunch also.

Question:

Given the first section of code is there a way to abstract those similar methods into a single method buy passing some object into the select Statement???

Thank you for your time.

Luke Hammer
  • 1,545
  • 2
  • 13
  • 28

6 Answers6

13

You would have to build the select

.Select(x =>x.property).

by hand. Fortunately, it isn't a tricky one since you expect it to always be the same type (string), so:

var x = Expression.Parameter(typeof(Person), "x");
var body = Expression.PropertyOrField(x, property);
var lambda = Expression.Lambda<Func<Person,string>>(body, x);

Then the Select above becomes:

.Select(lambda).

(for LINQ based on IQueryable<T>) or

.Select(lambda.Compile()).

(for LINQ based on IEnumerable<T>).

Note that anything you can do to cache the final form by property would be good.

chongo2002
  • 121
  • 1
  • 6
  • 12
Marc Gravell
  • 927,783
  • 236
  • 2,422
  • 2,784
  • so i was kinda on the right track with the reflection then? – Luke Hammer Dec 12 '17 at 21:15
  • @WizardHammer possibly - and you can do it via reflection too if you prefer - JamesFaix's answer shows how to use `GetValue` - you can also do something like (I'm not at an IDE right now): `var func = (Func)Delegate.CreateDelegate(typeof(Func), null, prop.GetGetMethod());` and pass `func` to `Select` – Marc Gravell Dec 12 '17 at 21:17
  • Thank you Marc I'm going with your Code for now. I'm curious about the preformance between yours and JamesFaix answer. – Luke Hammer Dec 12 '17 at 22:52
  • @WizardHammer if you use it rarely: it won't matter; if you use it lot - a delegate from either `Delegate.CreateDelegate` or `Expression.Compile` will be faster **as long as** you cache and re-use the delegate instance - don't generate it each time – Marc Gravell Dec 12 '17 at 23:28
  • this code I find harder to Read then The example From JamesFaix. But I was able to implement this add it works. – Luke Hammer Dec 12 '17 at 23:32
4

From your examples, I think what you want is this:

public static List<string> GetListOfProperty(IEnumerable<Person> 
    listOfPersons, string property)
{
    Type t = typeof(Person);         
    PropertyInfo prop = t.GetProperty(property);
    return listOfPersons
        .Select(person => (string)prop.GetValue(person))
        .Distinct()
        .OrderBy(x => x)
        .ToList();

}

typeof is a built-in operator in C# that you can "pass" the name of a type to and it will return the corresponding instance of Type. It works at compile-time, not runtime, so it doesn't work like normal functions.

PropertyInfo has a GetValue method that takes an object parameter. The object is which instance of the type to get the property value from. If you are trying to target a static property, use null for that parameter.

GetValue returns an object, which you must cast to the actual type.

person => (string)prop.GetValue(person) is a lamba expression that has a signature like this:

string Foo(Person person) { ... }

If you want this to work with any type of property, make it generic instead of hardcoding string.

public static List<T> GetListOfProperty<T>(IEnumerable<Person> 
    listOfPersons, string property)
{
    Type t = typeof(Person);         
    PropertyInfo prop = t.GetProperty(property);
    return listOfPersons
        .Select(person => (T)prop.GetValue(person))
        .Distinct()
        .OrderBy(x => x)
        .ToList();
}
JamesFaix
  • 5,975
  • 3
  • 30
  • 62
3

I would stay away from reflection and hard coded strings where possible...

How about defining an extension method that accepts a function selector of T, so that you can handle other types beside string properties

public static List<T> Query<T>(this IEnumerable<Person> instance, Func<Person, T> selector)
{
    return instance
        .Select(selector)
        .Distinct()
        .OrderBy(x => x)
        .ToList();
}

and imagine that you have a person class that has an id property of type int besides those you already expose

public class Person
{
    public int Id { get; set; }
    public string City { get; set; }
    public string CountryName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

all you need to do is fetch the results with type safe lambda selectors

var ids = listOfPersons.Query(p => p.Id);
var firstNames = listOfPersons.Query(p => p.FirstName);
var lastNames = listOfPersons.Query(p => p.LastName);
var cityNames = listOfPersons.Query(p => p.City);
var countryNames = listOfPersons.Query(p => p.CountryName);

Edit

As it seems you really need hardcoded strings as the property inputs, how about leaving out some dynamism and use a bit of determinism

public static List<string> Query(this IEnumerable<Person> instance, string property)
{
    switch (property)
    {
        case "ids": return instance.Query(p => p.Id.ToString());
        case "firstName": return instance.Query(p => p.FirstName);
        case "lastName": return instance.Query(p => p.LastName);
        case "countryName": return instance.Query(p => p.CountryName);
        case "cityName": return instance.Query(p => p.City);
        default: throw new Exception($"{property} is not supported");
    }
}

and access the desired results as such

var cityNames = listOfPersons.Query("cityName");
Dan Dohotaru
  • 2,111
  • 12
  • 13
  • see linqpad ready gist for the entire thing https://gist.github.com/dandohotaru/3c2a2b4eb1a07c43cb66c7044ed3f7ce – Dan Dohotaru Dec 12 '17 at 21:45
  • +1 Definitely no need for reflection or `Expression` unless these names are coming from an external source (e.g. database). Use the `Func`, Luke. – Kenneth K. Dec 12 '17 at 22:08
  • I agree with this as it relates to my example (which was simplified). But in reality, this is driven by an Ajax call that receives the Type of list that it wants. and expects a Json object back. Here is what it is based on @Marc Gravell example – Luke Hammer Dec 12 '17 at 22:33
  • public static string GenarateDataList(string property) { var requestedProperty = Expression.Parameter(typeof(EquipmentRequest), "x"); var body = Expression.PropertyOrField(requestedProperty, property); var lambda = Expression.Lambda>(body, requestedProperty); using (Entities db = new Entities()) { var list = db.EquipmentRequests.Select(lambda.Compile()).Distinct().OrderBy(x => x).ToList(); return JsonConvert.SerializeObject(list); } } – Luke Hammer Dec 12 '17 at 22:33
  • @marcgravell example does the work nicely in theory, but imagine that later in the development process, you need to rename LastName to FamillyName. This would imply that you need to update the client as well, because the client would send LastName as property selector, and that property no longer exists... – Dan Dohotaru Dec 12 '17 at 23:14
  • in any case i've updated my answer to reflect your actual needs, with a property names sent from the client – Dan Dohotaru Dec 12 '17 at 23:16
  • Seems like you use reflection and deal with its troubles or you have additional maintenance overhead. – Luke Hammer Dec 12 '17 at 23:29
  • This is really what I would be doing in my own code. Reflection is slow and a maintenance issue sometimes. – JamesFaix Dec 13 '17 at 01:10
2

You should be able to do it with Reflection. I use it something similar.

Just change your reflection try to this:

public static List<string> GetListOfValues(IEnumerable<Person> listOfPersons, string propertyName)
{
    var ret = new List<string>();

    PropertyInfo prop = typeof(Person).GetProperty(propertyName);
    if (prop != null)
        ret = listOfPersons.Select(p => prop.GetValue(p).ToString()).Distinct().OrderBy(x => x).ToList();

    return ret;
}

I hope it helps.

It's based on C# 6

Diego Rafael Souza
  • 4,599
  • 3
  • 18
  • 52
1

You can also use this. works for me.

public static class ObjectReflectionExtensions
{
    public static  object GetValueByName<T>(this T thisObject,  string propertyName)
    {
        PropertyInfo prop = typeof(T).GetProperty(propertyName);
        return prop.GetValue(thisObject);

    }
}

And call like this.

public static List<string> GetListOfProperty(IEnumerable<Person> listOfPersons, string propertyName)
    {
        return listOfPersons.Select(x =>(string)x.GetValueByName(propertyName)).Distinct().OrderBy(x=> x).ToList();
    }
Allard Vos
  • 11
  • 1
-1

If you want to select all the values:

object[] foos = objects.Select(o => o.GetType().GetProperty("PropertyName").GetValue(o)).ToArray();
Paul
  • 3,303
  • 10
  • 41
  • 78
  • 1
    imho this answer does not address the original question and while good Infomation I think is misplaced as an answer to this question. – Luke Hammer Dec 10 '19 at 20:00