/// <summary> /// Update building financials /// </summary> /// <param name="context"></param> /// <param name="building"></param> /// <param name="buildingEvaluations"></param> /// <param name="buildingFiscals"></param> public static void UpdateBuildingFinancials(this PimsContext context, Entity.Building building, ICollection <Entity.BuildingEvaluation> buildingEvaluations, ICollection <Entity.BuildingFiscal> buildingFiscals) { foreach (var buildingEvaluation in buildingEvaluations) { var existingEvaluation = building.Evaluations .FirstOrDefault(e => e.Date == buildingEvaluation.Date && e.Key == buildingEvaluation.Key); var updateEvaluation = existingEvaluation?.Value != buildingEvaluation.Value; if (existingEvaluation == null) { building.Evaluations.Add(buildingEvaluation); } else if (updateEvaluation) { context.Entry(existingEvaluation).CurrentValues.SetValues(buildingEvaluation); } } foreach (var buildingFiscal in buildingFiscals) { var originalBuildingFiscal = building.Fiscals .FirstOrDefault(e => e.FiscalYear == buildingFiscal.FiscalYear && e.Key == buildingFiscal.Key); var updateFiscal = originalBuildingFiscal?.Value != buildingFiscal.Value || originalBuildingFiscal?.EffectiveDate != buildingFiscal.EffectiveDate; if (originalBuildingFiscal == null) { building.Fiscals.Add(buildingFiscal); } else if (updateFiscal) { context.Entry(originalBuildingFiscal).CurrentValues.SetValues(buildingFiscal); } } }
/// <summary> /// Update a single child navigation property on a parent entity specified by T and parentId. /// Expects to be passed a complete list of child entities for the targeted navigation property. /// This method will update the database such that the navigation property for the parent contains the exact list of children passed to this method. /// </summary> /// <typeparam name="T">The parent entity type</typeparam> /// <typeparam name="I">The type of the id property</typeparam> /// <typeparam name="C">The type of the child navigation property being targeted for updates.</typeparam> /// <param name="context"></param> /// <param name="childNavigation"></param> /// <param name="parentId"></param> /// <param name="children"></param> public static void UpdateChild <T, I, C>(this PimsContext context, Expression <Func <T, object> > childNavigation, I parentId, C[] children) where T : IdentityBaseAppEntity <I> where C : IdentityBaseAppEntity <I> { var dbEntity = context.Find <T>(parentId); var dbEntry = context.Entry(dbEntity); var propertyName = childNavigation.GetPropertyAccess().Name; var dbItemsEntry = dbEntry.Collection(propertyName); var accessor = dbItemsEntry.Metadata.GetCollectionAccessor(); dbItemsEntry.Load(); var dbItemsMap = dbItemsEntry.CurrentValue.Cast <IdentityBaseAppEntity <I> >() .ToDictionary(e => e.Id); foreach (var item in children) { if (!dbItemsMap.TryGetValue(item.Id, out var oldItem)) { accessor.Add(dbEntity, item, false); } else { context.Entry(oldItem).CurrentValues.SetValues(item); dbItemsMap.Remove(item.Id); } } foreach (var oldItem in dbItemsMap.Values) { accessor.Remove(dbEntity, oldItem); context.Remove(oldItem); } }
/// <summary> /// Merge the specified 'updatedProject' changes into the specified 'originalProject'. /// </summary> /// <param name="originalProject"></param> /// <param name="updatedProject"></param> /// <param name="context"></param> public static void Merge(this Entity.Project originalProject, Entity.Project updatedProject, PimsContext context) { // Update a project var agency = originalProject.Agency; var metadata = originalProject.Metadata; context.Entry(originalProject).CurrentValues.SetValues(updatedProject); originalProject.Agency = agency; // TODO: this should not be necessary. originalProject.Metadata = metadata; context.SetOriginalRowVersion(originalProject); var agencies = originalProject.Agency.ParentId.HasValue ? new[] { originalProject.AgencyId } : context.Agencies.Where(a => a.ParentId == originalProject.AgencyId || a.Id == originalProject.AgencyId).Select(a => a.Id).ToArray(); // Update all properties foreach (var property in updatedProject.Properties) { var existingProperty = originalProject.Properties .FirstOrDefault(b => b.PropertyType == Entity.PropertyTypes.Land && b.ParcelId == property.ParcelId && b.ProjectId == updatedProject.Id || b.PropertyType == Entity.PropertyTypes.Building && b.ProjectId == updatedProject.Id && b.BuildingId == property.BuildingId); if (existingProperty == null) { //Todo: Navigation properties on project object were causing concurrency exceptions. var eproperty = property.PropertyType == Entity.PropertyTypes.Land ? context.Parcels.Find(property.ParcelId) : context.Buildings.Find(property.BuildingId) as Entity.Property; // Ignore properties that don't exist. if (eproperty != null) { if (property.PropertyType == Entity.PropertyTypes.Land) { var existingParcel = context.Parcels .Include(p => p.Agency) .Include(p => p.Evaluations) .Include(p => p.Fiscals) .FirstOrDefault(p => p.Id == property.ParcelId); existingParcel.ThrowIfPropertyNotInProjectAgency(agencies); if (existingParcel.ProjectNumber != null) { throw new InvalidOperationException("Parcels in a Project cannot be added to another Project."); } existingParcel.ProjectNumber = updatedProject.ProjectNumber; } else { var existingBuilding = context.Buildings .Include(b => b.Agency) .Include(p => p.Evaluations) .Include(p => p.Fiscals) .FirstOrDefault(p => p.Id == property.BuildingId); existingBuilding.ThrowIfPropertyNotInProjectAgency(agencies); if (existingBuilding.ProjectNumber != null) { throw new InvalidOperationException("Buildings in a Project cannot be added to another Project."); } existingBuilding.ProjectNumber = updatedProject.ProjectNumber; } originalProject.AddProperty(eproperty); } } else { if (property.PropertyType == Entity.PropertyTypes.Land) { // Only allow editing the classification and evaluations/fiscals for now existingProperty.Parcel.ProjectNumber = updatedProject.ProjectNumber; if (property.Parcel != null) { context.Entry(existingProperty.Parcel).Collection(p => p.Evaluations).Load(); existingProperty.Parcel.ClassificationId = property.Parcel.ClassificationId; existingProperty.Parcel.Zoning = property.Parcel.Zoning; existingProperty.Parcel.ZoningPotential = property.Parcel.ZoningPotential; foreach (var evaluation in property.Parcel.Evaluations) { var existingEvaluation = existingProperty.Parcel.Evaluations .FirstOrDefault(e => e.Date == evaluation.Date && e.Key == evaluation.Key); if (existingEvaluation == null) { existingProperty.Parcel.Evaluations.Add(evaluation); } else { context.Entry(existingEvaluation).CurrentValues.SetValues(evaluation); } } existingProperty.Parcel.RemoveEvaluationsWithinOneYear(property.Parcel, originalProject.DisposedOn); context.Entry(existingProperty.Parcel).Collection(p => p.Fiscals).Load(); foreach (var fiscal in property.Parcel.Fiscals) { var existingFiscal = existingProperty.Parcel.Fiscals .FirstOrDefault(e => e.FiscalYear == fiscal.FiscalYear && e.Key == fiscal.Key); if (existingFiscal == null) { existingProperty.Parcel.Fiscals.Add(fiscal); } else { context.Entry(existingFiscal).CurrentValues.SetValues(fiscal); } } } } else if (property.PropertyType == Entity.PropertyTypes.Building) { // Only allow editing the classification and evaluations/fiscals for now context.Entry(existingProperty.Building).Collection(p => p.Evaluations).Load(); existingProperty.Building.ProjectNumber = updatedProject.ProjectNumber; if (property.Building != null) { existingProperty.Building.ClassificationId = property.Building.ClassificationId; foreach (var evaluation in property.Building.Evaluations) { var existingEvaluation = existingProperty.Building.Evaluations .FirstOrDefault(e => e.Date == evaluation.Date && e.Key == evaluation.Key); if (existingEvaluation == null) { existingProperty.Building.Evaluations.Add(evaluation); } else { context.Entry(existingEvaluation).CurrentValues.SetValues(evaluation); } } existingProperty.Building.RemoveEvaluationsWithinOneYear(property.Building, originalProject.DisposedOn); context.Entry(existingProperty.Building).Collection(p => p.Fiscals).Load(); foreach (var fiscal in property.Building.Fiscals) { var existingFiscal = existingProperty.Building.Fiscals .FirstOrDefault(e => e.FiscalYear == fiscal.FiscalYear && e.Key == fiscal.Key); if (existingFiscal == null) { existingProperty.Building.Fiscals.Add(fiscal); } else { context.Entry(existingFiscal).CurrentValues.SetValues(fiscal); } } } } } } // Remove any properties from this project that are no longer associated. var removePropertyIds = originalProject.Properties.Where(p => p.Id != 0).Select(p => p.Id).Except(updatedProject.Properties.Where(p => p.Id != 0).Select(p => p.Id)); var removeProperties = originalProject.Properties.Where(p => removePropertyIds.Contains(p.Id)); var removeParcelIds = removeProperties.Where(p => p.ParcelId.HasValue).Select(p => p.ParcelId.Value).ToArray(); var removeParcels = context.Parcels.Where(p => removeParcelIds.Contains(p.Id)); removeParcels.ForEach(p => { p.ProjectNumber = null; context.Parcels.Update(p); }); var removeBuildingIds = removeProperties.Where(b => b.BuildingId.HasValue).Select(p => p.BuildingId.Value).ToArray(); var removeBuildings = context.Buildings.Where(p => removeBuildingIds.Contains(p.Id)); removeBuildings.ForEach(b => { b.ProjectNumber = null; context.Buildings.Update(b); }); originalProject.Properties.RemoveAll(p => removePropertyIds.Contains(p.Id)); // Update tasks foreach (var task in updatedProject.Tasks) { var originalProjectTask = originalProject.Tasks.FirstOrDefault(t => t.TaskId == task.TaskId); if (originalProjectTask == null) { originalProject.Tasks.Add(task); } else { context.Entry(originalProjectTask).CurrentValues.SetValues(task); } } // Update responses foreach (var response in updatedProject.Responses) { var originalProjectResponse = originalProject.Responses.FirstOrDefault(r => r.AgencyId == response.AgencyId); if (originalProjectResponse == null) { originalProject.Responses.Add(response); } else { context.Entry(originalProjectResponse).CurrentValues.SetValues(response); } } // Update notes foreach (var note in updatedProject.Notes) { var originalNote = originalProject.Notes.FirstOrDefault(r => r.Id == note.Id && note.Id > 0); if (originalNote == null) { originalProject.Notes.Add(note); } else { context.Entry(originalNote).CurrentValues.SetValues(note); } } var toStatus = context.ProjectStatus .Include(s => s.Tasks) .FirstOrDefault(s => s.Id == updatedProject.StatusId); updatedProject.Status = toStatus; // If the tasks haven't been specified, generate them. var taskIds = updatedProject.Tasks.Select(t => t.TaskId).ToArray(); // Add the tasks for project status if they are not already added. foreach (var task in toStatus.Tasks.Where(t => !taskIds.Contains(t.Id))) { originalProject.Tasks.Add(new Entity.ProjectTask(updatedProject, task)); } // Update project financials if the project is still active. if (!updatedProject.IsProjectClosed()) { originalProject.UpdateProjectFinancials(); } }
/// <summary> /// Update a single grandchild navigation property on a parent entity specified by T and parentId. /// Expects to be passed a complete list of child and grandchild entities for the targeted navigation property. /// This method will update the database such that the navigation property for the parent contains the exact list of children and grandchildren passed to this method. /// </summary> /// <typeparam name="T">The parent entity type</typeparam> /// <typeparam name="I">The type of the id property</typeparam> /// <typeparam name="C">The type of the child navigation property being targeted for updates.</typeparam> /// <param name="context"></param> /// <param name="childNavigation"></param> /// <param name="grandchildNavigation"></param> /// <param name="parentId"></param> /// <param name="childrenWithGrandchildren">The collection of children (and grandchildren) to update. Assumes grandchildren can be accessed via grandchildNavigation</param> /// <param name="canDeleteGrandchild">A callback to determine whether is safe to remove a grandchild entity</param> public static void UpdateGrandchild <T, I, C>( this PimsContext context, Expression <Func <T, object> > childNavigation, Expression <Func <C, object> > grandchildNavigation, I parentId, C[] childrenWithGrandchildren, Func <PimsContext, C, bool> canDeleteGrandchild) where T : IdentityBaseAppEntity <I> where C : IdentityBaseAppEntity <I> { var dbEntity = context.Find <T>(parentId); var dbEntry = context.Entry(dbEntity); var childPropertyName = childNavigation.GetPropertyAccess().Name; var childCollection = dbEntry.Collection(childPropertyName); var childAccessor = childCollection.Metadata.GetCollectionAccessor(); childCollection.Load(); var existingChildren = childCollection.CurrentValue.Cast <IdentityBaseAppEntity <I> >().ToDictionary(e => e.Id); // Compile the grandchildNavigation lambda expression so we can extract the value from the passed in array of children var grandchildPropertyName = grandchildNavigation.GetPropertyAccess().Name; var grandchildFunc = grandchildNavigation.Compile(); foreach (var child in childrenWithGrandchildren) { if (!existingChildren.TryGetValue(child.Id, out var existingChild)) { childAccessor.Add(dbEntity, child, false); } else { var dbChildEntry = context.Entry(existingChild); dbChildEntry.CurrentValues.SetValues(child); // load grandchild navigation property var grandchildReference = dbChildEntry.Reference(grandchildPropertyName); grandchildReference.Load(); // Update grandchild navigation with values passed in the array var grandchildValue = grandchildFunc(child); grandchildReference.TargetEntry.CurrentValues.SetValues(grandchildValue); existingChildren.Remove(child.Id); } } foreach (var existingChild in existingChildren.Values) { var dbChildEntry = context.Entry(existingChild); childAccessor.Remove(dbEntity, existingChild); context.Remove(existingChild); // Also remove the grandchild referenced by the child being removed if (canDeleteGrandchild(context, existingChild as C)) { // load grandchild navigation property var grandchildReference = dbChildEntry.Reference(grandchildPropertyName); grandchildReference.Load(); var dbGrandchild = grandchildReference.TargetEntry?.Entity; if (dbGrandchild != null) { context.Remove(dbGrandchild); } } } }