EF Lambda include navigation properties - c#

I have the following object, called Filter with the following properties:
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Type> Types{ get; set; }
public virtual ICollection<Step> Steps { get; set; }
public virtual ICollection<Flow> Flows { get; set; }
public virtual ICollection<Room> Rooms { get; set; }
When I select a list of Filters from the database, I have no idea how to include the collections (Types, Steps, Flows, Rooms). My code is as follows:
var filters = (
from filter in dbContext.DbSet<Filter>()
let rooms = (
from r in dbContext.DbSet<Room>()
select r
)
let eventTypes = (
from t in dbContext.DbSet<Type>()
select t
)
let processFlows = (
from f in dbContext.DbSet<Flow>()
select f
)
let processFlowSteps = (
from s in dbContext.DbSet<Step>()
select s
)
select filter
).ToList();
My collection of Filter is returned, but the collections inside are empty. Could you please tell me how can I achieve this?
Ps: I do not want to use Include because of performance issues, I don't like how Entity Framework generates the query and I would like to do it this way.

Your method works, you are just doing it slighly wrong.
To include a navigation property, all you have to do is a subselect (using linq), example:
var filters = (from filter in dbContext.DbSet<Filter>()
select new Filter
{
filter.Id,
filter.Name,
Rooms = (from r in dbContext.DbSet<Room>()
where r.FilterId == filter.Id
select r).ToList()
}).ToList();
Keep in mind that EF won't execute the query until you call a return method (ToList, Any, FirstOrDefault, etc). With this, instead of doing those ugly queries you want to avoid by not using Include(), it will simply fire two queries and properly assign the values in the object you want.

You need to use Include extension method:
var filters=dbContext.DbSet<Filter>()
.Include(f=>f.Types)
.Include(f=>f.Steps)
.Include(f=>f.Flows)
.Include(f=>f.Rooms)
.ToList()
Update
#MrSilent, Include extension method was made exactly for the purpose of loading related entities, I think the other option you have is executing a raw sql, but the way you are doing is not the way to go you have four roundtrips to your database and you need to use join instead in order to get the related entities, Include generates those joins for you and it's just one roundtrip.
This is, eg, another way I guess you could do it, but again, it is against the purpose of using EF, the idea of your model is also to represent the relationship between your tables, not just to represent them individually
var query= from f in context.DbSet<Filter>()
from s in f.Steps
from r in f.Rooms
from t in f.Types
from fl in f.Flows
select new {f, s, r, t, fl};

You can use lazy loading , how :
You first need to get the properties that are Include inclined to be virtual and then an empty constructor that is protected access type to do your job well.
public virtual ICollection<Type> Types{ get; set; }
public virtual ICollection<Step> Steps { get; set; }
public virtual ICollection<Flow> Flows { get; set; }
public virtual ICollection<Room> Rooms { get; set; }
And
//FOR EF !
protected Filter() { }
I think this solution will solve your problem.

Related

EF Core SQL Filter Translation

