1

I would like to ask you about a way to dynamically create a LINQ Select expression with a nested child collection. Fields in the selected child collection can be static, however I would like to dynamically pass a list of fields in the current entity, and also fields in other entities, referenced by navigation properties. Here's the static version of the query, similar to the one which I use in my code, in many places, and which I would like to create dynamically:

var listItems = _efDbContext.Blogs.Select(x => new {      
    ID = x.ID,
    Name = x.Name, //field from the current entity     
    AuthorName = x.Author.Name, //field referenced by navigation property
    ...<other fields from current or referenced entities(like AuthorName above), passed dynamically>
    Posts = x.Posts.Select(y => new { //this select is 'static', is the same for other queries 
       Id = x.Id,
       Name = x.Name
    })
});

I tried to figure out something from the answers in these two posts bewlo but I did not succeed. Answer by Ivan Stoev is really cool but it does not support referenced properties and I also have this static nested child collection select in my example above

Dynamically build select list from linq to entities query

EF Linq- Dynamic Lambda expression trees

I have also tried to accomplish this using libraries like Dynamic.Linq.Core or Automapper but it seems that the former does not support select with child collections and the latter needs a DTO class and I dont want to create hundred of DTOs for each combination of fields in the select expression.

I also thought that this whole approach might be overcomplicated and maybe I should use parametrized SQL query instead, and that's definitely a possible option, but I think that I could reuse this dynamic LINQ expression in different variations, in other places in my project, and it would be helpful in a long run :)

