3

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?

AndrewP
  • 1,598
  • 13
  • 24
  • You need to use LinqKit. See http://stackoverflow.com/a/36736907/1625737 – haim770 Dec 22 '16 at 23:11
  • I checked it out, but it doesn't seem to relate. I have no issue with nesting .Select's to do an inline projection, however, I want to encapsulate my projection logic into the view class that it returns – AndrewP Dec 22 '16 at 23:16
  • I'll drag myself out of bed to write an answer that'll show you how it *does* relate (-: – haim770 Dec 22 '16 at 23:19

2 Answers2

1

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

haim770
  • 48,394
  • 7
  • 105
  • 133
  • Worked a treat, thanks! I still need to be careful to not do something like `PersonView = PersonView.Projector.Invoke(o.SomeNavigation.ManyProperties.FirstOrDefault().SubProperty.Person)`, but that's just common sense... – AndrewP Dec 23 '16 at 00:32
0

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();
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • Link to Entities will throw using the above expression – haim770 Dec 22 '16 at 23:05
  • As @haim770 said, this still throws an exception- `Exception thrown: 'System.NotSupportedException' in EntityFramework.SqlServer.dll Additional information: The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.` – AndrewP Dec 22 '16 at 23:17
  • @AndrewP: Yes; you need to keep using the original expression tree in all LINQ to Entities calls. – SLaks Dec 22 '16 at 23:18