Entity Framework procedure call with navigation properties - c#

I want to call a procedure and access the navigation properties afterwards
using (DbContext c = new DbContext())
{
// System.Data.Objects.ObjectResult<Product>
List<Product> products = c.myProcedure(id).Include("ProductOptions").ToList();
// here the .Include() isn't available
}
my current approach is loading each navigation property seperately
using (DbContext c = new DbContext())
{
List<Product> products = c.myProcedure(id).ToList();
foreach(Product p in products)
{
if(!o.ProductOptions.IsLoaded)
p.ProductOptions.Load();
}
}
is working fine but super slow because of the subselect for each item.
Question: Is there a kind of Include() function or something else to speed up my code?

While using entity framework people tend to use Include instead of Select. The result seems to be the same, however there is a slight difference.
The difference between Include and Select is that Include fetches the complete object, while Select fetches only the selected data. Another difference is that your DbContext object remembers the Included data.
Databases are extremely optimized in fetching data. One of the slower parts of the query is the transfer of the selected data from the database management system to your process. Hence it is wise to limit the amount of fetched data.
Suppose you have database with Schools and their Students: a straightforward one-to-many relations: Every School has zero or more Students, every Student studies at exactly one School, using a foreign key to the School he attends.
Let's query School [10] with its thousand Students:
var result = dbContext.Schools
.Include(school => school.Students)
.Where(school => school.Id == 10)
.FirstOrDefault();
The Complete Student is transferred. Every Student will have the same foreign key SchoolId value 10. So this same number is transferred a thousand times (1001 if you also count School.Id), while you already know the value. what a waste of processing power!
When querying data, always use Select to fetch the data. Only use Include if you plan to update the fetched data
var result = dbContext.Schools
.Where(school => school.Id == 10)
.Select(school => new
{
// Select only the School properties you plan to use
Id = school.Id,
Name = school.Name,
Address = school.Address,
Students = school.Students
.Where(student => ...) // only if you don't want all Students
.Select(student => new
{
// again: select only the properties you plan to use
Id = student.Id,
Name = student.Name,
...
// not needed, you already know the value!
// SchoolId = student.SchoolId,
})
.ToList(),
})
.FirstOrDefault();
I've use new to create an anonymous type. This is the most efficient. If you really need to create Schools and Students, use new School and new Student
.Select(school => new School
{
...
Students = schoolStudents.Select(student => new Student
{
...
})
.ToList(),
})
A last remark. include(school.Students) can be found in using System.Data.Entity

Related

LINQ how do I specify select certain columns in a one to many joins

