Conditional Include() in Entity Framework [duplicate] - c#

This question already has answers here:
EF: Include with where clause [duplicate]
(5 answers)
Closed 2 years ago.
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; }
}

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

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();

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()

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.

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();

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.

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);
?

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)

Related

Entity framework using ThenInclude - exclude certain columns from linked entities

I have a query to return a Configuration that looks like this:
public JsonResult Configurations(int id)
{
var myConfiguration = dbContext.MyEntity
.Where(e => e.Id == id)
.Include(e => e.Group)
.ThenInclude(g => g.Configuration)
.ThenInclude(c => c.ConfigurationChildren)
.ThenInclude(cc => cc.ConfigurationGrandchildren)
.FirstOrDefault();
.Group?
.Configuration;
return Json(myConfiguration);
}
The Configuration has a Client property that I don't want to include in the returned Json, and the ConfigurationGrandchildren each have a Client property that I don't want to include. How do I exclude them?
Try to add [JsonIgnore] above the Client property.
Configuration.cs
public class Configuration
{
[JsonIgnore]
public string Client { get; set; }
}

EF Core - How to Include collection for item in included collection [duplicate]

This question already has answers here:
EF Core Second level ThenInclude missworks
(2 answers)
Closed 2 years ago.
I have Request class, which has two collections (Details and RequestHistoryRecords). Detail in Details collection also have DetailHistoryRecords collection.
I would like to delete the whole request with all referenced collections (by foreaching all items and delete them one by one). But I have issue to load all the collections at once.
classes
public class Request
{
public IList<Detail> Details { get; private set; } = new List<Detail>();
public IList<RequestHistory> RequestHistoryRecords { get; private set; } = new List<RequestHistory>();
}
public class Detail
{
public IList<DetailHistory> DetailHistoryRecords { get; private set; } = new List<DetailHistory>();
}
db context definition
// 1-n relationship
modelBuilder.Entity<Request>()
.HasMany(x => x.Details)
.WithOne();
// 1-n relationship
modelBuilder.Entity<Request>()
.HasMany(x => x.RequestHistoryRecords)
.WithOne();
// 1-n relationship
modelBuilder.Entity<Detail>()
.HasMany(x => x.DetailHistoryRecords)
.WithOne();
I tried to load all the collections with Include and ThenInclude method, but I do not see DetailHistoryRecords collection, I don't know why. Am I on the right track or should I do the loading completely differently ?
var request = _context.Request
.Include(x => x.Details) // Works
.ThenInclude(details => details./*I do not see DetailHistoryRecords here ?! */)
.Include(x => x.RequestHistoryRecords) // Works
.Single(x => x.Id == id);
AS I can see Details and RequestHistoryRecords are not depend on each other. you can remove below line in query.
.ThenInclude(details => details./*I do not see DetailHistoryRecords here ?! */)
Call below code
var request = _context.Request
.Include(x => x.Details) // Works
.Include(x => x.RequestHistoryRecords) // Works
.Single(x => x.Id == id);

The Include path expression must refer to a navigation property defined on the type. Error when retrieving Products with Features

