private async Task SyncSubject(MyPurdueSubject subject) { Console.WriteLine("\tSynchronizing " + subject.SubjectCode + " / " + subject.SubjectName + " ..."); var dbSubjectId = await EnsureSubject(subject.SubjectCode, subject.SubjectName); // Prepare a cache of all courses Dictionary <Tuple <string, string>, Course> dbCourseCache; using (var db = new ApplicationDbContext()) { dbCourseCache = db.Courses.AsNoTracking().Where(c => c.SubjectId == dbSubjectId) .ToDictionary(c => new Tuple <string, string>(c.Number, c.Title)); } Console.WriteLine("\t\tCached " + dbCourseCache.Count + " courses from database."); // Prepare a cache of all sections Dictionary <string, Section> dbSectionCache; using (var db = new ApplicationDbContext()) { db.Configuration.ProxyCreationEnabled = false; dbSectionCache = db.Sections.AsNoTracking().Include(s => s.Class).Include(s => s.Meetings.Select(m => m.Room.Building)) .Where(s => s.Class.TermId == TermId && s.Class.Course.SubjectId == dbSubjectId) .ToDictionary(s => s.CRN); } Console.WriteLine("\t\tCached " + dbSectionCache.Count + " sections from database."); // Fetch sections from MyPurdue Dictionary <string, MyPurdueSection> sectionsByCrn = null; sectionsByCrn = await Api.FetchSections(TermCode, subject.SubjectCode); Console.WriteLine("\t\tFetched " + sectionsByCrn.Count + " sections from MyPurdue."); // Group sections into classes by matching links var sectionFlatList = new List <MyPurdueSection>(sectionsByCrn.Values); var classGroups = GroupSections(sectionFlatList); // Process and insert each class foreach (var classGroup in classGroups) { // All of the foreign keys we need to find in order to hook sections up Guid?dbCampusId = null; Guid?dbCourseId = null; Guid?dbClassId = null; // Find the section that'll tell us course number and title (all of them should, but to be safe...) var sectionWithCourseInfo = classGroup.FirstOrDefault(cg => cg.Number.Length > 0 && cg.Title.Length > 0); if (sectionWithCourseInfo == null) { Console.WriteLine("\t\t** WARNING: No course information (title / number) found for CRNs " + string.Join(", ", classGroup.Select(c => c.Crn))); continue; } Console.WriteLine("\t\t" + subject.SubjectCode + sectionWithCourseInfo.Number + " - " + sectionWithCourseInfo.Title + " ..."); // Do we have any of the sections cached? var cachedSection = classGroup.Where(s => dbSectionCache.ContainsKey(s.Crn)) .Select(s => dbSectionCache[s.Crn]).FirstOrDefault(); if (cachedSection != null) { dbCampusId = cachedSection.Class.CampusId; dbCourseId = cachedSection.Class.CourseId; dbClassId = cachedSection.ClassId; } // Find or create any elements we don't have ... if (dbCampusId == null) { // Find / create the campus var sectionWithCampus = classGroup.FirstOrDefault(cg => cg.CampusCode.Length > 0); if (sectionWithCampus == null) { dbCampusId = await EnsureCampus("", ""); // No campus } else { dbCampusId = await EnsureCampus(sectionWithCampus.CampusCode, sectionWithCampus.CampusName); } } if (dbCourseId == null) { // Find / create the course var courseKey = new Tuple <string, string>(sectionWithCourseInfo.Number, sectionWithCourseInfo.Title); if (dbCourseCache.ContainsKey(courseKey)) { dbCourseId = dbCourseCache[courseKey].CourseId; } else { using (var db = new ApplicationDbContext()) { var dbCourse = new Course() { CourseId = Guid.NewGuid(), SubjectId = dbSubjectId, Title = sectionWithCourseInfo.Title, Number = sectionWithCourseInfo.Number, Description = sectionWithCourseInfo.Description, CreditHours = classGroup.OrderByDescending(c => c.CreditHours).FirstOrDefault().CreditHours, Classes = new List <Class>() }; db.Courses.Add(dbCourse); await db.SaveChangesAsync(); dbCourseCache[courseKey] = dbCourse; dbCourseId = dbCourse.CourseId; } } } if (dbClassId == null) { // Find / create the class using (var db = new ApplicationDbContext()) { var dbClass = new Class() { ClassId = Guid.NewGuid(), CampusId = (Guid)dbCampusId, CourseId = (Guid)dbCourseId, TermId = TermId, Sections = new List <Section>() }; db.Classes.Add(dbClass); await db.SaveChangesAsync(); dbClassId = dbClass.ClassId; } } // Update / create each section var updatedSections = new List <Section>(); var newSections = new List <Section>(); var instructorEntities = new Dictionary <Guid, Instructor>(); var newMeetings = new List <Meeting>(); var deleteMeetings = new List <Meeting>(); foreach (var section in classGroup) { Section dbSection; if (dbSectionCache.ContainsKey(section.Crn)) { dbSection = dbSectionCache[section.Crn]; var sectionChanged = false; // Check for changes if (dbSection.ClassId != dbClassId || dbSection.Type != section.Type || dbSection.Capacity != section.Capacity || dbSection.Enrolled != section.Enrolled || dbSection.RemainingSpace != section.RemainingSpace || dbSection.WaitlistCapacity != section.WaitlistCapacity || dbSection.WaitlistCount != section.WaitlistCount || dbSection.WaitlistSpace != section.WaitlistSpace) { dbSection.ClassId = (Guid)dbClassId; dbSection.Type = section.Type; dbSection.Capacity = section.Capacity; dbSection.Enrolled = section.Enrolled; dbSection.RemainingSpace = section.RemainingSpace; dbSection.WaitlistCapacity = section.WaitlistCapacity; dbSection.WaitlistCount = section.WaitlistCount; dbSection.WaitlistSpace = section.WaitlistSpace; sectionChanged = true; } // Update meetings // First, delete any meetings that don't exist in the latest MyPurdue pull foreach (var meeting in dbSection.Meetings.ToList()) { // TODO: compare instructors var matches = section.Meetings.Where(m => m.Type == meeting.Type && m.StartTime == meeting.StartTime && m.EndTime == meeting.StartTime.Add(meeting.Duration) && m.DaysOfWeek == meeting.DaysOfWeek && m.BuildingCode == meeting.Room.Building.ShortCode && m.RoomNumber == meeting.Room.Number ); if (matches.Count() <= 0) { dbSection.Meetings.Remove(meeting); deleteMeetings.Add(meeting); sectionChanged = true; } } // Add all of the meetings that don't exist. var existingMeetings = dbSection.Meetings.ToList(); // copy of list to avoid changes during loop foreach (var meeting in section.Meetings) { var matches = existingMeetings.Where(m => m.Type == meeting.Type && m.StartTime == meeting.StartTime && m.Duration == meeting.EndTime.Subtract(meeting.StartTime) && m.DaysOfWeek == meeting.DaysOfWeek && m.Room.Building.ShortCode == meeting.BuildingCode && m.Room.Number == meeting.RoomNumber ); if (matches.Count() <= 0) { // make sure instructors exist var instructors = new List <Instructor>(); foreach (var inst in meeting.Instructors) { var instructorId = await EnsureInstructor(inst.Item2, inst.Item1); if (!instructorEntities.ContainsKey(instructorId)) { instructorEntities[instructorId] = new Instructor() { InstructorId = instructorId }; } instructors.Add(instructorEntities[instructorId]); } // make sure the building exists var dbBuildingId = await EnsureBuilding((Guid)dbCampusId, meeting.BuildingCode, meeting.BuildingName); // make sure the room exists var dbRoomId = await EnsureRoom(dbBuildingId, meeting.RoomNumber); // Create the actual meeting object var newMeeting = new Meeting() { MeetingId = Guid.NewGuid(), Type = meeting.Type, StartTime = meeting.StartTime, Duration = meeting.EndTime.Subtract(meeting.StartTime), Instructors = instructors, RoomId = dbRoomId, DaysOfWeek = meeting.DaysOfWeek, SectionId = dbSection.SectionId, StartDate = meeting.StartDate, EndDate = meeting.EndDate }; dbSection.Meetings.Add(newMeeting); newMeetings.Add(newMeeting); sectionChanged = true; } } // If we've made any changes, flag this for committing to the DB later if (sectionChanged) { foreach (var m in dbSection.Meetings.ToList()) { m.Room = null; // Prevent EF from trying to attach the entity } dbSection.Class = null; // Prevent EF from trying to find the entity if (section.Meetings.Count > 0) { var startDate = section.Meetings.OrderBy(m => m.StartDate).Select(m => m.StartDate).First(); var endDate = section.Meetings.OrderByDescending(m => m.EndDate).Select(m => m.EndDate).First(); dbSection.StartDate = startDate; dbSection.EndDate = endDate; } updatedSections.Add(dbSection); } } else { // Section isn't cached. Create a new one. dbSection = new Section() { SectionId = Guid.NewGuid(), CRN = section.Crn, ClassId = (Guid)dbClassId, Meetings = new List <Meeting>(), Type = section.Type, Capacity = section.Capacity, Enrolled = section.Enrolled, RemainingSpace = section.RemainingSpace, WaitlistCapacity = section.WaitlistCapacity, WaitlistCount = section.WaitlistCount, WaitlistSpace = section.WaitlistSpace }; foreach (var meeting in section.Meetings) { // make sure instructors exist var instructors = new List <Instructor>(); foreach (var inst in meeting.Instructors) { var instructorId = await EnsureInstructor(inst.Item2, inst.Item1); if (!instructorEntities.ContainsKey(instructorId)) { instructorEntities[instructorId] = new Instructor() { InstructorId = instructorId }; } instructors.Add(instructorEntities[instructorId]); } // make sure the building exists var dbBuildingId = await EnsureBuilding((Guid)dbCampusId, meeting.BuildingCode, meeting.BuildingName); // make sure the room exists var dbRoomId = await EnsureRoom(dbBuildingId, meeting.RoomNumber); var newMeeting = new Meeting() { MeetingId = Guid.NewGuid(), Type = meeting.Type, StartTime = meeting.StartTime, Duration = meeting.EndTime.Subtract(meeting.StartTime), Instructors = instructors, RoomId = dbRoomId, DaysOfWeek = meeting.DaysOfWeek, Section = dbSection, StartDate = meeting.StartDate, EndDate = meeting.EndDate }; dbSection.Meetings.Add(newMeeting); // We don't need to add to newMeetings here because this is a new section // and EF will take care of adding new related entities. } if (section.Meetings.Count > 0) { var startDate = section.Meetings.OrderBy(m => m.StartDate).Select(m => m.StartDate).First(); var endDate = section.Meetings.OrderByDescending(m => m.EndDate).Select(m => m.EndDate).First(); dbSection.StartDate = startDate; dbSection.EndDate = endDate; } newSections.Add(dbSection); } } using (var db = new ApplicationDbContext()) { db.Configuration.ProxyCreationEnabled = false; Console.WriteLine("\t\t\t{0} inserted / {1} updated sections", newSections.Count, updatedSections.Count); // Attach all instructors as unchanged foreach (var i in instructorEntities.Values) { db.Instructors.Attach(i); db.Entry(i).State = EntityState.Unchanged; } foreach (var s in updatedSections) { db.Entry(s).State = EntityState.Modified; } foreach (var m in deleteMeetings) { //db.Meetings.Remove(db.Meetings.Find(m.MeetingId)); // Requires hitting DB twice. var mid = new Meeting() { MeetingId = m.MeetingId }; db.Meetings.Attach(mid); db.Meetings.Remove(mid); } foreach (var m in newMeetings) { db.Meetings.Add(m); } foreach (var s in newSections) { db.Sections.Add(s); } await db.SaveChangesAsync(); } } }