(I'm using EF Core 3.1)

mctl87
  • 351
  • 8
  • 16

2 Answers2

4

My solution from the linked post handles quite simplified scenario. It can be extended for handling nested properties and method invocations, but then main problem will it is that it result isn't truly dynamic. It requires a predefined type and selectively selects/populates members of that type. Which makes it similar to AutoMapper explicit expansion feature, thus it might be better to use the later since it offers flexible automatic/manual mapping options.

For truly dynamic output you need a library which generates dynamic classes at runtime. Dynamic LINQ is one of them, and apart from its unusual expression language actually can be used for your scenario (at least the one from https://dynamic-linq.net/, because there are many flavors of that library and all they support/do not support some things).

Following is an example using System.Linq.Dynamic.Core package (or EF Core version of it Microsoft.EntityFrameworkCore.DynamicLinq in case you need async queryable support):

var selectList = new List<string>();
// Dynamic part
selectList.Add("ID");
selectList.Add("Name");
selectList.Add("Author.Name as AuthorName");
// Static part. But the nested Select could be built dynamically as well
selectList.Add("Posts.Select(new (Id, Name)) as Posts");

var listItems = _efDbContext.Blogs
    .Select($"new ({string.Join(", ", selectList)})")
    .ToDynamicList();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    So it is possible to do what I want using Linq.Dynamic.Core... Hmm I must have overlooked that in the docs... -.- I will try that asap and update this comment. Thank you very much ! – mctl87 Mar 15 '21 at 14:32
  • This looks like it should work, but I always get the error that `No applicable method 'Select' exists in type` anything I put in place of `Posts`. I tried adding a `Select` method to the child class to wrap the `Select` from the library, but for some reason I can't seem to get my data classes to see that the library's `Select` exists with any `using` statement or explicit reference that I've been able to come up with. Any suggestions? – Trevortni Nov 05 '21 at 16:59
  • @Trevortni `Posts` must be a property of the parent class (in this case `Blog`) and at minimum be `IEnumerable`, e.g. `class Foo { public IEnumerable Bars { get; set; } }`. Also make sure to use the recent version of the packages from the links, because there are many implementations of DynamicLINQ and each one supports/does not support some methods. – Ivan Stoev Nov 05 '21 at 17:26
  • So this won't work if the child is one to one? It has to be a list of children, it can't be like `Blog.Author.Name`? Like `Author.Select(new (Name, EyeColor)) As Author`? – Trevortni Nov 05 '21 at 18:02
  • @Trevortni If it is reference type property, then you just generate the member (w/o `Select`), e.g. `selectList.Add("new (Author.Name as Name, Author.EyeColor as EyeColor) as Author");` – Ivan Stoev Nov 05 '21 at 18:26
  • @IvanStoev So combined with the rest of it, that would end up as `.Select("new (ID, Name, new (Author.Name as Name, Author.EyeColor as EyeColor) as Author)"? Because when I try that, I get `Value cannot be null. (Parameter 'expression')`. – Trevortni Nov 05 '21 at 18:49
  • Possibly related: if I let it stay on the base level, `.Select("new (ID, Name, Author.Name as Name, Author.EyeColor as EyeColor)", I get `Unknown column 's.Name' in 'field list'`. And the actual real column of what I'm using there is definitely there. – Trevortni Nov 05 '21 at 18:52
  • Doh! I had tried adding Name to my base item on an earlier attempt, and it doesn't have it's mappings defined, and I forgot to remove it. Reattempting.... – Trevortni Nov 05 '21 at 19:46
  • Okay, with that removed and my function as `.Select("new (ID, Name, new (Author.Name as Name) as Author")`, I get `No property or field 'Name' exists in type 'Blog'`. I have confirmed that this is referring to the bolded Name: `Author.Name as` **Name**. – Trevortni Nov 05 '21 at 20:01
  • @Trevortni I have no idea what's the problem with your code. All these syntaxes work for me (except the one with `Author.Name as Name` - of course there cannot be two `Name` members in the destination object), including the simplest `.Select("new (ID, Name, new (Author.Name) as Author"` – Ivan Stoev Nov 06 '21 at 17:40
0

If you're okay with adding an additional dependency, I strongly suggest relying on Automapper's ProjectTo(), which effectively replaces your Select with a method that allows for reusable mappings, including nested ones.

A simple example of this would be:

public class PostDto
{
    public string Title { get; set; }
}

public class BlogDto
{
    public string Name { get; set; }
    public PostDto[] Posts { get; set; }
}

public class AuthorDto
{
    public string Name { get; set; }
    public PostDto MostRecentPost { get; set; }
}

var configuration = new MapperConfiguration(cfg => {
    cfg.CreateMap<Post, PostDto>();
    cfg.CreateMap<Blog, BlogDto>();
    cfg.CreateMap<Author, AuthorDto>()
       .ForMember(
           dto => dto.MostRecentPost, 
           conf => conf.MapFrom(
               author => author.Posts
                               .OrderByDescending(x => x.Date)
                               .FirstOrDefault()));
});

public List<OrderLineDTO> GetLinesForOrder(int orderId)
{
    using (var context = new orderEntities())
    {
        var blogsWithPosts = context
                               .Blogs
                               .ProjectTo<BlogDto>(configuration)
                               .ToList();

        var authorsWithPost = context
                               .Authors
                               .ProjectTo<AuthorDto>(configuration)
                               .ToList();
    }
}

Notice how I didn't even need to mention Post or PostDto in my actual LINQ query. This is because ProjectTo uses the mapping configuration to figure out which fields it needs to fill in, and the configuration contains the information on which entity fields are needed for each DTO field.

I assumed for the most part that the names of all properties between entity and DTO matched, because then you don't need to manually map them. This was done to keep the example simple, but also included an example of how you can manually map AuthorDto.MostRecentPost to an actual subquery.
For more intricate mappings, I suggest looking at Automapper's documentation. That is more than I can explain in a single answer here.

Effectively, ProjectTo generates the appropriate Select statement for you.

This also means that if I decide to change PostDto and its mapping, that this change is automatically reflected in the two LINQ queries without needing to change the LINQ queries themselves.

Flater
  • 12,908
  • 4
  • 39
  • 62
  • Thank you for your answer, however I think that it doesnt help me in my case. Our BlogDto class contains 'Name' property only but it could also contain 100 properties. And in some queries I would like to fetch only 5 of them and 10 others in another query and so on... I dont want to make AutoMapper translate that Projection into SQL select with 100 columns. – mctl87 Mar 15 '21 at 14:27
  • 1
    @mctl87: The general advice would be to create specific DTOs to fit your specific needs, rather than trying to use one DTO in many different ways. Otherwise you're going to run into many issues down the line such as additional null-checking, null-reference exceptions, default (unused) values being indistinguishable from real data that happens to equal the default value, and so on. – Flater Mar 15 '21 at 14:30
  • I agree with you on that part. There are undeniable advantages when you use DTOs. I also use them in my queries. However there are rare situations where dynamic object seems a bit more reusable and does not contain so much unnecessary information. – mctl87 Mar 15 '21 at 14:57