I have a situation where I have a LINQ query. It has two joins (one to many) but it is bringing back all of the columns in the joined tables. I'm not sure how to create the LINQ query to only being back a few fields from the joined tables.
var data = from mc in ctx.MembershipChapters
where mc.PartitionKey == controllerStateManager.PartitionKey && mc.MembershipId == membershipId
join prd in ctx.Products on mc.ProductId
equals prd.Id into prods
from prd in prods.DefaultIfEmpty()
join oli in ctx.OrderLineItems on mc.OrderLineItemId equals oli.OrderLineItemId into olis
from oli in olis.DefaultIfEmpty()
select new
{
MembershipName = mc.Membership.Name,
Products = prods.Select(p => new {
ProductName = p.Name, ProductId = p.Id }),
OrderLineItems = olis.Select(o => new { OrderLineItemName = o.Description, OrderLineItemId = o.OrderLineItemId })
};
controllerStateManager.Data = data.ToList();
This does not work...I get an error: "o" is not in scope.
Basically the output should follow this:
MembershipChapter
---> OrderLineItems
----------> Products
I'm new to LINQ and I have been struggling on this for far too long.
If you have a one-to-many relationship, and you want the "one" items, each one with its zero or more subitems, like Schools with their zero or more Students; Customers with their zero or more Orders, or, as in your case: MembershipChapters with their OrderLineItems, consider to use one of the overloads of Queryable.GroupJoin.
If you start on the "many" side, and you want each item with its one parent item, so you want the Student with the School he attends, or the Order with the one and only Customer who placed the order, use one of the overloads of Queryable.Join.
I almost always use the overload that has a parameter resultSelector, so you can exactly define what should be in the result.
Requirement: given tables MembershipChapters, OrderLineItems and Products. There is a one-to-many relationship between MembershipChapters and OrderLineItems. Every MembershipChapter has zero or more OrderLineItems, every OrderLineItem belongs to exactly one MembershipChapter, namely the MembershipChapter that the foreign key refers to. There is a similar one to many relation between OrderLineItems and Products. Give me all (or some) MembershipChapters, each MembershipChapter with its zero or more OrderlineItems, and each OrderLineItem with its zero or more Products.
var result = dbContext.MemberShipChapters
.Where(membershipChapter => ...) // only if you don't want all MembershipChapters
.GroupJoin(dbContext.OrderLineItems,
membershipChapter => membershipChapter.Id, // from every membershipChapter get the primary key
orderlineItem => orderLineItem.MembershipChapterId, // from every OrderlineItem get the foreign key
// parameter resultSelector: from every MembershipChapter with its zero or more
// OrderLineItems, make one new:
(membershipChapter, orderLineItemsOfThisMembershipChapter) => new
{
// Select only the membershipChapter properties that you plan to use
Id = membershipChapter.Id,
Name = membershipChapter.Name,
...
// The zero or more OrderLineItems of this membershipChapter
OrderLineItems = orderLineItemsOfThisMembershipChapter
.Select(orderLineItem => new
{
// Select only the OrderLineItems that you plan to use:
Id = orderLineItem.Id,
...
// not needed, you already know the value
// MembershipChapterId = orderLineItem.MembershipChapterId,
})
.ToList(),
});
This is fairly straightforward. However, if you want to GroupJoin three tables, then this looks horrible, although it is doable.
Another method that looks simpler:
var result = dbContext.MemberShipChapters
.Where(membershipChapter => ...)
.Select((membershipChapter => new
{
Id = membershipChapter.Id,
Name = membershipChapter.Name,
...
OrderLineItems = dbContext.OrderLineItems
// keep only the OrderLineItems with a foreign key referring to this MembershipChapter
.Where(orderLineItem => orderLineItem.MemberShipChapterId == membershipChapter.Id)
.Select(orderLineItem => new
{
Id = orderLineItem.Id,
...
// do the same with the third table
Products = dbContext.Products
.Where(product => product.OrderLineItemId == orderLineItem.Id)
.Select(product => new
{
Id = product.Id,
Price = product.Price,
...
})
.ToList(),
})
.ToList(),
});
It is a little hard to read, but if the domain is linked correctly then I think you just want to end up with a query like this:
from ol in ctx.OrderLines where
ol.MembershipChapter.PartitionKey == controllerStateManager.PartitionKey
select new {ol.Whatever, ol.Product.Whatever};

The Include path expression must refer to a navigation property defined on the type (select data using LINQ)

I was trying to select data using LINQ
and I have a list called "products" and I want just these items that exist in products list
var Owner = db.Owners
.Where(m => m.ID == id)
.Include(m => m.Products.Where(item1 => products.Any(item2 => item2.ProductID == item1.ProductID)).ToList())
.FirstOrDefault();
but I'm getting this error :
System.ArgumentException: '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'
Include is meant to fetch complete rows of a table, inclusive primary and foreign keys.
Usually it is not efficient to fetch the complete rows of a table. For example, suppose you have a database with Schools and Students. There is a one-to-many relation between Schools and Students: every School has zero or more Students, every Student attends exactly one School, namely the School that the foreign key refers to.
If you fetch School [10] with its 2000 Students, then every Student will have a foreign key SchoolId with a value 10. If you use Include and fetch complete Student rows you will transfer this value 10 over 2000 times. What a waste of processing power!
A DbContext has a ChangeTracker object. Whenever you fetch data without using Select, so if you fetch complete rows, then the fetched rows are stored in the ChangeTracker, together with a Clone of it. You get a reference to the Clone (or the original, doesn't matter). When you change properties of the fetched data, you change the value in the Clone. When you call SaveChanges, the values of all properties of all originals in the ChangeTracker are compared with the values in the Clones. The items that are changed are updated in the database.
So if you fetch School [10] with its 2000 Students, you are not only fetching way more data than you will ever use, but you will also store all these Students in the ChangeTracker together with a Cloned Student. If you call SaveChanges for something completely different (change of the telephone number of the School for instance), then all Students are compared by value property by property with their Clones.
Generic rule:
Whenever you fetch data using Entity Framework, always use Select, and Select only the properties that you actually plan to use. Only fetch complete rows and only use Include if you plan to update the fetched data.
Using Select will also solve your problem:
int ownerId = ...
IEnumerable<Product> products = ...
var Owner = db.Owners.Where(owner => owner.ID == ownerId)
.Select(owner => new
{
// Select only the Owner properties that you actually plan to use
Id = owner.Id,
Name = owner.Name,
// get the Products of this Owner that are in variable products
Products = owner.Products
.Where(product => products.Any(p => p.ProductId == product.ProductId)
.Select(product => new
{
// Select only the Product properties that you plan to use
Id = product.Id,
Price = product.Price,
...
// No need to fetch the foreign key, you already fetched the value
// OwnerId = product.OwnerId,
})
.ToList(),
...
})
.FirstOrDefault();
I used automatic types (new {...}). If you really want to create Owner and Properties, use:
var Owner = db.Owners.Where(...)
.Select(owner => new Owner
{
Id = owner.Id,
...
Products = owner.Products.Where(...).Select(product => new Product
{
Id = product.Id,
...
})
.ToList(),
})
.FirstOrDefault();
Try the following:
var productIds = products.Select(x => x.ProductID);
var Owner = db.Owners
.Where(m => m.ID == id)
.Include(m => m.Products.Where(product => productIds.Contains(product.ProductID))
.FirstOrDefault();

Joining two tables using Include() with conditional filter

I have existing code to update. I want to join another table called Table3.
Since the query has an include to Table2, I want to add another .Include with a conditional filter and avoid a left join.
When I add the .include with the .where, I can't access t3Id. Intellisense just shows the table, Table3 and not the Id field.
Did I miss a syntax? Thanks.
Table1 has a key called t1Id.
var query = (from e in ctx.Table1
.Include(r => r.Table2.Select(p => p.name))
.Include(rj => rj.Table3).Where(s => s.t1Id == t3Id)
select e).ToList();
Table1 will have the following:
Id name
1 Joe
2 Mary
3 Harry
Table3 will have the following:
t1Id title
3 staff
3 fulltime
Expected Outcome:
1 Joe
2 Mary
3 Harry [{2, staff}, {3, fulltime}]
Since Harry has a record in the mapping table, he will have an array of Table3 rows.
I just noticed your EF comment. The EF property Table1.Table3 should already load the related entities without using a where clause when you use the include.
Try this extension method
public static IEnumerable<TEntity> GetAllIncluding(this params Expression<Func<TEntity, object>>[] includesProperties)
{
return includesProperties.Aggregate<Expression<Func<TEntity, object>>,
IQueryable<TEntity>>(_dbSet, (current, includeProperty)
=> current.Include(includeProperty));
}
To use the function
var data = table1.GetAllIncluding(x => x.Table2, y => y.Table3);
This should already load the related entities without having to filter.
It is better to use Select instead of Include whenever you can. Select will allow you to query only the properties you really plan to use, making the transfer of the selected data from the database management system to your process faster.
For instance, if you query "Schools with their Students", ever Student will have a foreign key with a value equal to the School's primary key. So if you have School 10, you'll now that all its 5000 Students will have a SchoolId with a value 10. It is a bit of a waste to send this same value 10 over 5000 times.
When querying data, always use Select. Only use Include if you plan to update the fetched data.
Your query (in method syntax):
var result = dbContext.Table1
.Where(table1Element => ...) // only if you don't want all elements of Table1
.Select(table1Element => new
{
// only select the table1 elements that you plan to use:
Id = table1Element.Id,
Name = table1Element.Name,
// Select the items that you want from Table 2:
Table2Items = table1Element.Table2
.Where(table2Element => ...) // only if you don't want all table2 elements
.Select(table2Element => new
{
// Select only the properties of table2 you plan to use:
Id = table2Element.Id,
Name = table2Element.Name,
...
// the following not needed, you already know the value:
// Table1Id = table2Element.table1Id, // foreign key
})
.ToList(),
// Table3: your new code:
Table3Items = table1Element.Table3
.Select(table3Element => new
{
// again: only the properties you plan to use
Id = table3Element.Id,
...
// the following not needed, you already know the value:
// Table1Id = table3Element.table1Id, // foreign key
})
.ToList(),
});
You see that it is much easier for the reader to see which properties he gets? If one of the tables is expanded, then the new properties are not fetched in this query, after all: the user apparently didn't need the new properties. They are also not described in the specifications of this function.
Note: Because I used new, my types were anonymous types. You can only use them within your function. If you need to return the fetched data, put the data in a known class:
.Select(table1Element => new Table1Class()
{
Id = table1Element.Id,
Name = table1Element.Name,
...
});
Again: consider not to fill the foreign keys, as you probably won't use them

Entity Framework Takes Too Much Time where Adding "WHERE Clause"

I'm working on Schools System, and the System have many Users(teachers And Managers).
(Tools)
Asp.net 5 (MVC With C#) VS 2015
SQL SERVER (Azure)
Code-First Approach
Each user Assigned to One Or More Of Classes.
(The User Should only See His Classes And Students inside of Classes)
in the other side
to catch Each User Classes
I've write this Method
public static IEnumerable<Class> GetClasses()
{
string UserId = HttpContext.Current.User.Identity.GetUserId();
ApplicationDbContext db = new ApplicationDbContext();
var Job = db.Jobs.Where(j => j.UserId == UserId).FirstOrDefault();
if (Job.EntityLevelID == 1)
{
var x = db.Classes.Where(a => a.Levels.SupervisionCenter.Organization.Id == Job.EntityID) as IEnumerable<Class>;
return x;
}
return null;
}
in The Students List Controller I just want the User to get his Students which are Only in his Classes
to get all the Students i write this Code
var items = db.Students.AsEnumerable().Where(o => o.Name.ToLower().Contains(search)).AsQueryable();
This Code will Give all the Students, it takes around 10 Sec to load and finish all the data (But that's is not needed Bcs, i need to Filter on ClassID Field).
when i need to filter on user Permissions based-classes
I've edit the code to be
var items = db.Students.AsEnumerable().Where(o => o.Name.ToLower().Contains(search))
.Where(s => UserDB.GetClasses().Select(c => c.Id).Contains(s.ClassID.Value))
.AsQueryable();
in the Previous case, when i add the where query it takes more than 80 Sec
EDIT (1)
The Organization Chart Will Be
Organizations
SuperVisionCenter
Schools
Classes
So I don't know what is the Problem I've made it here, can you please advise me for this
Thanks And Regards.
The very first thing that is slowing you down is AsEnumerable()
See here the effects of using AsEnumerable()
Basically you are getting all students in memory then running your where clause in app memory instead of just getting selected students.
Edit 1:
o.Name.ToLower().Contains(search)
I dont think you need to do ToLower() as MSSQL is case insensitive by default. This should remove the need for db.Students.AsEnumerable()
Edit 2:
Why don't you use join ? That would be a good optimisation to begin with.
something like
List<Student> data = (from student in db.Students
join clss in db.Classes on student.Class equals clss
where student.Name.Contains(search)).ToList()
This solved My Problem, and it takes around 3 to 4 seconds only.
int[] Classes = UserDB.GetClasses().Select(c => c.Id).ToArray();
var items = db.Students.Where(o => o.Name.ToLower().Contains(search))
.Where(r => Classes.Contains(r.ClassID.Value)).AsQueryable();

Deleting selected relations in many-to-many relationship in EF?

In SQL, I have:
[Bands] 1 ---- * [BandsGenres] * ---- 1 [Genres]
In my EF layer, it translates to:
[Bands] * ---- * [Genres]
(BandsGenres is BandId and GenreId)
My problem is, I can't seem to figure out how to delete just the selected relationships I need. Basically, I'm trying to update a list of type Genre for a band. A list of Guids is passed in and I'm able to get those using joins. However, the .Remove() method isn't removing the old genres and adding the new ones. I tried using .Attach(g) and .DeleteObject(g) but that removed the actual Genre (from Genres) instead of just the relationship.
public void UpdateBandGenres(Guid bandId, IEnumerable<Guid> genres)
{
using (var ctx = new OpenGroovesEntities())
{
var newGenres = from g in ctx.Genres
join t in genres on g.GenreId equals t
select g;
//var bandGenres = ctx.Genres.Include("Bands").Where(g => g.Bands.Any(b => b.BandId == bandId));
var bandGenres = ctx.Bands.SingleOrDefault(b => b.BandId == bandId).Genres;
bandGenres.ToList().ForEach(g =>
{
bandGenres.Remove(g);
});
newGenres.ToList().ForEach(g => bandGenres.Add(g));
ctx.SaveChanges();
}
}
How can I delete/add or update my list of genre relationships for a band, given a list of genre IDs? Thanks.
If I understand your problem correcly, the genres collection contains all the Genre objects that should be in the Band genre list as a result of running the UpdateBandGenres method (and not just the list to add or list to delete). In that case the simplest would be removing all the genres from the collection and adding all the genres with the Guids from your genres collection.
First of all, you don't need joins to grab your newGenres:
var newGenres = ctx.Genres.Where(g => genres.Contains(g.GenreId));
Secondly, you need to fetch the Band object, since modifying its Genres collection will tell EF to modify the BandGenres table in SQL:
Band band = ctx.Bands.SingleOrDefault(b => b.BandId == bandId);
After that you can clear the band.Genres collection and add the newGenres on top. As a result you code will look like:
public void UpdateBandGenres(Guid bandId, IEnumerable<Guid> newGenreIds)
{
using (var ctx = new OpenGroovesEntities())
{
List<Genre> newGenres = ctx.
Genres.
Where(g => newGenreIds.Contains(g.GenreId)).
ToList();
Band band = ctx.Bands.Single(b => b.BandId == bandId);
band.Genres.Clear();
newGenres.ForEach(band.Genres.Add);
ctx.SaveChanges();
}
}
Btw, I'd also recommend being consistent with naming your variables - e.g. IEnumerable<Guid> genres might be a bit misleading, since it's actually a collection of Guids, and not a collection of Genre objects. Therefore I named it newGenreIds to be consistent with your Guid bandId variable name.

Categories

Resources