Entity Framework Core 3.1 - Part 3

Entity Framework Core 3.1 - Part 3

Overview

In my previous post, we talked about data access and updates in entity framework.

A naive way to update related data, especially when the context isn't tracking your object, as in, you send data about an existing entity in the database, from the UI, and EF has to figure out what part of the updated record has changed and needs an update in the database.

If you ever encounter a situation where you are to update only one related object of a primary entity, then you might be in for a surprise. I am going to use an example that I followed to understand this from Julie Lerman's Pluralsight course on EFCore 3. I have never had to do this personally in the round-about way that the example states. However, I am pretty sure I might end up with tricky situations as I spend more time with EF.

 1    /*
 2    // this is probably only ever done for a demonstration. 
 3    // Had I known which quote I needed to modify, I would have fetched that directly
 4    // and modified it. Not done it this way.
 5    // The SQL generated for the following savechanges, would be specifically related // to the Quote that was modified. Because the Change Tracker is in scope, 
 6    // everything is as expected.
 7    */
 8    private static void ModifyingRelatedDataWhenTracked()
 9    {
10        var philosopher = _context.Philosophers.Include(p => p.Quotes).FirstOrDefault(p=> p.id==2);
11        philosopher.Quotes[0].Text = "Did you hear that?";
12        _context.SaveChanges();
13    }
14    
15    /*
16    // I doubt anyone would do something like this.
17    // This is a roundabout way of updating a related record.
18    // the sql generated for this one, will surprisingly update every quote
19    // associated to the philosopher, which is ridiculous and not obvious to 
20    // anyone reading the c# code below
21    */
22    private static void ModifyingRelatedDataWhenNotTracked()
23    {
24        var philosopher = _context.Philosophers.Include(s => s.Quotes).FirstOrDefault(s=> s.id==2);
25        philosopher.Quotes[0].Text = "Did you hear that?";
26        var quote = philosopher.Quotes[0];
27        using(var newContext = new PhilosopherContext())
28        {
29            newContext.Quotes.Update(quote);
30            newContext.SaveChanges();
31        }
32    }
33    
34    /*
35     * How on earth do we get around it? If you thought you could use Attach(),
36     * then you are wrong. I thought the same too. But nope. Apparently in this
37     * scenario, Attach() would mark all as unchanged!
38     * The solution, use the DbContext's Entry() method.
39    */
40    private static void ModifyingRelatedDataWhenNotTracked()
41    {
42        var philosopher = _context.Philosophers.Include(s => s.Quotes).FirstOrDefault(s=> s.id==2);
43        philosopher.Quotes[0].Text = "Did you hear that?";
44        var quote = philosopher.Quotes[0];
45        using(var newContext = new PhilosopherContext())
46        {
47            // Entry focuses specifically focuses on the entity passed to it
48            newContext.Entry(quote).State = EntityState.Modified;
49            newContext.SaveChanges();
50        }
51    }

Interacting with Many to many relationships

Let us revisit the Student and Courses example in part 1. I will paste the code below to refresh your memory.

 1    // the mapping/join table that connects students with courses
 2    // and vice versa
 3    public class StudentCourse
 4    {
 5        public int StudentId { get; set; }
 6        public Student Student { get; set; }
 7    
 8        public int CourseId { get; set; }
 9        public Course Course { get; set; }
10    }
11    
12    public class Student
13    {
14        public int Id { get; set; }
15        public string Name { get; set; }
16        public IList<StudentCourse> StudentCourses { get; set; }
17    }
18    
19    public class Course
20    {
21        public int Id { get; set; }
22        public string CourseName { get; set; }
23        public IList<StudentCourse> StudentCourses { get; set; }
24    }

Both Student and Course have a navigation property StudentCourses to easily fetch the courses that a student has signed up for and also the students that have signed for a course. This is a good real world, many to many relationship example.
So in this example, although there is a StudentCourse join table, it is not added to the DbContext as a separate DbSet for direct modification. This means you cannot use context.StudentCourses.Add() to add a record there. You have to add a record via the main entity's navigation property. So what do you do now?
DbContext.Add() to the rescue.

1    private static void AddStudentCourse(int studentId, int courseId)
2    {
3        var studentCourseJoin = new StudentCourse { StudentId = studentId, CourseId = courseId; };
4        dbContext.Add(studentCourseJoin);
5        dbContext.SaveChanges();
6    }

What if you have a course and you want to say, a student has joined this course? If the context is tracking all changes in the scope, then you can do this as follows:

 1    /* 
 2     * as EF is tracking changes, by writing the following code, 
 3     * EF will be able to find out that this Student is enlisted 
 4     * for the course fetched earlier.
 5    */
 6    private static void EnlistStudentIntoCourse()
 7    {
 8        var course = dbContext.Courses.Find(1);
 9        course.StudentCourses.Add(new StudentCourse { StudentId = 2 } );
10        dbContext.SaveChanges();
11    }

