Filter on nested property - c#

I have the following models:
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Description { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
I have an IQueryable like:
var results = Blog.Include(x => x.Posts);
Everything works great until I want to filter on a property of the Post class. I need something like this:
var filteredResults = results.Where(x => x.Posts.Where(y => y.Description == "Test"));
This works If I append Any() to the second .Where(). This would not be right though because I only want to return the matching Posts, not all.
Any suggestions on how to approach this?

Entities don't filter like this. A Blog entity will, and should refer to ALL Posts it is associated with. EF can apply global filters to data to accommodate things like soft-delete (IsActive) or tenancy (ClientId) scenarios, but not filtered children like this.
This is a view/consumer concern, not a domain one so you should be looking to separate those concerns using Projection to return the data you want:
string postFilter = "Test";
var filteredResults = context.Blogs
.Where(x => x.Posts.Any(p => p.Description == postFilter))
.Select(x => new BlogViewModel
{
BlogId = x.BlogId,
Title = x.Title,
FilteredPosts = x.Posts.Where(p => p.Description == postFilter)
.Select(p => new PostViewModel
{
PostId = p.PostId,
Description = p.Description,
Text = p.Text,
// ...
{).ToList()
}).ToList();

You could approach this from bottom up.
var blogIds = Posts.Where(x => x.Description == "Test").Select(x => x.BlogId);
var result = Blog.Where(x => blogIds.Contains(x.Id))
Note that you might want to do:
x => x.Description.Contains("Test")
instead of:
x => x.Description == "Test"
in first query
You'll still have to map corresponding posts to each blog though
Update
Steve's answer is correct. I'll just add that it may translate in a lot of nested select queries. You can check the output in SQL Server profiler, or in the output window in Visual Sudio. So here's everything including mapping:
var posts = Posts.Where(x => x.Description == "test").ToList();
var blogIds = posts.Select(x => x.BlogId).ToList();
var blogs = Blog.Where(x => blogIds.Contains(x.Id)).ToList();
foreach(var blog in blogs)
blog.Posts = posts.Where(x => x.BlogId == x.Id).ToList()

Related

Unable to write an Include query with FirstOrDefault() and condition

I'm writing an entity framework query which needs Eager loading multiple levels based on condition.
var blogs1 = context.Blogs
.Include(x => x.Posts.FirstOrDefault(h => h.Author == "Me"))
.Include(x => x.Comment)
.FirstOrDefault();
public class Blog
{
public int BlogId { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Author { get; set; }
public int BlogId { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int PostId
public int CommentId { get; set; }
public string CommentValue { get; set;}
}
var blogs2 = context.Blogs
.Include("Posts.Comments")
.ToList();
I expect result to have first or default Blog and first or default Post for that blog by author "Me" and a list of all comments.
When query for blogs1is executed I see following exception
blogs2 query works as expected
The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Parameter name: path
FirstOrDefault executes the query and you can not use it inside Include as its purpose is to include navigational properties. You will need to modify the query to one of the below two ways:
Method 1: Its two two step process:
var blogs1 = context.Blogs
.Include(x => x.Posts.Select(p => p.Comments))
**// .Include(x => x.Comment) // This include is incorrect.**
.FirstOrDefault(x => x.Posts.Any(h => h.Author == "Me"));
var myPosts = blogs1?.Posts.Where(p => p.Author == "Me");
Method 2:
var myPosts = context.Posts.Include(p => p.Blog).Include(p => p.Comments).Where(p => p.Author == "Me");

How to include only selected properties on related entities

I can include only related entities.
using (var context = new BloggingContext())
{
// Load all blogs, all related posts
var blogs1 = context.Blogs
.Include(b => b.Posts)
.ToList();
}
However, I don't need entire BlogPost entity. I'm interested only in particular properties, e.g:
using (var context = new BloggingContext())
{
// Load all blogs, all and titles of related posts
var blogs2 = context.Blogs
.Include(b => b.Posts.Select(p => p.Title) //throws runtime exeption
.ToList();
foreach(var blogPost in blogs2.SelectMany(b => b.Posts))
{
Console.Writeline(blogPost.Blog.Id); //I need the object graph
Console.WriteLine(blogPost.Title); //writes title
Console.WriteLine(blogPost.Content); //writes null
}
}
You either use Include which loads the entire entity, or you project what you need to a .Select:
var blogs2 = context.Blogs
.Select(x => new
{
BlogName = x.BlogName, //whatever
PostTitles = x.Post.Select(y => y.Title).ToArray()
})
.ToList();
Or, you could do something like this:
var blogs2 = context.Blogs
.Select(x => new
{
Blog = x,
PostTitles = x.Post.Select(y => y.Title).ToArray()
})
.ToList();
A Select is always better when you don't need the entire child, as it prevents querying unneeded data.
In fact what you want is: split an entity in a common, representational part and a special part that you don't always want to pull from the database. This is not an uncommon requirement. Think of products and images, files and their content, or employees with public and private data.
Entity framework core supports two ways to achieve this: owned type and table splitting.
Owned type
An owned type is a type that's wrapped in another type. It can only be accessed through its owner. This is what it looks like:
public class Post
{
public int ID { get; set; }
public Blog Blog { get; set; }
public string Title { get; set; }
public PostContent Content { get; set; }
}
public class PostContent
{
public string Content { get; set; }
}
And the owned-type mapping:
modelBuilder.Entity<Post>().OwnsOne(e => e.Content);
Where Blog is
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
}
However, as per the docs:
When querying the owner the owned types will be included by default.
Which means that a statement like...
var posts = context.Posts.ToList();
...will always get you posts and their contents. Therefore, owned type is probably not the right approach for you. I still mentioned it, because I found out that when Posts are Included...
var blogs = context.Blogs.Include(b => b.Posts).ToList();
...the owned types, PostContents, are not included (DISCLAIMER: I'm not sure if this is a bug or a feature...). In this case, when the owned types should be included a ThenInclude is required:
var blogs = context.Blogs.Include(b => b.Posts)
.ThenInclude(p => p.Content).ToList();
So if Posts will always be queried through Blogs, owned type may be appropriate.
I don't think this applies here, but it does when children having owned types have an identifying relationship with their parents (classical example: Order-OrderLine).
Table splitting
With table splitting a database table is split up into two or more entities. Or, from the objects side: two or more entities are mapped to one table. The model is almost identical. The only difference is that PostContent now has a required primary key property (ID, of course having the same value as Post.ID):
public class Post
{
public int ID { get; set; }
public Blog Blog { get; set; }
public string Title { get; set; }
public PostContent Content { get; set; }
}
public class PostContent
{
public int ID { get; set; }
public string Content { get; set; }
}
And the table-splitting mapping:
modelBuilder.Entity<Post>()
.HasOne(e => e.Content).WithOne()
// or .WithOne(c => c.Post) if there is a back reference
.HasForeignKey<PostContent>(e => e.ID);
modelBuilder.Entity<Post>().ToTable("Posts");
modelBuilder.Entity<PostContent>().ToTable("Posts");
Now Posts will always be queried without their contents by default. PostContent should always be Include()-ed explicitly.
Also, PostContent can now be queried without its owner Post:
var postContents = context.Set<PostContent>().ToList();
I think this is exactly what you're looking for.
Of course you can do without these mappings if you'll always use projections when you want to fetch posts without contents.
You can try this :
using (var context = new BloggingContext())
{
var blogProps = context.Blogs
.SelectMany(b =>
b.Posts.Select(p =>
new { Blog = b, PostTitle = p.Title }
)
)
.ToList();
}
EDIT
If you want to stick to your data model, you could try something like this :
using (var context = new BloggingContext())
{
var blogProps = context.Blogs
.Select(b =>
new Blog
{
Name = b.Name,
Posts = new List<Post>(b.Posts.Select(p =>
new Post
{
Title = p.Title
})
}
)
.ToList();
}
I think there's a much easier way to do this. Projection is nice and all, but what if you want all the columns from your parent entity and most of them from the child? When those types have a lot of properties, using projection means you have a lot of lines of code to write just to select everything you want except the few that you don't. Well, since using projection means your entities won't be tracked, it's much easier to use .AsNoTracking() and then just empty out the things you don't want.
var foos = await _context.DbSet<Foo>()
.AsQueryable()
.Where(x => x.Id == id)
.Include(x => x.Bars)
.AsNoTracking()
.ToListAsync();
foreach (var foo in foos)
{
foreach (Bar bar in foo.Bars)
{
bar.Baz = null;
}
}

LINQ how to Filter the data based on the value of the properties

i have a problem with filtering data in LINQ , here is my Model :
public class CoursePlan
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Semester { get; set; }
public string ModuleCode { get; set; }
public string ModuleName { get; set; }
public string Credits { get; set; }
public string OrderNumber { get; set; }
public string ModuleStatus { get; set; }
}
and here is my data Json
the problem here some modules having same OrderNumber which mean they are optional , student must study one of them and if student already study one of them , i should ignore other modules in same order number.
in other way to describe the question
i want to return a list of CoursePlan and on this list if there is two items having same OrderNumber check the ModuleStatus for each one of them and if any one is Completed remove other modules on that order otherwise return them all .
here is my code
var coursePlanList = await _sqLiteAsyncConnection.Table<CoursePlan>().ToListAsync();
var groupedData = coursePlanList.OrderBy(e => e.Semester)
.GroupBy(e => e.OrderNumber)
.Select(e => new ObservableGroupCollection<string, CoursePlan>(e))
.ToList();
for now im solving this by this algorithm and not sure if it's the best
var coursePlanList = await _sqLiteAsyncConnection.Table<CoursePlan>().ToListAsync();
List<CoursePlan> finalList = new List<CoursePlan>();
var counter = 0;
foreach (var itemPlan in coursePlanList)
{
if (counter > 0 && counter < coursePlanList.Count)
if (itemPlan.OrderNumber == coursePlanList[counter - 1].OrderNumber)
{
if (itemPlan.ModuleStatus == "Completed")
{
finalList.RemoveAll(a => a.OrderNumber == itemPlan.OrderNumber);
finalList.Add(itemPlan);
}
Debug.WriteLine(itemPlan.ModuleName + "With -->" + coursePlanList[counter - 1].ModuleName);
}
else
finalList.Add(itemPlan);
counter++;
}
var groupedData = finalList.OrderBy(e => e.ModuleStatus)
.ThenBy(e => e.Semester)
.GroupBy(e => e.Semester)
.Select(e => e)
.ToList();
CoursePlanViewList.BindingContext = new ObservableCollection<IGrouping<string, CoursePlan>>(groupedData);
Any advise or guidance would be greatly appreciated
Let me rephrase your requirement: you want to show all plans per OrderNumber that meet the condition: none of the plans in their group should be "Completed" or the plans themselves should be "Completed". All this grouped by Semester:
var plansQuery =
from p in _sqLiteAsyncConnection.Table<CoursePlan>()
group p by p.Semester into sem
select new
{
PlansInSemester =
from p in sem
group p by p.OrderNumber into gp
select new
{
PlansInOrderNumber =
gp.Where(p => !gp.Any(p1 => p1.ModuleStatus == "Completed")
|| p.ModuleStatus == "Completed")
}
};
This gives you an IQueryable that produces the course plans you want to select, but grouped in two levels, so the final result is obtained by flattening the query twice:
var coursePlanList = await plansQuery
.SelectMany(x => x.PlansInSemester
.SelectMany(y => y.PlansInOrderNumber)).ToListAsync()

LINQ to Entities, Where Any In

How to write 'Where Any In' in LINQ to Entity?
Here is my model :
class Chair
{
public int Id { get; set; }
public int TableId { get; set; }
public Table Table { get; set; }
}
class Table
{
public int Id { get; set; }
public ICollection<Chair> Chairs { get; set; }
public ICollection<Category> Categories { get; set; }
public Table()
{
Chairs = new List<Chair>();
Categories = new List<Category>();
}
}
class Category
{
public int Id { get; set; }
public ICollection<Table> Tables { get; set; }
}
I also got a simple list of Category :
List<Category> myCategories = new List<Category>(c,d,e);
I want to get only that Chairs that belongs to Table that got one of the Category from myCategories List. Thats what im trying to do :
var result =
ctx.Chairs.Where(x => x.Table.Categories.Any(y => myCategories.Any(z => z.Id == y.Id))).ToList();
I think its ok but what i get is error :
"Unable to create a constant value of type 'ConsoleApplication1.Category'. Only primitive types or enumeration types are supported in this context"
Try to compare with in-memory categories Ids collection, instead of categories collection.
var myCategoriesIds = myCategories.Select(c => c.Id).ToArray();
var result =
context.Chairs
.Where(
x => x.Table.Categories.Any(
y => myCategoriesIds.Contains(y.Id)))
.ToList();
this is because ctx.Chairs is a collection that is in database, you should retrieve that collection first in order to compare it with in-memory data:
var result = ctx
.Chairs
.AsEnumerable() // retrieve data
.Where(x =>
x.Table.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)))
.ToList();
EDIT: that wouldn't be the correct thing to do if you have a lot of entities on database, what you can do is to split it into two queries:
var tables = ctx.Tables
.Where(x =>
x.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)));
var result = ctx.Chairs
.Where(x =>
tables.Any(t=> t.Id == x.TableId))
.ToList();
You can select Ids from myCategories and use it last statement.
var CategoryIds = myCategories.Select(ct => ct.Id);
var result = ctx.Chairs.Where(x => x.Table.Categories.Any(y => CategoryIds.Any(z => z == y.Id))).ToList();

Only select certain items from related table using linq

Hi I have a 2 tables BlogPost and BlogComments
I want to get all BlogPosts that have a state of "published".
and get all BlogComments for each post that has a state of "published".
Im using something like this to get the blogposts:
var BlogPosts = (from p in db.BlogPosts where p.State == State.Published select p).ToArray();
but due to the relationship with BlogComments it autiomatically has all the BlogComments (both published and unpublished).
How can I just get the "published" comments for each of the blogposts (i.e the approved ones)
thanks
Try selecting a new BlogPostViewModel, an analog to BlogPost but with an IEnumerable<BlogComment> instead, with just the blog post data and a collection of published comments.
select new BlogPostViewModel {
Title = p.Title,
Body = p.Body,
Comments = p.Comments.Where( c => c.Published );
});
Where BlogPostViewModel is:
public class BlogPostViewModel
{
public string Title { get; set; }
public string Body { get; set; }
public IEnumerable<BlogComment> Comments { get; set; }
}
var BlogPosts = from p in db.BlogPosts where p.State == State.Published select new {
Post = p,
Comments = (from c in db.BlogComments where c.State == State.Published && c.Post = p select c)
};
var publishedComments = db.BlobPosts.Where(p => p.State == State.Published)
.SelectMany(p => p.Comments)
.Where(c => c.State == State.Published);
Sorry for not using the query syntax. Hopefully this will get you on your way though.

Categories