43

I have seen a few answers to similar questions, however I cannot seem to work out how to apply the answer to my issue.

var allposts = _context.Posts
            .Include(p => p.Comments)
            .Include(aa => aa.Attachments)
            .Include(a => a.PostAuthor)
            .Where(t => t.PostAuthor.Id == postAuthorId).ToList();

Attachments can be uploaded by the Author (type Author) or Contributor (type Contributor). What I want to do, is only get the Attachments where the owner of the attachment is of type Author.

I know this doesn't work and gives an error:

.Include(s=>aa.Attachments.Where(o=>o.Owner is Author))

I've read about Filtered Projection here

EDIT - link to article: : http://blogs.msdn.com/b/alexj/archive/2009/10/13/tip-37-how-to-do-a-conditional-include.aspx,

but I just can't get my head around it.

I don't want to include the filter in the final where clause as I want ALL posts, but I only want to retrieve the attachments for those posts that belong to the Author.

EDIT 2: - Post schema requested

public abstract class Post : IPostable
{

    [Key]
    public int Id { get; set; }

    [Required]
    public DateTime PublishDate { get; set; }

    [Required]
    public String Title { get; set; }

    [Required]
    public String Description { get; set; }

    public Person PostAuthor { get; set; }
    public virtual ICollection<Attachment> Attachments { get; set; }
    public List<Comment> Comments { get; set; }
}
Vivek Nuna
  • 25,472
  • 25
  • 109
  • 197
grayson
  • 945
  • 1
  • 11
  • 28
  • Can you show us the `Posts` schema please? – Sateesh Pagolu Sep 24 '15 at 00:26
  • @DarkKnight - see edit – grayson Sep 24 '15 at 00:32
  • @grayson What you're asking to do is not possible. `Linq2Sql` will convert your code into raw `SQL`, and it'll return child rows via a join. You can't do this kind of conditional join in `SQL`. Your only option is to remove `.Include(aa => aa.Attachments)`, and have a second query which returns attachments depending on whether or not the owner is an author/contributor. – Rob Sep 24 '15 at 01:57

8 Answers8

54

EF Core 5.0 is introducing Filtered Include soon.

var blogs = context.Blogs
    .Include(e => e.Posts.Where(p => p.Title.Contains("Cheese")))
    .ToList();

Reference: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-5.0/whatsnew#filtered-include

Vivek Nuna
  • 25,472
  • 25
  • 109
  • 197
22

From the link you posted I can confirm that trick works but for one-many (or many-one) relationship only. In this case your Post-Attachment should be one-many relationship, so it's totally applicable. Here is the query you should have:

//this should be disabled temporarily
_context.Configuration.LazyLoadingEnabled = false;
var allposts = _context.Posts.Where(t => t.PostAuthor.Id == postAuthorId)
                       .Select(e => new {
                           e,//for later projection
                           e.Comments,//cache Comments
                           //cache filtered Attachments
                           Attachments = e.Attachments.Where(a => a.Owner is Author),
                           e.PostAuthor//cache PostAuthor
                        })
                       .AsEnumerable()
                       .Select(e => e.e).ToList();
Hopeless
  • 4,397
  • 5
  • 37
  • 64
  • 1
    do I need to add "_context.Configuration.LazyLoadingEnabled = true;" to renable lazy loading after this call? – grayson Sep 24 '15 at 14:48
  • 1
    @grayson yes, if you want to re-enable that. – Hopeless Sep 24 '15 at 15:04
  • 1
    @Hopeless know this question is old, but my recent questions was marked as duplicate to this one, my requirement is when `Attachments` has additional child properties they are not being available in result. Link to my question https://stackoverflow.com/questions/58347487/unable-to-write-an-include-query-with-firstordefault-and-condition – Krtti Oct 18 '19 at 20:29
  • 1
    @Krtti I'm not so sure if your issue can be solved with what answered here, actually I don't understand your issue well (from what you described in the linked question, there are at least 2 ways of understanding). Also someone added an answer there and looks like it's correct for one way of understanding the issue. The idea in my answer is just simple enough to apply, when it's not working maybe it's not applicable. Finally it's a long time I've not been working with EF and Linq-to-entity, so I may not help you much. – Hopeless Oct 19 '19 at 01:25
  • 1
    @Hopeless no worries, my question was answered and thanks for taking time to inform – Krtti Oct 20 '19 at 22:03
  • And one more thing, can I have Include statement for the tables on which Foreign key is not defined but they have common Columns that actually we can bring in? I mean they are related tables but not actually foreign key defined on them, can I use those tables also in Include statement - thank you for all this support - its a great help guys. – Abdul Nov 20 '19 at 19:11
4

Remove the virtual keyword from your Attachments navigation property to prevent lazy loading:

public ICollection<Attachment> Attachments { get; set; }

First method: Issue two separate queries: one for the Posts, one for the Attachments, and let relationship fix-up do the rest:

