EF6 calling include after asenumerable - c#

I have an entity with a few ordinary 1 to many relationships and one collection with implicit 1 to many relationship without any relationship defined.
What I'm trying to do is to make it work together like:
IQueryable<Employee> leftJoin =
_dbContext.EmployeeList
.GroupJoin(
inner: _dbContext.ContactList.Where(x => x.TableType == TableType.Employee),
outerKeySelector: employee => employee.EmployeeId,
innerKeySelector: contact => contact.TableId,
resultSelector: (employee, contacts) => new { employee, contacts = contacts.DefaultIfEmpty() }
)
.AsEnumerable()
.Select(x =>
{
x.employee.ContactList = x.contacts;
return x.employee;
})
.AsQueryable()
.Include(x => x.EmployeeRoleMapList);
The main hassle is I need to somehow init Employee.ContactList from joined set, within IQuerable it's not possible, I'm made to cast it to the IEnumerable but after that ordinary include doesn't work which is logical as well. Is there some workaround or some different way I could use to achieve this?

Related

join table to table related to the parent table

Still new to LINQ so be nice.
This query:
var query = _ODSContext.AllFacilities
.Where(f => f.AllFacilityContacts.Any(c => ProviderContactIds.Contains(c.ContactID) &&
(c.ContactTypeName == "Primary Rep")))
.Where(f => f.TermDate > DateTime.Now)
.Include(a => a.Address)
.Include(b => b.AllFacilityContacts)
.Include(c => c.AllPractitionerLocations)
.Include(e => e.AllFacilityServices)
.OrderBy(f => f.FacilityName);
works fine.
However, I want to add a table that joins to AllpractitionerLocations:
Here's what I tried:
var query = _ODSContext.AllFacilities
.Where(f => f.AllFacilityContacts.Any(c => ProviderContactIds.Contains(c.ContactID) &&
(c.ContactTypeName == "Primary Rep")))
.Where(f => f.TermDate > DateTime.Now)
.Include(a => a.Address)
.Include(b => b.AllFacilityContacts)
.Include(c => c.AllPractitionerLocations)
.Include(d => d.AllPractitionerNetworkSpecialty)
.Include(e => e.AllFacilityServices)
.OrderBy(f => f.FacilityName);
But I get:
'AllFacility' does not contain a definition for
'AllPractitionerNetworkSpecialties' and no accessible extension method
'AllPractitionerNetworkSpecialties' accepting a first argument of type
'AllFacility' could be found (are you missing a using directive or an assembly reference?
Which is technically accurate. AllPractitionerNetworkSpecialties is related to AllPractitionerLocations.
How do I do the join between those two tables in LINQ?
Thanks,
You use ThenInclude
Simplistically, Include starts off from the root entity, ThenInclude carries on from the entity type that you call it on. If you conceive your graph as a hub-spoke type affair with AllFacilities as the hub, your Include starts another spoke from the hub, whereas ThenInclude continues an existing spoke. If a customer has orders and orders have products, you'd context.Customer.Include(... Orders).ThenInclude(... Products). If you wanted to "go back to the hub" and get the Customer>Address>Country>TaxYearCodings you'd
context.Customer
.Include(... Orders)
.ThenInclude(... Products)
.Include(... Address)
.ThenInclude(... Country)
.ThenInclude(... TaxYearCodings)
We (at work) tend to indent another level when we ThenInclude to signify "continuing a spoke", and indent all the Includes the same to signify "going back to the hub".
Possibly also worth pointing out that you can chain your Include and ThenInclude by accessing the chain "in a one-er" but you can't navigate into collections, only single props - you start another inclusion when you hit a collection
customer.Include(c => c.Address.Country.TaxYearCodings).ThenInclude(tyc => ...)
Be careful; the amount of data you load when you start (then)including a lot can be enormous

Entity Framework Structure

I am going through this tutorial to help me better understand the EF Structure. I currently use SQL.
https://learn.microsoft.com/en-us/aspnet/core/data/ef-rp/read-related-data?view=aspnetcore-2.1&tabs=visual-studio
In this example, it shows the instructor, office, student, course, grade, and assignments
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}
To help me better understand the syntax would this SQL Statement be the equivalent?
SELECT *
FROM Instructor INNER JOIN
OfficeAssignment ON Instructor.ID = OfficeAssignment.InstructorID INNER JOIN
Department ON Instructor.ID = Department.InstructorID INNER JOIN
Course ON Department.DepartmentID = Course.DepartmentID INNER JOIN
Enrollment ON Course.CourseID = Enrollment.CourseID INNER JOIN
CourseAssignment ON Course.CourseID = CourseAssignment.CourseID INNER JOIN
Student ON Enrollment.StudentID = Student.ID
WHERE Instructor.ID = #ID AND Course.CourseID = #CourseID ORDER BY Instructor.Lastname
It helps to use entities as objects rather than thinking of them as tables. Yes, they typically correlate directly to the underlying tables, but that is a means to an end. You can leverage the relationships more directly than simply treating it as another way to write SQL.
For example:
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
This will correspond roughly to an SQL statement with a bunch of inner joins and an OrderBy clause. In the realm of EF though, this would be considered bad practice. The reason is that like an SQL statement with inner joins, you are effectively doing a "SELECT *" across all of those tables. Do you really want all of the columns of all of the joined tables?
AsNoTracking() merely tells EF that for the data retrieved, you aren't going to modify it, so don't bother tracking dirty state. This is a performance tweak for read operations.
ToListAsync() performs the query as an awaitable operation which will free up the thread the method was called on. No magic multi-threaded execution here, just the call can hand off to SQL Server, release it's thread, then be assigned a new thread based on a continuation point after the await.
One warning sign I see with the example is the use of the null-able parameters. Can this method validly be called with:
Neither an ID or course ID?
and
An ID with no course ID?
and
A course ID with no ID?
and
Both an ID and course ID?
If any of these combinations is invalid then the method should be split up or refined.
Getting back to the "SELECT *" behaviour, using EF you have a lot of power hiding behind the scenes ready to turn Linq map/reduce operations into SQL to run against the server and return you a meaningful, minimal set of data.
For example:
var query = _context.Instructors.AsQueryable();
if (id.HasValue)
query = query.Where(i => i.ID == id.Value);
query = query.OrderBy(i => i.LastName);
var instructors = await query.Select(i => new InstructorIndexData
{
InstructorId = i.ID,
// ...
Courses = i.CourseAssignments.Select(ca => new CourseData {
CourseId = ca.Course.ID,
CourseName = ca.Course.Name,
//..
}
}).ToListAsync()
if (courseId.HasValue)
{
var enrollments = await query.SelectMany(i => i.Courses.SingleOrDefault(c => c.CourseID == courseID.Value).Enrollments.Select(e => new EnrollmentData
{
InstructorId = i.ID,
EnrollmentId = e.EnrollmentID,
CourseId = e.Course.CourseID,
//...
}).ToListAsync();
// From here, group the Enrollments by Instructor ID and add them to the Instructor index data.
var groupedEnrollments = enrollments.GroupBy(e => e.InstructorId);
foreach(instructorId in groupedEnrollments.Keys)
{
var instructor = instructors.Single(i => i.InstructorId == instructorId);
instructor.Enrollments = groupedEnrollments[instructorId].ToList();
}
}
Now the caveat here is that I'm basing this on memory and with a rough guess of your structure and desired output. The key points would be leveraging the IQueryable and issuing Select statements to just pull back the exact data you need to populate the objects you want to provide to a view.
I do this in 2 query executions, one to get the instructor(s), then the second to get the enrollments if requested based on the provided course ID. Personally I'd split this into two methods since I'd expect the enrollments would be optional. Also there is a difference between fetching one instructor, and all instructors. In cases where potentially large amounts of data are returned, you should look at establishing pagination with Skip() and Take() to avoid expensive queries bogging down the CPU, network, and memory usage.

C# LINQ Filter deep nested list

I've a structure based of list containing other list. I need to filter the list based on the value of a property based in the deepest part of the list.
Right now I'm doing this:
queryable = queryable
.Include(x => x.Carriers)
.ThenInclude(c => c.CarrierActions)
.ThenInclude(ca => ca.Action)
.ThenInclude(ac => ac.ActionFacts);
queryable = queryable
.Where(x => x.Carriers.Any(
carriers => carriers.CarrierActions.Any(
carrieractions =>
carrieractions.Action.ActionTypeId ==
ActionTypeEnum.DosemeterCalculateDeepDose)));
I join the needed tables, then I filter them based on the ActionTypeId based 3 levels below the top list.
First off all, is it possible to do this in 1 step ( include the filtering with the joins ), second of all, the second part is not working as my list gets empty, but I'm certain that actions with that type get values.
Using .NET Core 2.0.3 btw!
To answer your first part, you can do this
queryable = queryable
.Include(x => x.Carriers)
.ThenInclude(c => c.CarrierActions)
.ThenInclude(ca => ca.Action)
.ThenInclude(ac => ac.ActionFacts)
.Where(x => x.Carriers.Any(
carriers => carriers.CarrierActions.Any(
carrieractions =>
carrieractions.Action.ActionTypeId ==
ActionTypeEnum.DosemeterCalculateDeepDose)))
To your second part, it should be working, id check your data, as this is pretty straight forward

Using .Where() clause inside an .Include() on a linq query with multiples includes

I would like to get a collection of Customers including several properties among which is the address but only when it has not been deleted yet (SuppressionDate == null)
IQueryable<Customer> customers =
context.Persons.OfType<Customer>()
.Include(customer => customer.Addresses)
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors);
I have tried several ways to use the where clause in order to filter address:
...
.Include(customer => customer.Addresses.Where(a => a.SuppressionDate == null))
.Include(customer => customer.Bills)
...
That was my first try but it raises the following exception:
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
I've also tried with the same where clause at the end of the Include() and at the end of the query but neither seems to work.
I'm currently using a workaround which is iterate through the collection of customer and remove the addresses that are deleted as such:
foreach(Customer c in customers){
customer.Addresses = customer.Addresses.Where(a => a.SuppressionDate == null).ToList();
}
Being fairly new to linq to object/entities, I was wondering if there was a built-in way to achieve this.
If you were getting a single customer you could use explicit loading like this:
var customer = context.Persons
.OfType<Customer>()
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors)
.FirstOrDefault(); //or whatever
context.Entry(customer).Collections(x => x.Addresses).Query().Where(x => x.SuppressionDate == null).Load();
That makes a nice query and two simple calls to the database. But in this case you are getting a list (or collection or whatever) of customer and there's no shortcuts. Your "workaround" may cause a lot of chatter with the database.
So you probably just have to take it one step at time:
//1. query db to get customers
var customers = context.Persons
.OfType<Customer>()
.Include(customer => customer.Bills)
.Include(customer => customer.Code)
.Include(customer => customer.Tutors)
.ToList();
//2. make an array of all customerIds (no db interation here)
var customerIds = customers.Select(x => x.CustomerId).ToArray();
//3. query db to get addresses for customers above
var addresses = context.Addresses.Where(x => customerIds.Contains(x.CustomerId).ToList();
//4. assign addresses to customers (again, no db chatter)
foreach (var customer in customers)
{
customer.Addresses = addresses
.Where(x => x.CustomerId == customer.CustomerId && x.SuppressionDate == null)
.ToList();
}
Not too bad- still just two queries to database.

Why am I getting a Compile time error in this linq query?

I am getting a compile time error when compiling the below code and I can't see why:
Relations are many to many relations
var contacts = groups_to_querry
.SelectMany(x => x.Contacts)
.Where(x => x.ID == Guid.Empty)
.SelectMany(p => p.ContactDetails)
.Where(x => x.ID == Guid.Empty)
.SelectMany(x => x.Contacts); //This line gives me a compile time error
//Error : The Type argumetns for method 'System.Linq.Enumerable.SelectMany<TSource,Tresult>
//(System.Collections.Generic.IEnumerable<TSource>, System.Func<TSource,
//System.Collections.Generic.IEnumerable<TResult>>)' cannot be infrred from the usage.
//Try specifying the type arguments explicitly
The second time you call for .SelectMany(x => x.Contacts), you are currently working with a collection of ContactDetails. It is doubtful that you would be able to use SelectMany on it. You would need to use Select instead.
SelectMany is used when you want to select multiple collections of items and put them into one IEnumerable. Select is used on individual fields. Since you are working with objects of type ContactDetail (which I assume can only have one contact), you would need use Select
EDIT: Here is what you're doing in a nutshell, step by step:
groups_to_querry.SelectMany(x => x.Contacts): From all the groups that I want to query select all of their many contacts. Each group has many contacts, so put them all into a single IEnumerable collection of type Contact
.Where(x => x.ID == Guid.Empty): ...but only those Contacts with an empty ID
.SelectMany(p => p.ContactDetails): Then select all of those Contacts' many ContactDetails. Each Contact has many ContactDetails, so put them all into a single IEnumerable collection of type ContactDetail
.Where(x => x.ID == Guid.Empty): ...but only those ContactDetails with an empty ID
.SelectMany(x => x.Contacts);: Now select each of the ContactDetails' many Contacts. However, since the compiler knows that there is a one-to-many relationship between Contacts and ContactDetails (and not the other way around) that statement is not possible, and thus shows a compile error
I'm interpreting your intended query as "from multiple groups of contacts, select all contacts that have ID=Guid.Empty and also have details that all have ID=Guid.Empty".
The way your code is actually interpreted is "from all contacts that have Guid.Empty, select all details that have Guid.Empty, and from those details select all contacts". The first problem is that you end up selecting from details. This means the final SelectMany should be a Select, because x.Contacts here refers to the many-to-one relationship from details to contacts.
The second problem is that the result will contain duplicates of contacts, because the same contact is included for each details. What you should be doing instead is filtering the contacts directly based on their details collections, like this:
groups_to_query
.SelectMany(g => g.Contacts)
.Where(c => c.ID == Guid.Empty)
.Where(c => c.ContactDetails.All(d => d.ID == Guid.Empty))
Note this would also select contacts that have zero details, which is different behavior from your query, so I'm not sure if it's what you want. You could add another filter for ContactDetails.Any() if not.
Edit: Since you're using Entity Framework, the above probably won't work. You may need to select the details in a subquery and then filter in memory after it executes:
var queryResult =
groups_to_query
.SelectMany(g => g.Contacts)
.Where(c => c.ID == Guid.Empty)
.Select(c => new {
contact = c,
detailIDs = c.ContactDetails.Select(d => d.ID)
}).ToList();
var contacts =
queryResult
.Where(r => r.detailIDs.All(id => id == Guid.Empty))
.Select(r => r.contact);

Categories