I'm using EF Core 5.0.1 with ASP NET 5.0.1 Web API and I want to build a query with PredicateBuilder using LinqKit.Microsoft.EntityFrameworkCore 5.0.2.1
For the purposes of the question I simplified my model to:
public class User
{
public long IdUser { get; set; }
public string Name { get; set; }
public virtual ICollection<UserDepartment> UserDepartments { get; set; }
}
public class Department
{
public long IdDepartament { get; set; }
public string Name { get; set; }
public virtual ICollection<UserDepartment> UsersDepartment { get; set; }
}
public UserDepartment
{
public long IdUser { get; set; }
public long IdDepartment { get; set; }
public virtual Department { get; set; }
public virtual User { get; set; }
}
One User can have many Departaments and one Departament can have many Users
Three models have its correspondent table in SQL Server and a IEntityTypeConfiguration class with the appropriate relationships set up.
All I want to achieve is to search any User that belongs to any Departament which Department.Name is in a List<String>.
The List<String> contains a list of keywords, not the exact Department Name
Department table has this kind of rows:
IdDepartment
Name
1
Administration
2
HHRR
3
Sales
4
Marketing
And the List<String> can be any keyword like "Admin", "Sal", "Mark" and so on.
First attempt
... was to build a predicate like that:
List<string> kwDepartments = new List<String> {"mark","admin"};
var predicate = PredicateBuilder.New<User>(true);
predicate = predicate.And(x => x.UserDepartments.Where(y => kwDepartments.Any(c => y.Department.Name.Equals(c))).Any());
This produces a SQL with IN operator, like that:
...[t].[Name] IN (N'mark', N'admin'))
Obviously this is not what I want, but if I use .Contains instead of .Equals, an exception is thrown
The LINQ expression could not be translated
I think this is because I'm trying to evaluate a non-primitive value.
Second attempt
... was to iterate over kwDepartments and add an .Or for each string, like that:
foreach (string dep in kwDepartments )
{
predicateDep = predicateDep.Or(x => x.UserDepartments.Where(y=> y.Department.Name.Contains(dep)).Any());
}
predicate = predicate.And(predicateDep);
This returns the match that I expect, but two problems too.
The SQL translation is poor performance as EF Core anidates a INNER JOIN for each keyword. No matter if kwDepartment has two or three elements, but with 100 or 1000 elements will be inadmissible.
Each INNER JOIN on Departments table have a SELECT with all the table fields and i did not need none of them. I've tried to put a .Select(x=> x.Name) statement in the predicate to take only two fields but it make no effect.
Third attempt
... was using Full Text Search using EF.Functions.FreeText but it seems to make no difference.
My goal is to build a predicate that translate in something similar to:
SELECT [c.IdUser]. [c.Name]
FROM [User] AS [c]
WHERE EXISTS (
SELECT 1
FROM [UserDepartment] AS [u]
INNER JOIN (
SELECT [c0].[IdDepartment], [c0].[Name] <--ONLY NEED TWO FIELDS INSTEAD OF ALL FIELDS
FROM [Department] AS [c0]
) AS [t] ON [u].[IdDepartment] = [t].[IdDepartment]
WHERE ([c].[IdUser] = [u].[IdUser]) AND ([t].[Name] like (N'admin%') or [t].[Name] like (N'mark%')))
It is not mandatory to use the LIKE operator, but i put there for better understanding.
Thanks again!
You are on the right track with Or predicate, but instead of multiple Or predicates on user.UserDepatments.Any(single_match) you should create single Or based predicate to be used inside the single user.UserDepatments.Any(multi_or_match).
Something like this:
var departtmentPredicate = kwDepartments
.Select(kw => Linq.Expr((Department d) => EF.Functions.Like(d.Name, "%" + kw + "%")))
.Aggregate(PredicateBuilder.Or);
and then
predicate = predicate.And(u => u.UserDepartments
.Select(ud => ud.Department) // navigate to department
.AsQueryable() // to be able to use departtmentPredicate expression directly
.Any(departtmentPredicate));
With that code and the sample list, DbSet<User>().Where(predicate) is translated to something like this:
DECLARE #__p_1 nvarchar(4000) = N'%mark%';
DECLARE #__p_2 nvarchar(4000) = N'%admin%';
SELECT [u].[IdUser], [u].[Name]
FROM [User] AS [u]
WHERE EXISTS (
SELECT 1
FROM [UserDepartment] AS [u0]
INNER JOIN [Department] AS [d] ON [u0].[IdDepartment] = [d].[IdDepartament]
WHERE ([u].[IdUser] = [u0].[IdUser]) AND (([d].[Name] LIKE #__p_1) OR ([d].[Name] LIKE #__p_2)))

use Linq to form a relationship where none exists in the database

I've been using the .Net Core Linq expressions .Include and .ThenInclude to great success with my Entity Framework Core project.
However, I need to combine 2 models that are not in any type of relationship in the database.
My SQL query looks like this:
SELECT * FROM selectedEnzymes se
LEFT JOIN enzymeDefinitions ed ON se.selectedEnzymeID = ed.selectedEnzymeID
WHERE se.ecosystemID = 7
selectedEnzymes and enzymeDefinitions are both models.
But there is no relationship between them even though they both contained a selectedEnzymeID. In the database, there is no key.
So I was wondering, is there a way to use Linq to combine two models in such a way if no relationship exists?
Thanks!
You can use the LINQ Join and Select as you do in SQL.
Starting with something like this as models and list of both Classes:
public class SelectedEnzymes
{
public int SelectedEnzymeId { get; set; }
public int EcosystemId { get; set; }
public string OtherPropertySelectedEnzymes { get; set; }
}
public class EnzymeDefinitions
{
public int SelectedEnzymeId { get; set; }
public string OtherPropertyEnzymeDefinitions { get; set; }
}
List<SelectedEnzymes> selectedEnzymesList = new List<SelectedEnzymes>();
List<EnzymeDefinitions> enzymeDefinitionList = new List<EnzymeDefinitions>();
You are able to do something like this:
var query = selectedEnzymesList // table in the "FROM"
.Join(enzymeDefinitionList, // the inner join table
selected => selected.SelectedEnzymeId, // set the First Table Join parameter key
definition => definition.SelectedEnzymeId, // set the Secont Table Join parameter key
(selected, definition) => new { SelectedEnzyme = selected, EnzymeDefinition = definition }) // selection -> here you can create any kind of dynamic object or map it to a different model
.Where(selectAndDef => selectAndDef.SelectedEnzyme.EcosystemId == 7); // where statement
So I was wondering, is there a way to use Linq to combine two models
in such a way if no relationship exists?
In fact, this is similar to the method of obtaining two related tables.
You can directly use the following linq to achieve:
var data = (from se in _context.SelectedEnzymes
join ed in _context.EnzymeDefinitions
on se.SelectedEnzymeId equals ed.SelectedEnzymeId
where se.EcosystemId == 7
select new { se.Name,se.LocationId, ed.Name,ed.CountryId }).ToList();
Here is the result:

How to utilize EF and LINQ to add filters and specify tables, columns, filters and order by dynamically

I have database structure like this (all Parent and Childs are tables),
What I am doing
Now I want to create ad hoc reporting page, which will let user select tables and columns in these tables.
When user will select tables and columns, then they can add filters (is in list or contains or filter by etc..) to columns.
I will send all this information as json to a web service will create, then in web service I am planning to use EntityFramework to get required dataset.
What I already have done
I am able to create HTML UI, web services, database layer with repositories and UOW, database etc..
Question
I mean I can do this,
var result = context.ParentA.Include("Child1.SubChild1").Include(....).Where(..
But I am not sure how can I specify columns I want or add filters.
For Specifying Column Names you can use a select with your linq query like this :
var result = context.ParentA.Include("Child1.SubChild1").Include(....).Where(..).select(s=> new {Name = s.ParentName , SubName = s.SubChild.Name });
For adding Filters you need to define them in the where clause
.Where(p=>p.Name.contains("someValue"))
If you could create some class where you can define the filters then you can create an IQueryable<T> adding filters:
class Filters
{
public bool FilterByColumnA { get; set; }
...
public bool FilterByColumnN { get; set; }
public int FromColumnA { get; set; }
public int ToColumnA { get; set; }
...
public string FromColumnN { get; set; }
public string ToColumnN { get; set; }
}
Then you can build the query:
IQueryable<Entity> query = context.Set<Entity>();
if (filters.FilterByColumnA)
query = query.Where(e => e.ColumnA > filters.FromColumnA && e.ColumnA < filters.ToColumnA);
...
if (filters.FilterByColumnN)
query = query.Where(e => e.ColumnN > filters.FromColumnN && e.ColumnN < filters.ToColumnN);
You can see this question for more details.
To select the properties dynamically, you can choose which explicitly load using Include():
DbQuery<Entity> query = context.Set<Entity>();
foreach (var prop in properties)
query = query.Include(prop);
If the properties are declared virtual, then they will be loaded into memory only when the value is needed. If you are using OData for example, you can create select queries from the url.
Another solution may be using Expressions.
But if you really need a dynamic approach you can check this nuget package.

Does AsQueryable() on ICollection really makes lazy execution?

I am using Entity Framework CodeFirst where I have used Parent Child relations using ICollection as
public class Person
{
public string UserName { get;set}
public ICollection<Blog> Blogs { get; set;}
}
public class Blog
{
public int id { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
Ok, so far everything is working ok, but my concern is, whenever I want to get the Blogs of a person, I get it as
var thePerson = _context.Persons.Where(x => x.UserName = 'xxx').SingleOrDefault();
var theBlogs = thePerson.Blogs.OrderBy(id).Take(5);
Now, I understand that, when the line is executed, all Blogs for that person is loaded into the memory and then sorting and selecting is done from memory. That is not ideal for a record of Person who has large number of blogs. I want to make the Blog Child as IQueryable so that the Sorting and Selecting is done in SQL database before pulling to Memory.
I know I could declare the Blogs as IQueryable in my context so that I could directly query as
var theBlogs = _context.Blogs.Where(.....)
but that is not feasible for me due to design choice, I want to avoid any circular reference as much as possible due to serialization problem. So, I did not make any reference of the parent entity in my child.
I found that, i can call AsQueryable() method on the blogs as
var theBlogs = thePerson.Blogs.AsQueryable().OrderBy(id).Take(5);
That looks like a magic for me and seems too good to be true. So my question. Does this AsQueryable really make the ICollection as IQueryable in reality and makes all Query process in SQL Server (Lazy loading) OR it is just a casting where Blogs are loaded into memory as like before, but change the interface from ICollection to IQueryable ?
So actually it appears that writing your navigation property as IQueryable<T> is not possible.
What you could do is adding a navigation property to Blog:
public class Blog
{
public int id { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public virtual Person Owner { get; set; }
}
From that, you can query as follows so it won't load everything into memory:
var thePerson = _context.Persons.Where(x => x.UserName = 'xxx').SingleOrDefault();
var results = _context.Blogs.Where(z => z.Person.Name = thePerson.Name).OrderBy(id).Take(5)
I suggest you to try LINQPad to see how LINQ is translated into SQL, and what is actually requested from the DB.
A better approach is described in Ladislav's answer. In your case:
var theBlogs = _context.Entry(thePerson)
.Collection(x => x.Blogs)
.Query()
.OrderBy(x => x.id)
.Take(5);

How do I do a OfType<>().Count() on a heterogenous collection in NHibernate?

I have an entity that looks like this:
public class Album
{
public virtual string Name { get; set; }
public virtual IEnumerable<Media> { get; set; }
public virtual IEnumerable<Picture>
{
get { return Media.OfType<Picture>(); }
}
public virtual IEnumerable<Video>
{
get { return Media.OfType<Video>(); }
}
public virtual IEnumerable<Audio>
{
get { return Media.OfType<Audio>(); }
}
}
Where Media is the abstract base class and Picture, Video, and Audio are subtypes of Media, so the IEnumerable<Media> collection is heterogenous.
I have a DTO for Album that looks like this:
public class AlbumDTO
{
public string Name { get; set; }
public int PictureCount { get; set; }
public int VideoCount { get; set; }
public int AudioCount { get; set; }
}
Where each count is being populated by doing <collection>.Count();. Although this code works fine and I get the count for each media type, the generated SQL is less than ideal:
SELECT * FROM Media WHERE media.Album_id = 1
SELECT * FROM Media WHERE media.Album_id = 2
SELECT * FROM Media where media.Album_id = 3
In other words, it's grabbing all the Media first from the database, and then performing the OfType<T>.Count() afterwards in memory. Problem is, if I'm doing this over all the Albums, it will select all the Media from the database, which potentially could be thousands of records. Preferably, I'd like to see something like this (I'm using table-per-hierarchy mapping):
SELECT COUNT(*) FROM Media WHERE media.Album_id = 1 AND discriminator = 'Picture'
SELECT COUNT(*) FROM Media WHERE media.Album_id = 1 AND discriminator = 'Video'
SELECT COUNT(*) FROM Media WHERE media.Album_id = 1 AND discriminator = 'Note'
Does anyone know how I can configure NHibernate to do this? Or will I have to modify my Album entity in order to get the correct behavior?
First off, your code won't compile; you're missing the property name of the IEnumerable<Media> (I assume it's Media), and also of the filters.
Second, you have to understand a little about what's going on. From this behavior, I'm pretty sure you've mapped your Album with a HasMany relationship to Media. NH lazy-loads by default, so when you first retrieve the Album from the DB, Media is given a reference to an NHibernate object called a PersistentBag. This is simply a placeholder that looks like an IEnumerable, and holds the logic to populate the real list when it is actually needed. All it can do is pull the records as mapped in the HBM when its GetEnumerator() method is called (and that happens in virtually every Linq method). So, when you call OfType, you're not working with an NHibernate IQueryable anymore, which can build a SQL statement that does exactly what you want. Instead, you're asking for each element in the list you think you already have, and NHibernate complies.
You have some options if all you want is the Count. The easiest, if possible, is simply to go back to the Session and ask for a whole new query on Album:
session.Linq<Album>().Where(a=>a.Id = 1).Select(a=>a.Media.OfType<Picture>()).Count();
This will directly build a statement that goes to the DB and gets the count of the records. You're not lazy-loading anything, you're asking the Repository for a count, which it knows how to directly translate to SQL.

Categories