Levitikon
Levitikon

Reputation: 7847

Detect if LambdaExpression is for property of object

I'm trying to implement a type of eager loading functionality in a repository by passing in expressions for child objects needing hydration.

    Candidate Get(int candidateId, params Expression<Func<Candidate, object>>[] includes);

So my services can call with something like this:

        candidateRepository.Get(1, p => p.Notes, p => p.Profile, p => p.Application);

Where Notes, Profile, and Application are all properties of Candidate. Something like this:

    public class Candidate
    {
        public string Name { get; set; }

        public IList<CandidateNote> Notes { get; set; }

        public Profile Profile { get; set; }

        public Application Application { get; set; }
    }

So, inside the repository I need to determine if a property was passed into the expression params to actually try hydrating it. But I'm not seeing a elegant way to achieve this.

    public Candidate Get(int candidateId, params Func<Candidate, object>[] includes)
    {
        ...

        if (candidate.Notes property was specified?)
        {
           candidate.Notes = this.candidateNoteRepository.List(candidateId);
        }

        return candidate;
    }

It looks like I can get the property name from the expression via (includes[0].Body as MemberExpression).Member.Name, but it seems there should be an easier way. Plus I'm not a fan of string comparison. I really wouldn't want to do it like this unless I have to:

        if (includes[0].Body as MemberExpression).Member.Name == "Notes")
        {

It seems like it should be something very simple like

        if (includes[0].Is(candidate.Notes) or includes[0](candidate.Notes).Match())
        {

Incase it comes up, I'd very much like to keep includes as an array of Expressions because although I'm working with ADO.NET today, we're planning to go the way of Entity Framework eventually and these expressions will work very nicely with the Include methods.

        return this.Filter(m => m.Candidates.CandiateId == candidateId)
            .Include(includes[0])

Upvotes: 3

Views: 234

Answers (2)

Servy
Servy

Reputation: 203827

You probably don't want to do this. It will force you to specify all of the property values at compile time, which will be both tedious, error prone, and not very extensible.

Instead you're better off just getting the MemberInfo object from the expression and then use that to get that member's value and set that member's value of the given object.

Start off with a helper to get the MemberInfo object from expressions following the pattern you've shown:

public static MemberInfo GetMemberInfo(LambdaExpression expression)
{
    var body = expression.Body as MemberExpression;
    if (body == null)
        return null;
    else
        return body.Member;
}

Then you can get a collection of the member objects you need to fetch:

var members = includes.Select(selector => GetMemberInfo(selector))
    .Where(member => member != null)
    //this validates the member is from the type itself; 
    //remove if such validation isn't desired
    .Intersect(typeof(Candidate).GetMembers());

Then fetch the value of all of those members through the query or whatever else you need to do to actually get the data.

Then create your new object and populate the value of each member:

var newItem = new Candidate();

foreach(var member in members)
{
    //Note that GetValueForMember shouldn't be doing a query.
    //it should be getting getting the value from the results
    member.SetValue(newItem, GetValueForMember(member));
}

Upvotes: 1

Markus
Markus

Reputation: 22481

As you say it: string comparison is not a good way because it is not easily extensible. You could inspect the member more closely to decide whether it is a valid include.
One option is to have a look at the return type of the property. If it is derived from IEnumerable then it might be a valid include.
If you cannot find the necessary facts to base the decision on in the property itself, you can use a custom attribute to mark the include properties in a class. Then you can check the CustomAttributes property of a possible include to decide whether it is a valid include.

Upvotes: 0

Related Questions