Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LINQ select property by name [duplicate]

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.

like image 641
Luke Hammer Avatar asked Dec 12 '17 21:12

Luke Hammer


3 Answers

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");
like image 139
Dan Dohotaru Avatar answered Nov 02 '22 15:11

Dan Dohotaru


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.

like image 16
Marc Gravell Avatar answered Nov 02 '22 14:11

Marc Gravell


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();
}
like image 5
JamesFaix Avatar answered Nov 02 '22 13:11

JamesFaix