LINQ select property by name [duplicate]
Asked Answered
U

6

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.

Unscientific answered 12/12, 2017 at 21:1 Comment(4)
Could you state your question in words in addition to showing the code?Laszlo
DV for calling GetProperty() and coming here for help without even reading the MSDN documentation on it. Your answer is in the documentation.Means
@Laszlo It's just another one who wants to select a property's value given the property's name, as a string.Means
@Laszlo I added Whitespace to more easily see the Question.Unscientific
K
18

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.

Kalmia answered 12/12, 2017 at 21:7 Comment(5)
so i was kinda on the right track with the reflection then?Unscientific
@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<Person,string>)Delegate.CreateDelegate(typeof(Func<Person,string>), null, prop.GetGetMethod()); and pass func to SelectKalmia
Thank you Marc I'm going with your Code for now. I'm curious about the preformance between yours and JamesFaix answer.Unscientific
@WizardHammer if you use it rarely: it won't matter; if you use it lot - a delegate from either Delegate.CreateDelegate or Expression<T>.Compile will be faster as long as you cache and re-use the delegate instance - don't generate it each timeKalmia
this code I find harder to Read then The example From JamesFaix. But I was able to implement this add it works.Unscientific
G
6

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();
}
Gynandrous answered 12/12, 2017 at 21:13 Comment(1)
I understand this code better than the example @Marc Gravell. In my case it's going to always be a string.Unscientific
P
4

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");
Poetess answered 12/12, 2017 at 21:39 Comment(8)
see linqpad ready gist for the entire thing gist.github.com/dandohotaru/3c2a2b4eb1a07c43cb66c7044ed3f7cePoetess
+1 Definitely no need for reflection or Expression unless these names are coming from an external source (e.g. database). Use the Func, Luke.Tiddly
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 exampleUnscientific
public static string GenarateDataList(string property) { var requestedProperty = Expression.Parameter(typeof(EquipmentRequest), "x"); var body = Expression.PropertyOrField(requestedProperty, property); var lambda = Expression.Lambda<Func<EquipmentRequest, string>>(body, requestedProperty); using (Entities db = new Entities()) { var list = db.EquipmentRequests.Select(lambda.Compile()).Distinct().OrderBy(x => x).ToList(); return JsonConvert.SerializeObject(list); } }Unscientific
@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...Poetess
in any case i've updated my answer to reflect your actual needs, with a property names sent from the clientPoetess
Seems like you use reflection and deal with its troubles or you have additional maintenance overhead.Unscientific
This is really what I would be doing in my own code. Reflection is slow and a maintenance issue sometimes.Gynandrous
E
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

Electromotive answered 12/12, 2017 at 21:14 Comment(0)
T
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();
    }
Tamanaha answered 2/11, 2018 at 11:48 Comment(0)
R
-1

If you want to select all the values:

object[] foos = objects.Select(o => o.GetType().GetProperty("PropertyName").GetValue(o)).ToArray();
Randi answered 10/12, 2019 at 2:9 Comment(1)
imho this answer does not address the original question and while good Infomation I think is misplaced as an answer to this question.Unscientific

© 2022 - 2024 — McMap. All rights reserved.