In my opinion, lazy loading (which would in my opinion be the case where enumerating the navigation property would trigger a database access) is a bad access pattern, as it simply means that database access will happen at surprising places, which can make application performance difficult to predict.
All the solutions below use an import from System.Data.Entity
.
Solution 1: Use eager loading with an Include
var course = await db.Courses.Include(c => c.Students).FirstOrDefaultAsync(c => c.ID == CourseID);
var student = course.Students.First(p => p.ID == StudentID);
Advantages:
- One database access that is required to load all objects - and this solution scales very well if you want to retrieve more than one
Course
object at a time;
- The
Students
navigation property is loaded and can be used freely;
Drawbacks:
- There is always at least one database access;
- The whole set of related
Student
objects is loaded even if you needed only one;
Solution 2: Use the LoadAsync
method that exists on the concrete collection class;
This solution relies on the fact that lazy-loaded collections are from the EntityCollection<TEntity>
class.
First, I would define an extension method:
public static async Task LoadAsync<T>(ICollection<T> collection)
where T : class
{
if (collection == null) throw new ArgumentNullException("collection");
var entityCollection = collection as System.Data.Entity.Core.Objects.DataClasses.EntityCollection<T>;
if (entityCollection == null || entityCollection.IsLoaded) return;
await entityCollection.LoadAsync(CancellationToken.None).ConfigureAwait(false);
}
Then you could write something like:
var course = await db.Courses.FindAsync(CourseID);
await course.Students.LoadAsync();
var student = course.Students.First(p => p.ID = StudentID);
Advantage:
- There may be no database access at all if the objects are already loaded in the context;
- The navigation property
Students
is guaranteed to be loaded;
Drawbacks:
- Susceptible to the "N+1 queries" issue;
- Both the
Course
and the set of related Student
objects can grow stale, which may trigger concurrency issues down the road; (note that concurrency issues that affect a relationship are harder to resolve than concurrency issues that affect a single record)
Solution 3: Use the CreateSourceQuery
method on the concrete class to load only the Student
object that you want.
OK, doing that does not work, and actually is a pretty bad idea.
However, a solution with the same advantages/drawbacks can be written, but in another way:
var course = await db.Courses.FindAsync(CourseID);
var studentsQuery = from c in db.Courses
where c.ID == CourseID
from s in c.Students
select s;
var student = await studentsQuery.FirstAsync(p => p.ID = StudentID);
Advantage:
- You only load the one
Student
object that you are going to use;
Drawbacks:
- The
Students
navigation property is not loaded, meaning that it cannot be used without potentially triggering a database access;
- The second line will always trigger a database access (susceptible to the "N+1 queries" issue, or even running the method times);
Solution 4: Eager loading, more selective: load both the course and the student that interest you in the initial LINQ query.
I am not 100% sure that that solution will work as written.
var query = from c in db.Courses
where c.ID == CourseID
select new { course = c, student = c.Students.First(p => p.ID == StudentID) };
var result = await query.FirstOrDefaultAsync();
var course = result.course;
var student = result.student;
Advantages:
- Only one database access is required to retrieve both objects;
- You only retrieve the objects that you are going to work on;
Drawbacks:
- The
Students
navigation property is not loaded, meaning that it cannot be used without potentially triggering a database access;
** When to use which solution? **
- If you need the navigation property to be filled (either because you know that you will make use of most of its elements, or because you want to pass the parent entity to another component that is allowed to make use of that property however it wants), then use solution 1 or 2;
- If you do not need the navigation property to be filled, then use solution 4. Only use solution 3 if you categorically have the
Course
object already loaded;