Projecting a single record with a static Func<>

470 Views Asked by At

I have been using a pattern for projecting from Entity Framework to business domain views. I am nesting it, i.e. calling one projection from within another projection. It works well for collections, but I can't work out how to use the same pattern for projecting a single entity.

For collections, my code looks something like:

public class PersonView
{
    public int Id {get;private set;}
    public string FullName { get; set; }
    public static Expression<Func<Person, PersonView>> Projector = p => new PersonView {
         Id = p.PersonId,
         FullName = p.FirstName + " " + p.LastName
    };
}
//...
context.People.Select(PersonView.Projector).ToList();  // returns a list of PersonViews

If I create a list containing the 1 element, or otherwise get creative with the LINQ, I can get it to work, but would prefer a neater solution if possible.

// convert single element to list, then project it. Works, but is messy
var orderDetails = context.Orders.Where(...)
    .Select(o => new { 
       Id = o.Id, 
       Date = o.Date, 
       PersonView = new [] { o.Person }.AsQueryable().Select(PersonView.Projector).FirstOrDefault()
}).FirstOrDefault();

I would like something like (the below does not work, because linq to entities cannot invoke the Func<>):

public class PersonView
{
    public int Id {get;private set;}
    public string FullName { get; set; }
    public static Func<Person, PersonView> ProjectorFn = p => new PersonView {
         Id = p.PersonId,
         FullName = p.FirstName + " " + p.LastName
    };        
    public static Expression<Func<Person, PersonView>> ProjectorExpr = p => ProjectorFn(p);
}
 var orderDetails = context.Orders.Where(...)
    .Select(o => new { 
       Id = o.Id, 
       Date = o.Date, 
       PersonView = PersonView.ProjectorFn(o.Person)
}).FirstOrDefault();

//...
var peopleWithOrders = context.People.Where(p => p.Orders.Any())
    .Select(PersonView.ProjectorExpr);

Any suggestions?

2

There are 2 best solutions below

1
On BEST ANSWER

The essence of the problem is that the following line in your projection

PersonView = PersonView.ProjectorFn(o.Person)

Cannot be translated into a store query because ProjectorFn is no longer an Expression but a generic delegate (Func<Person, PersonView>).

Now, what you actually want is to use the original expression contained in your PersonView.Projector field, but obviously you can't because it cannot be invoked (without compiling to delegate) hence cannot return your desired PersonView type.

LinqKit is aiming to solve this problem using its own Invoke() extension method that while letting your code compile, will make sure your expression gets replaced back to its original form.

To enable the interception, you have to use the AsExpandable() method that is extending the entity set:

using LinqKit.Extensions;

var orderDetails = context.Orders
    .AsExpandable()
    .Where(...)
    .Select(o => new { 
       Id = o.Id, 
       Date = o.Date, 
       PersonView = PersonView.Projector.Invoke(o.Person)
    })
    .FirstOrDefault();

More on LinqKit

3
On

You need to keep using the direct expression tree, but also compile it to a normal delegate:

public static readonly Func<Person, PersonView> ProjectorFunc = Projector.Compile();