So now that you have done a direct insert and a related insert into the StudentCourses join table, let us take a look at the Delete.

1    /*
2     * Code is very similar to the Add written earlier, except that this time we use Remove method instead. 
3    */
4    private static void RemoveStudentCourse(int studentId, int courseId)
5    {
6        var studentCourseJoin = new StudentCourse { StudentId = studentId, CourseId = courseId; };
7        dbContext.Remove(studentCourseJoin);
8        dbContext.SaveChanges();
9    }

Querying many-to-many relationships

As EFCore forces you to have a join table, it is useful to think about querying many-to-many relationships as querying a parent, child and grandchild at once.

If you are querying based on Student, then Student is the parent, StudentCourse is the Child and the Course would be the grand child.

This can be queried using a special combination.

 1    /*
 2     * One way to do this.
 3     * The downside to this is that to access courses, you 
 4     * have to navigate through two levels 
 5    */
 6    private static void GetStudentWithCourses()
 7    {
 8        var studentWithCourses = dbContext.Students
 9            .Include(s => s.StudentCourses)
10            .ThenInclude(sc => sc.Course)
11            .FirstOrDefault(s => s.Id == 2);
12    }
13    
14    /*
15     * Another way to do the exact same thing, probably clearer
16    */
17    private static void GetStudentWithCoursesUsingProjections()
18    {
19        var studentWithCourses = dbContext.Students.Where(s => s.Id == 2)
20            .Select(s => new 
21            {
22                Student = s,
23                Courses = s.StudentCourses.Select(sc => sc.Course)
24            })
25            .FirstOrDefault();
26    }

Persisting Data in One to One

Let us first set the stage for this one with an example scenario, so that it is easier to understand what I am trying to talk about.

 1    /*
 2     * The University has decided to issue every sutdent with an IPad
 3     * this information is now stored like this
 4    */
 5    public class Student
 6    {
 7        public int Id { get; set; }
 8        public string Name { get; set; }
 9        public IList<StudentCourse> StudentCourses { get; set; }
10        // every student has an university registered ipad
11        public IPad IPad { get; set; }
12    }
13    
14    /*
15     * IPads have a studentId in them to map back to the Student but no navigation property to retrieve a student record directly
16    */
17    public class IPad
18    {
19        public int Id { get; set; }
20        public string Name { get; set; }
21        public int StudentId { get; set; }
22    }

In this case, EF will also create a unique foreign key constraint when you try and generate the database creation scripts for this one. So that every studentId in the IPad table is unique.

Let us see how to add a new student with a new IPad

 1    /*
 2     * The most straightforward way of adding one entity and its
 3     * related entity in one go for a 1-1 relationship
 4     * SQL would be to insert into Students, get identity, and insert into IPads with that id.
 5    */
 6    private static void AddNewStudentWithIpad()
 7    {
 8        var student = new Student { Name = "Barrack Obama" };
 9        student.IPad = new IPad { Name = "Barrack's Ipad" };
10        dbContext.Students.Add(student);
11        dbContext.SaveChanges();
12    }

But many students in the University currently do not have an official IPad yet. They are only getting this now. So how do we just add an IPad record for an existing student?

 1    /*
 2     * Not complicated. Still simple
 3    */
 4    private static void AddNewIpadToExistingStudentUsingId()
 5    {
 6        var iPad = new IPad { Name = "Michelle's Ipad", StudentId = 2 };
 7        // use dbcontext add, as there are no DbSets defined for IPads yet
 8        dbContext.Add(iPad);
 9        dbContext.SaveChanges();
10    }
11    
12    /*
13     * What if you had the Student object in memory? That would be dead simple!
14    */
15    private static void AddNewIpadToExistingStudentInMemory()
16    {
17        var student = dbContext.Students.Find(2);
18        // associate an IPad record to this student
19        student.IPad = new IPad { Name = "Barrack's Ipad" };
20        dbContext.SaveChanges();
21    }
22    
23    /*
24     * What if you had the Student object in memory? But then this is a different 
25     * context? use the Attach Method to let EF know that this is an existing object that is being modified! EF Core will identify that student has an ID and IPad doesn't and generate the right query to insert just the Ipad record.
26    */
27    private static void AddNewIpadToExistingStudentInMemoryNewContext()
28    {
29        var student = dbContext.Students.AsNoTracking().FirstOrDefault(s => s.Id == 2);
30        // associate an IPad record to this student
31        student.IPad = new IPad { Name = "Barrack's Ipad" };
32        using(var newContextForDemo = new UniversityContext())
33        {
34            newContextForDemo.Attach(student);
35            newContextForDemo.SaveChanges();
36        }
37    }

Well, those were the most important bits for working with related data.

In the next post, we'll look at how we can work with Stored Procedures and Raw SQL.

comments powered by Disqus