List<Post> postsWithAuthoredAttachments = _context.Posts
    .Include(p => p.Comments) 
    .Include(p => p.PostAuthor)
    .Where(p => p.PostAuthor.Id == postAuthorId)
    .ToList();

List<Attachment> filteredAttachments = _context.Attachments
    .Where(a => a.Post.PostAuthor.Id == postAuthorId)
    .Where(a => a.Owner is Author)
    .ToList()

Relationship fixup means that you can access these filtered Attachments via a Post's navigation property

Second method: one query to the database followed by an in-memory query:

var query = _context.Posts
    .Include(p => p.Comments) 
    .Include(p => p.PostAuthor)
    .Where(p => p.PostAuthor.Id == postAuthorId)
    .Select(p => new 
        {
            Post = p,
            AuthoredAttachments = p.Attachments
                Where(a => a.Owner is Author)
        }
    );

I would just use the anonymous type here

var postsWithAuthoredAttachments = query.ToList()

or I would create a ViewModel class to avoid the anonymous type:

List<MyDisplayTemplate> postsWithAuthoredAttachments = 
     //query as above but use new PostWithAuthoredAttachments in the Select

Or, if you really want to unwrap the Posts:

List<Post> postsWithAuthoredAttachments = query.//you could "inline" this variable
    .AsEnumerable() //force the database query to run as is - pulling data into memory
    .Select(p => p) //unwrap the Posts from the in-memory results
    .ToList()
Colin
  • 22,328
  • 17
  • 103
  • 197
3

You can use this implementation of an extension method (eg.) Include2(). After that, you can call:

_context.Posts.Include2(post => post.Attachments.Where(a => a.OwnerId == 1))

The code above includes only attachments where Attachment.OwnerId == 1.

Jan Palas
  • 1,865
  • 1
  • 23
  • 35
  • Hi, this doesn't appear to work when there are no Attachments for a post. A, I missing something – grayson Oct 12 '15 at 23:39
1

try this

var allposts = _context.Posts
        .Include(p => p.Comments)
        .Include(a => a.PostAuthor)
        .Where(t => t.PostAuthor.Id == postAuthorId).ToList();

_context.Attachments.Where(o=>o.Owner is Author).ToList();
Yi Yin
  • 11
  • 1
1

For net core

https://learn.microsoft.com/ru-ru/ef/core/querying/related-data/explicit

var allposts = _context.Posts
        .Include(p => p.Comments)
        .Include(a => a.PostAuthor)
        .Where(t => t.PostAuthor.Id == postAuthorId).ToList();

_context.Entry(allposts)
        .Collection(e => e.Attachments)
        .Query()
        .Where(e=> e.Owner is Author)
        .Load();

it makes 2 query to sql.

  • 3
    This i not working cause you try to use explicit loading on a list of posts. But the .Entry() can only be used on a single entity. – Retrogott Oct 28 '20 at 12:58
-3

Lambda in Include() may only point to a property:

.Include(a => a.Attachments)
.Include(a => a.Attachments.Owner);

Your condition doesn't makes sense for me because Include() means join and you either do it or not. And not conditionally.

How would you write this in raw SQL?


Why not just this:

context.Attachments
       .Where(a => a.Owner.Id == postAuthorId &&
                   a.Owner.Type == authorType);

?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
  • Thanks. I am pretty new to all this. I can do that to get the attachments of the desired type, but I don't know how to merge that back in to the Post. My DisplayTemplate derives from the Post and displays Post.Attachments – grayson Sep 24 '15 at 00:35
  • how you would write in raw SQL: select * from table1 x join table2 y on x.fkey=y.fkey and (condition with y) – Austin_Anderson Apr 12 '18 at 18:30
  • @Austin_Anderson: this is not conditional join, is it? – abatishchev Apr 13 '18 at 17:04
  • 3
    A join is always conditional. It's just usually conditioned on pkeys. However, you can total join on pkey AND other conditions. Matter of fact, since JOINS are generally preferred in large datasets. – DiscipleMichael Apr 26 '18 at 21:00
  • @DiscipleMichael: A join is never conditional, it either presents or not. And performing one might be costly. And good performance tune is to eliminate joins you know would yield nothing. In U-SQL it's done using Dynamic SQL usually. But here's the question about EF. – abatishchev Apr 27 '18 at 16:58
-5

Assuming "a" being of type "YourType", a conditonal include could be solved by using a method extension, e.g.

public static class QueryableExtensions
{
    public static IQueryable<T> ConditionalInclude<T>(this IQueryable<T> source, bool include) where T : YourType
    {
        if (include)
        {
            return source
                .Include(a => a.Attachments)
                .Include(a => a.Attachments.Owner));
        }

        return source;
    }
}

... then just use this like you are using .Include, e.g.

bool yourCondition;

.ConditionalInclude(yourCondition)
CHS
  • 138
  • 1
  • 1
  • 7