I have a db with products looking like this:
I am trying to retrieve Products with the features based on the selected language.
When I try this:
PromotedProducts = db.ProductLanguageDetail.
Where(a => a.cls_Language.Language == language).
Include(a => a.Product.ProductLanguageFeature).
ToList();
I get the products for selected language, and also all the fetures for this products, but the features are not filtered by the language, so I get features in multiple languages.
When I try to filter the features:
PromotedProducts = db.ProductLanguageDetail.
Where(a => a.cls_Language.Language == language).
Include(a => a.Product.ProductLanguageFeature.Where(feature => feature.IdLanguage == a.IdLanguage)).
ToList();
I get an error:"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"
What am I doing wrong?
Thanks
The key thing with EF entities and queries is that the entity structure should always be thought of as a complete representation of the data structure. This means that when you look at a product that has a relationship to features by language, the product entity should always have visibility to all of it's related data, it's never "filtered". This is one reason why you should avoid passing entities to views because you end up exposing more information about your schema than the view needs.
What it sounds like you want is a list of products that have at least 1 feature for a specified language, then to see only the features for that product that match that language.
As entities, this would be:
var promotedProducts = db.Products
.Where(x => x.ProductLanguageFeatures.Any(f => f.LanguageId == language))
.Select(x => new
{
Product = x,
Features = x.ProductLanguageFeatures.Where(f => f.LanguageId == language).ToList()
}).ToList();
This would give you a list of anonymous types containing the Products that have a feature for that language, and the applicable collection of features for that language.
To return that as a single structure I would recommend creating view models for the product and it's features:
[Serializable]
public class ProductViewModel
{
public int Id { get; set; }
public ICollection<ProductFeatureViewModel> Features { get; set;} = new List<ProductFeatureViewModel>();
}
[Serializable]
public class ProductFeatureViewModel
{
public int Id { get; set; }
public string Feature { get; set; }
}
Then load these up via projection:
var promotedProducts = db.Products
.Where(x => x.ProductLanguageFeatures.Any(f => f.LanguageId == language))
.Select(x => new ProductViewModel
{
Id = x.Id,
Features = x.ProductLanguageFeatures
.Where(f => f.LanguageId == language)
.Select(f => new ProductFeatureViewModel
{
Id = f.Id,
Feature = f.Feature
}).ToList()
}).ToList();
That might look like a bit of boring code to have to write, but Automapper can help considerably with it's ProjectTo method. This gives your view/consumer just the data that is applicable to it the way that makes sense for it.
Entities should reflect pure data state.
View models can reflect business / view state.
You have to run query on Product to instruct how EF should construct the sub-query. Try code below.
PromotedProducts = db.ProductLanguageDetail.
Where(a => a.cls_Language.Language == language).
Include(a => a.Product
.Select(p => p.ProductLanguageFeature.Where(feature => feature.IdLanguage == a.IdLanguage))).
ToList();
UPDATE
Unfortunately I cannot use Select after Product..
As you didn't provide Minimal, Reproducible Example I am just guessing here, but based on the relationship you should do following query as you are only interested in those object with particular language.
PromotedProducts = db.cls_Language
.Where(l => l == language)
.Select(l => new {
Lanugage = l,
ProductDetail = l.ProductLanguageDetail,
Product = l.ProductLanguageDetail.Product,
ProductLanguageFeature = l.ProductLanguageFeature
}
.ToList()
.Select(x => x.ProductDetail);

Linq select group by where syntax

I have these classes:
User {
//identity user class
public IList<Proyecto> Proyectos { get; set; }
public bool IsImportant { get; set }
}
Proyecto
{
public Guid Id { get; set; }
// more properties here
}
What I am trying to do is to group by userId all the projects they have created and where IsImportant is true.
I have tried this
var usuariosRobotVmInDb =await _dbContext.Users.Include(p=>p.Proyectos)
.groupby(u=>u.id)
.Where(u => u.IsImportant== True)
.Select(x=> new usuariosRobotViewModel
{
Id=x.Id,
Name=x.Name,
LastName=x.LastName,
Phone=x.Phone,
Email=x.Email,
proyectosVms=x.Proyectos.Select(a=>new ProyectosVm {Id=a.Id,Date=a.Date})
}).ToListAsync();
But it seems I can not use a groupBy and a where clause...How can I fix this? thanks
As you use navigation properties and the projects are already properties of your User class then you do not need to group by anything. Just retrieve the users where Important is true:
var result = await _dbContext.Users.Include(p => p.Proyectos)
.Where(u => u.IsImportant);
(And of course you can add the projection to the usuariosRobotViewModel object)
The problem with LINQ is that developers are still thinking in the SQL way and trying to convert it to LINQ Which sometimes doesn't work and sometimes is not optimal. In your case, you're using a navigation property when you're including the Proyectos object, and LINQ already knows the relationship between the User and Proyectos objects, so you don't need to group or do anything else:
var usuariosRobotVmInDb = await _dbContext.Users.Include(p => p.Proyectos)
.Where(u => u.IsImportant) //Same as u.IsImportant == true
.Select(x => new usuariosRobotViewModel {
Id = x.Key,
Nombre = x.Nombre,
...
}).ToListAsync();

Ef core : Filter out an item from a list of a list

I have this kind of model
public class Blog
{
public IList<Post> Posts { get; set; }
}
public class Post
{
public PostType PostType { get; set; }
}
public class PostType
{
public string Code { get; set; } // "Code1" and "Code2"
}
What I want is, to return all Blogs with a post of PostType Code1 or a Blog with no post(assuming a blog may not have a post)
To do that, I wrote this Ef linq query:
_dbContext.Blogs.Include(b => b.Posts).ThenInclude(b => b.PostType)
.Where(b => b.Posts.Count == 0 || b.Posts.Any(p => p.PostType.Code == "Code1").ToList();
The problem with this query is; if the blog has posts with types Code1 and Code2, the above query includes both Posts of code Code2 and Code1 because I'm using Any.
So I tried this: instead of Any, I used All
_dbContext.Blogs.Include(b => b.Posts).ThenInclude(b => b.PostType)
.Where(b => b.Posts.Count == 0 || b.Posts.All(p => p.PostType.Code == "Code1").ToList();
But with the above scenario, this query returns nothing.
With the given situation, is there a way to return all blogs with a post of post type Code1, without including post type Code2 using a single Ef LINQ query?
EDITED:
Found this blog... https://entityframework.net/include-with-where-clause
UPDATED
This feature is now available on .net EF core 5 Filtered Includes
The solution is like Thierry V's one, except for storing the filtered posts in a separate dictionary to avoid EF tracking side-effects: https://learn.microsoft.com/en-us/ef/core/querying/tracking.
However, I cannot really see the reason behind such code. Normally, you would grab all the blogs that satisfy your condition (containing any post of Code1), and then filter them as needed.
var blogs = _dbContext.Blogs.Include(b => b.Posts).ThenInclude(b => b.PostType)
.Where(b => b.Posts.Count == 0 || b.Posts.Any(p => p.PostType.Code == "Code1")
.ToList();
// Storing the filterd posts in a dictionary to avoid side-effects of EF tracking.
var dictionary = new Dictionary<int, List<Post>>();
foreach (var blog in blogs) {
dictionary[blog.BlogId] = blog.Posts.Where(p => p.PostType.Code == "Code1").ToList();
}
var blogs = _dbContext.Blogs.Include(b => b.Posts).ThenInclude(b => b.PostType)
.Where(b => b.Posts.Count == 0 || b.Posts.Any(p => p.PostType.Code == "Code1").ToList();
// blogs contains posts which have Code1, and maybe Code2
//filter the posts by assigning only the posts with Code1
blogs.ForEach(b=> b.Posts = b.Posts.Where( p => p.PostType.Code == "Code1"))

Categories

Resources