// Recursively get modified properties public static IEnumerable <IEnumerable <string> > GetModifiedProperties (this ITrackable item, ObjectVisitationHelper visitationHelper = null) { // Prevent endless recursion ObjectVisitationHelper.EnsureCreated(ref visitationHelper); if (!visitationHelper.TryVisit(item)) { yield break; } foreach (var navProp in item.GetNavigationProperties()) { foreach (var refProp in navProp.AsReferenceProperty()) { foreach (var modifiedProps in refProp.EntityReference.GetModifiedProperties(visitationHelper)) { yield return(modifiedProps); } } foreach (var colProp in navProp.AsCollectionProperty()) { foreach (ITrackable child in colProp.EntityCollection) { foreach (var modifiedProps in child.GetModifiedProperties(visitationHelper)) { yield return(modifiedProps); } } } } yield return(item.ModifiedProperties); }
// Recursively get entity states public static IEnumerable <EntityState> GetEntityStates(this DbContext context, ITrackable item, EntityState?entityState = null, ObjectVisitationHelper visitationHelper = null) { // Prevent endless recursion ObjectVisitationHelper.EnsureCreated(ref visitationHelper); if (!visitationHelper.TryVisit(item)) { yield break; } foreach (var colProp in item.GetNavigationProperties().OfCollectionType()) { foreach (ITrackable child in colProp.EntityCollection) { foreach (var state in context.GetEntityStates(child, visitationHelper: visitationHelper)) { if (entityState == null || state == entityState) { yield return(state); } } } } yield return(context.Entry(item).State); }
// Recursively set tracking state public static void SetTrackingState(this ITrackable item, TrackingState state, ObjectVisitationHelper visitationHelper = null) { // Prevent endless recursion ObjectVisitationHelper.EnsureCreated(ref visitationHelper); if (!visitationHelper.TryVisit(item)) { return; } foreach (var navProp in item.GetNavigationProperties()) { foreach (var refProp in navProp.AsReferenceProperty()) { refProp.EntityReference.SetTrackingState(state, visitationHelper); refProp.EntityReference.TrackingState = state; } foreach (var colProp in navProp.AsCollectionProperty()) { foreach (ITrackable child in colProp.EntityCollection) { child.SetTrackingState(state, visitationHelper); child.TrackingState = state; } } } }
/// <summary> /// Recursively enable or disable tracking on trackable entities in an object graph. /// </summary> /// <param name="item">Trackable object</param> /// <param name="enableTracking">Enable or disable change-tracking</param> /// <param name="visitationHelper">Circular reference checking helper</param> /// <param name="oneToManyOnly">True if tracking should be set only for OneToMany relations</param> public static void SetTracking(this ITrackable item, bool enableTracking, ObjectVisitationHelper visitationHelper = null, bool oneToManyOnly = false) { // Iterator entity properties foreach (var navProp in item.GetNavigationProperties()) { // Skip if 1-M only if (!oneToManyOnly) { // Set tracking on 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { // Get ref prop change tracker ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name); if (refChangeTracker != null) { // Set tracking on ref prop change tracker refChangeTracker.SetTracking(enableTracking, visitationHelper, oneToManyOnly); } } } // Set tracking on 1-M and M-M properties (if not 1-M only) foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>()) { bool isOneToMany = !IsManyToManyChildCollection(colProp.EntityCollection); if (!oneToManyOnly || isOneToMany) { colProp.EntityCollection.SetTracking(enableTracking, visitationHelper, oneToManyOnly); } } } }
public void Child_Set_Modified_Props_Should_Not_Set_Parent_Siblings() { // NOTE: SetModifiedProperties should only set ModifiedProperties on // downstream 1-M entities. // Arrange const string propName = "UnitPrice"; var nw = new MockNorthwind(); var order = nw.Orders[0]; var customer = order.Customer; var detail1 = order.OrderDetails[0]; var detail2 = order.OrderDetails[1]; var detail3 = order.OrderDetails[2]; detail1.Order = order; detail2.Order = order; detail3.Order = order; var visitationHelper = new ObjectVisitationHelper(null); visitationHelper.TryVisit(order.OrderDetails); // Act detail1.SetModifiedProperties(new List <string> { propName }, visitationHelper.Clone()); // Assert Assert.Null(order.ModifiedProperties); Assert.Null(customer.ModifiedProperties); Assert.Null(detail1.ModifiedProperties); Assert.Null(detail2.ModifiedProperties); Assert.Null(detail3.ModifiedProperties); }
/// <summary> /// Recursively enable or disable tracking on trackable entities in an object graph. /// </summary> /// <param name="item">Trackable object</param> /// <param name="enableTracking">Enable or disable change-tracking</param> /// <param name="visitationHelper">Circular reference checking helper</param> public static void SetTracking(this ITrackable item, bool enableTracking, ObjectVisitationHelper visitationHelper = null) { // Iterator entity properties foreach (var navProp in item.GetNavigationProperties()) { // Set tracking on 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { // Get ref prop change tracker ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name); if (refChangeTracker != null) { // Set tracking on ref prop change tracker refChangeTracker.SetTracking(enableTracking, visitationHelper); } } // Set tracking on 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>()) { colProp.EntityCollection.SetTracking(enableTracking, visitationHelper); } } }
/// <summary> /// Insert item at specified index. /// </summary> /// <param name="index">Zero-based index at which item should be inserted</param> /// <param name="item">Item to insert</param> protected override void InsertItem(int index, TEntity item) { if (Tracking) { // Set entity identifier if (item is IIdentifiable) { ((IIdentifiable)item).SetEntityIdentifier(); } // Listen for property changes item.PropertyChanged += OnPropertyChanged; // Exclude this collection and Parent entity (used in M-M relationships) // from recusive algorithms: SetTracking and SetState. var visitationHelper = new ObjectVisitationHelper(Parent); visitationHelper.TryVisit(this); // Enable tracking on trackable properties item.SetTracking(Tracking, visitationHelper.Clone()); // Mark item and trackable collection properties item.SetState(TrackingState.Added, visitationHelper.Clone()); // Fire EntityChanged event if (EntityChanged != null) { EntityChanged(this, EventArgs.Empty); } } base.InsertItem(index, item); }
/// <summary> /// Recursively remove items marked as deleted. /// </summary> /// <param name="changeTracker">Change-tracking collection</param> /// <param name="visitationHelper">Circular reference checking helper</param> public static void RemoveRestoredDeletes(this ITrackingCollection changeTracker, ObjectVisitationHelper visitationHelper = null) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Iterate items in change-tracking collection var items = changeTracker as IList; if (items == null) { return; } var count = items.Count; for (int i = count - 1; i > -1; i--) { // Get trackable item var item = items[i] as ITrackable; if (item == null) { continue; } // Prevent endless recursion if (visitationHelper.TryVisit(item)) { // Iterate entity properties foreach (var navProp in item.GetNavigationProperties()) { // Process 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { // Get changed ref prop ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name); // Remove deletes on rep prop if (refChangeTracker != null) { refChangeTracker.RemoveRestoredDeletes(visitationHelper); } } // Process 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>()) { colProp.EntityCollection.RemoveRestoredDeletes(visitationHelper); } } } // Remove item if marked as deleted if (item.TrackingState == TrackingState.Deleted) { var isTracking = changeTracker.Tracking; changeTracker.InternalTracking = false; items.RemoveAt(i); changeTracker.InternalTracking = isTracking; } } }
/// <summary> /// See if there are changes in an object graph. /// </summary> /// <param name="item">Trackable object</param> /// <returns>True if there are changes in the object graph</returns> public static bool HasChanges(this ITrackable item) { var visitationHelper = new ObjectVisitationHelper(); bool hasChanges = item.HasChanges(visitationHelper, new Dictionary <ITrackable, bool>(ObjectReferenceEqualityComparer <ITrackable> .Default)); return(hasChanges); }
private static void SetChanges(this DbContext context, ITrackable item, EntityState state, ObjectVisitationHelper visitationHelper, ITrackable parent = null, string propertyName = null) { // Set state for child collections foreach (var navProp in item.GetNavigationProperties()) { string propName = navProp.Property.Name; // Apply changes to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { ITrackable trackableReference = refProp.EntityReference; if (visitationHelper.IsVisited(trackableReference)) { continue; } context.ApplyChanges(trackableReference, item, visitationHelper, propName); if (context.IsRelatedProperty(item.GetType(), propName, RelationshipType.OneToOne)) { context.SetChanges(trackableReference, state, visitationHelper, item, propName); } } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { foreach (ITrackable trackableChild in colProp.EntityCollection.Reverse()) { // Prevent endless recursion if (visitationHelper.TryVisit(trackableChild)) { // TRICKY: we have just visited the item // As a side effect, ApplyChanges will never be called for it. context.SetChanges(trackableChild, state, visitationHelper, item, propName); } } } } // If M-M child, set relationship for deleted items if (state == EntityState.Deleted && parent != null && propertyName != null && (context.IsRelatedProperty(parent.GetType(), propertyName, RelationshipType.ManyToMany))) { context.Entry(item).State = item.TrackingState == TrackingState.Modified ? EntityState.Modified : EntityState.Unchanged; context.SetRelationshipState(item, parent, propertyName, EntityState.Deleted); return; } // Set entity state context.Entry(item).State = state; }
private static ObjectVisitationHelper CreateVisitationHelperWithIdMatching(DbContext dbContext) { var visitationHelper = new ObjectVisitationHelper(new IdMatcher() { DbContext = dbContext }); return(visitationHelper); }
/// <summary> /// Restore items marked as deleted. /// </summary> /// <param name="changeTracker">Change-tracking collection</param> /// <param name="visitationHelper">Circular reference checking helper</param> public static void RestoreDeletes(this ITrackingCollection changeTracker, ObjectVisitationHelper visitationHelper = null) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Get cached deletes var removedDeletes = changeTracker.GetChanges(true).Cast <ITrackable>().ToList(); // Restore deleted items if (removedDeletes.Any()) { var isTracking = changeTracker.Tracking; changeTracker.Tracking = false; foreach (var delete in removedDeletes) { var items = changeTracker as IList; if (items != null && !items.Contains(delete)) { items.Add(delete); } } changeTracker.Tracking = isTracking; } foreach (var item in changeTracker.Cast <ITrackable>()) { // Prevent endless recursion if (!visitationHelper.TryVisit(item)) { continue; } // Iterate entity properties foreach (var navProp in item.GetNavigationProperties()) { // Process 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { // Get changed ref prop ITrackingCollection refChangeTracker = item.GetRefPropertyChangeTracker(refProp.Property.Name); // Restore deletes on rep prop if (refChangeTracker != null) { refChangeTracker.RestoreDeletes(visitationHelper); } } // Process 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty <ITrackingCollection>()) { colProp.EntityCollection.RestoreDeletes(visitationHelper); } } } }
public void Reset(IValidatableEntity validatableEntity = null) { _objectVisitationHelper.Reset(); if (validatableEntity != null) { ObjectVisitationHelper.TryVisit(validatableEntity); } SetConfiguration(null); }
/// <summary> /// Recursively set tracking state on trackable entities in an object graph. /// </summary> /// <param name="item">Trackable object</param> /// <param name="state">Change-tracking state of an entity</param> /// <param name="visitationHelper">Circular reference checking helper</param> /// <param name="isManyToManyItem">True is an item is treated as part of an M-M collection</param> public static void SetState(this ITrackable item, TrackingState state, ObjectVisitationHelper visitationHelper, bool isManyToManyItem = false) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Prevent endless recursion if (!visitationHelper.TryVisit(item)) { return; } // Recurively set state for unchanged, added or deleted items, // but not for M-M child item if (!isManyToManyItem && state != TrackingState.Modified) { // Iterate entity properties foreach (var colProp in item.GetNavigationProperties().OfCollectionType <ITrackingCollection>()) { // Process 1-M and M-M properties // Set state on child entities bool isManyToManyChildCollection = IsManyToManyChildCollection(colProp.EntityCollection); foreach (ITrackable trackableChild in colProp.EntityCollection) { trackableChild.SetState(state, visitationHelper, isManyToManyChildCollection); } } } // Deleted items are treated a bit specially if (state == TrackingState.Deleted) { if (isManyToManyItem) { // With M-M properties there is no way to tell if the related entity should be deleted // or simply removed from the relationship, because it is an independent association. // Therefore, deleted children are marked unchanged. if (item.TrackingState != TrackingState.Modified) { item.TrackingState = TrackingState.Unchanged; } return; } // When deleting added item, set state to unchanged else if (item.TrackingState == TrackingState.Added) { item.TrackingState = TrackingState.Unchanged; return; } } item.TrackingState = state; }
public static string GetValidationResultsText(IValidatableEntity validatableEntity, IEnumerable <IValidationResult> customResults = null) { var returnValue = new StringBuilder(); var validationResultIndex = 0; customResults?.Concat(GetValidationResults(validatableEntity, ObjectVisitationHelper.CreateInstance())).Each(vr => { returnValue.AppendFormat(ValidationResultFormat, vr.Description, validationResultIndex + 1, Environment.NewLine); validationResultIndex++; }); return(returnValue.ToString()); }
/// <summary> /// For internal use. /// Turn change-tracking on and off with proper circular reference checking. /// </summary> public void SetTracking(bool value, ObjectVisitationHelper visitationHelper) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Prevent endless recursion if (!visitationHelper.TryVisit(this)) { return; } // Get notified when an item in the collection has changed foreach (TEntity item in this) { // Prevent endless recursion if (item == null) { continue; } if (!visitationHelper.TryVisit(item)) { continue; } // Property change notification if (value) { item.PropertyChanged += OnPropertyChanged; } else { item.PropertyChanged -= OnPropertyChanged; } // Enable tracking on trackable collection properties item.SetTracking(value, visitationHelper); try { if (item is IIdentifiable) { ((IIdentifiable)item).SetEntityIdentifier(); } } catch (Exception ex) { // to catch stackoverflow } // Set entity identifier } _tracking = value; }
/// <summary> /// Remove item at specified index. /// </summary> /// <param name="index">Zero-based index at which item should be removed</param> protected override void RemoveItem(int index) { // Mark existing item as deleted, stop listening for property changes, // then fire EntityChanged event, and cache item. if (Tracking) { // Get item by index TEntity item = Items[index]; // Exclude this collection and Parent entity (used in M-M relationships) // from recusive algorithms: SetModifiedProperties, SetTracking and SetState. var visitationHelper = new ObjectVisitationHelper(Parent); visitationHelper.TryVisit(this); // Remove modified properties item.ModifiedProperties = null; item.SetModifiedProperties(null, visitationHelper.Clone()); // Stop listening for property changes item.PropertyChanged -= OnPropertyChanged; // Disable tracking on trackable properties item.SetTracking(false, visitationHelper.Clone(), true); // Mark item and trackable collection properties bool manyToManyAdded = Parent != null && item.TrackingState == TrackingState.Added; item.SetState(TrackingState.Deleted, visitationHelper.Clone()); // Fire EntityChanged event if (EntityChanged != null) { EntityChanged(this, EventArgs.Empty); } // Cache deleted item if not added or already cached if (item.TrackingState != TrackingState.Added && !manyToManyAdded && !_deletedEntities.Contains(item)) { _deletedEntities.Add(item); } } base.RemoveItem(index); }
private static void LoadRelatedEntitiesOnProperties(this DbContext context, ITrackable item, ObjectVisitationHelper visitationHelper, bool loadAll) { // Recursively load related entities foreach (var navProp in item.GetNavigationProperties()) { // Apply changes to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { context.LoadRelatedEntities(new[] { refProp.EntityReference }, item, visitationHelper, loadAll); } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { context.LoadRelatedEntities(colProp.EntityCollection, item, visitationHelper, loadAll); } } }
/// <summary> /// Recursively set tracking state on trackable properties in an object graph. /// </summary> /// <param name="item">Trackable object</param> /// <param name="modified">Properties on an entity that have been modified</param> /// <param name="visitationHelper">Circular reference checking helper</param> public static void SetModifiedProperties(this ITrackable item, ICollection <string> modified, ObjectVisitationHelper visitationHelper = null) { // Prevent endless recursion ObjectVisitationHelper.EnsureCreated(ref visitationHelper); if (!visitationHelper.TryVisit(item)) { return; } // Iterate entity properties foreach (var colProp in item.GetNavigationProperties().OfCollectionType <ITrackingCollection>()) { // Recursively set modified foreach (ITrackable child in colProp.EntityCollection) { child.SetModifiedProperties(modified, visitationHelper); child.ModifiedProperties = modified; } } }
// This method assumes entity and contract have same shape protected void SetContractFields(object entity, object contract, ObjectVisitationHelper visitationHelper) { var isEntitySingular = typeof(IBedrockEntity).GetTypeInfo().IsAssignableFrom(entity.GetType()); var isEntityCollection = typeof(IEnumerable <IBedrockEntity>).GetTypeInfo().IsAssignableFrom(entity.GetType()); var isContractSingular = typeof(IBedrockEntity).GetTypeInfo().IsAssignableFrom(contract.GetType()); var isContractCollection = typeof(IEnumerable <IBedrockEntity>).GetTypeInfo().IsAssignableFrom(contract.GetType()); if (isEntitySingular && isContractSingular) { SetContractFieldsInternal(entity, contract, visitationHelper); } else if (isEntityCollection && isContractCollection) { var entities = entity as IEnumerable <IBedrockEntity>; var contracts = contract as IEnumerable <IBedrockEntity>; var contractList = new List <IBedrockEntity>(contracts); entities.Each((e, i) => SetContractFieldsInternal(e, contractList[i], visitationHelper)); } }
/// <summary> /// For internal use. /// Turn change-tracking on and off with proper circular reference checking. /// </summary> public void SetTracking(bool value, ObjectVisitationHelper visitationHelper, bool oneToManyOnly, EventHandler entityChanged = null) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Prevent endless recursion if (!visitationHelper.TryVisit(this)) { return; } // Get notified when an item in the collection has changed foreach (TEntity item in this) { // Prevent endless recursion if (!visitationHelper.TryVisit(item)) { continue; } // Property change notification if (value) { item.PropertyChanged += OnPropertyChanged; } else { item.PropertyChanged -= OnPropertyChanged; } // Enable tracking on trackable collection properties item.SetTracking(value, visitationHelper, oneToManyOnly, entityChanged); // Set entity identifier if (item is IIdentifiable) { ((IIdentifiable)item).SetEntityIdentifier(); } } _tracking = value; }
// Recursively get tracking states public static IEnumerable <TrackingState> GetTrackingStates (this ITrackable item, TrackingState?trackingState = null, ObjectVisitationHelper visitationHelper = null) { // Prevent endless recursion ObjectVisitationHelper.EnsureCreated(ref visitationHelper); if (!visitationHelper.TryVisit(item)) { yield break; } foreach (var navProp in item.GetNavigationProperties()) { foreach (var refProp in navProp.AsReferenceProperty()) { foreach (var state in refProp.EntityReference.GetTrackingStates(visitationHelper: visitationHelper)) { if (trackingState == null || state == trackingState) { yield return(state); } } } foreach (var colProp in navProp.AsCollectionProperty()) { foreach (ITrackable child in colProp.EntityCollection) { foreach (var state in child.GetTrackingStates(visitationHelper: visitationHelper)) { if (trackingState == null || state == trackingState) { yield return(state); } } } } } yield return(item.TrackingState); }
private static void ApplyChangesOnProperties(this DbContext context, ITrackable item, ObjectVisitationHelper visitationHelper, TrackingState?state = null) { // Recursively apply changes foreach (var navProp in item.GetNavigationProperties()) { // Apply changes to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { context.ApplyChanges(refProp.EntityReference, item, visitationHelper, navProp.Property.Name, state); } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty <IList>()) { // Apply changes on collection property for each state. // Process added items first, then process others. ApplyChangesOnCollectionProperties(TrackingState.Added, true, navProp, colProp, context, item, visitationHelper, state); ApplyChangesOnCollectionProperties(TrackingState.Added, false, navProp, colProp, context, item, visitationHelper, state); } } }
private static void MergeChanges(this ITrackingCollection originalChangeTracker, IEnumerable <ITrackable> updatedItems, ObjectVisitationHelper visitationHelper, bool isTrackableRef = false) { ObjectVisitationHelper.EnsureCreated(ref visitationHelper); // Process each updated item foreach (var updatedItem in updatedItems) { // Prevent endless recursion if (!visitationHelper.TryVisit(updatedItem)) { continue; } // Get matching orig item var origItem = originalChangeTracker.Cast <ITrackable>() .GetEquatableItem(updatedItem, isTrackableRef); if (origItem == null) { continue; } // Back fill entity identity on trackable ref if (isTrackableRef) { var origItemIdentifiable = (IIdentifiable)origItem; origItemIdentifiable.SetEntityIdentifier(); ((IIdentifiable)updatedItem).SetEntityIdentifier(origItemIdentifiable); } // Iterate entity properties foreach (var navProp in updatedItem.GetNavigationProperties()) { // Set to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { ITrackingCollection refPropChangeTracker = origItem.GetRefPropertyChangeTracker(navProp.Property.Name); if (refPropChangeTracker != null) { refPropChangeTracker.MergeChanges(new[] { refProp.EntityReference }, visitationHelper, true); } } // Set 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { var origItemsChangeTracker = origItem.GetEntityCollectionProperty <ITrackingCollection>(navProp.Property).EntityCollection; if (origItemsChangeTracker != null) { // Merge changes into trackable children origItemsChangeTracker.MergeChanges(colProp.EntityCollection, visitationHelper); } } } // Set properties on orig item origItem.SetEntityProperties(updatedItem, originalChangeTracker); // Accept changes origItem.AcceptChanges(); } // Remove cached deletes originalChangeTracker.RemoveCachedDeletes(); }
public static IValidationResult[] GetValidationResults(IValidatableEntity validatableEntity, ObjectVisitationHelper visitationHelper) { var type = validatableEntity.GetType(); var nonValidated = SharedUtility.ApplicationContext.DomainGraphCache.GetPropertiesAttributes <NonValidated>(type); var brokenRules = new List <IValidationResult>(validatableEntity.FieldValidationManager.Results); var children = new List <PropertyInfo>(SharedUtility.ApplicationContext.DomainGraphCache.GetPropertiesOfType <IValidatableEntity>(type).Where(x => !nonValidated.ContainsKey(x))); children.AddRange(new List <PropertyInfo>(SharedUtility.ApplicationContext.DomainGraphCache.GetPropertiesOfType <IEnumerable <IValidatableEntity> >(type).Where(x => !nonValidated.ContainsKey(x)))); visitationHelper.TryVisit(validatableEntity); foreach (var child in children) { var childValue = child.GetValue(validatableEntity, null); if (childValue is IEnumerable <IValidatableEntity> ) { var values = child.GetValue(validatableEntity, null) as IEnumerable <IValidatableEntity>; foreach (var value in values) { if (value != null) { if (!visitationHelper.TryVisit(value)) { continue; } brokenRules.AddRange(GetValidationResults(value, visitationHelper)); } } } else { var value = childValue as IValidatableEntity; if (value != null) { if (!visitationHelper.TryVisit(value)) { continue; } brokenRules.AddRange(GetValidationResults(value, visitationHelper)); } } } return(brokenRules.ToArray()); }
private static async Task LoadRelatedEntitiesAsync(this DbContext context, IEnumerable <ITrackable> items, ITrackable parent, ObjectVisitationHelper visitationHelper, CancellationToken cancellationToken, bool loadAll) { // Return if no items if (items == null) { return; } // Get selected items var selectedItems = loadAll ? items : items.Where(t => t.TrackingState == TrackingState.Added || (parent != null && parent.TrackingState == TrackingState.Added)); var entities = selectedItems.Cast <object>().Where(i => !visitationHelper.IsVisited(i)); // Collection 'items' can contain entities of different types (due to inheritance) // We collect a superset of all properties of all items of type ITrackable var allProps = (from entity in entities from prop in entity.GetType().GetProperties() where typeof(ITrackable).IsAssignableFrom(prop.PropertyType) select prop).Distinct(); // Populate related entities on all items foreach (var prop in allProps) { // Get related entities string propertyName = prop.Name; Type propertyType = prop.PropertyType; IEnumerable <object> relatedEntities = await context.GetRelatedEntitiesAsync(entities, prop.DeclaringType, propertyName, propertyType, cancellationToken).ConfigureAwait(false); // Continue if there are no related entities if (!relatedEntities.Any()) { continue; } // ObjectVisitationHelper serves here as an identity cache relatedEntities = relatedEntities.Select(e => visitationHelper.FindVisited(e) ?? e); // Set related entities context.SetRelatedEntities(entities, relatedEntities, prop, prop.DeclaringType, propertyName, propertyType); } // Recursively populate related entities on ref and child properties foreach (var item in items) { // Avoid endless recursion if (!visitationHelper.TryVisit(item)) { continue; } bool loadAllRelated = loadAll || item.TrackingState == TrackingState.Added || (parent != null && parent.TrackingState == TrackingState.Added); await context.LoadRelatedEntitiesOnPropertiesAsync(item, visitationHelper, cancellationToken, loadAllRelated).ConfigureAwait(false); } }
private static void ApplyChanges(this DbContext context, ITrackable item, ITrackable parent, ObjectVisitationHelper visitationHelper, string propertyName, TrackingState?state = null) { // Prevent endless recursion // if (!Exists(context, item)) return; if (!visitationHelper.TryVisit(item)) { return; } // Check for null args if (context == null) { throw new ArgumentNullException("context"); } if (item == null) { throw new ArgumentNullException("item"); } // If M-M child, set relationship for added or deleted items if (parent != null && propertyName != null && (context.IsRelatedProperty(parent.GetType(), propertyName, RelationshipType.ManyToMany))) { // If parent is added set tracking state to match var trackingState = item.TrackingState; if (parent.TrackingState == TrackingState.Added) { trackingState = parent.TrackingState; } // If tracking state is added set entity to unchanged, // then add or delete relation. if (trackingState == TrackingState.Added || trackingState == TrackingState.Deleted) { context.Entry(item).State = item.TrackingState == TrackingState.Modified ? EntityState.Modified : EntityState.Unchanged; context.SetRelationshipState(item, parent, propertyName, trackingState.ToEntityState()); } else { // Set entity state for modified or unchanged if (item.TrackingState == TrackingState.Modified) { context.Entry(item).State = EntityState.Modified; } else if (item.TrackingState == TrackingState.Unchanged) { context.Entry(item).State = EntityState.Unchanged; } } // Set state for child collections context.ApplyChangesOnProperties(item, visitationHelper); return; } // Exit if parent is added or deleted, // and it's not a M-1 relation if (parent != null && (parent.TrackingState == TrackingState.Added || parent.TrackingState == TrackingState.Deleted) && !context.IsRelatedProperty(parent.GetType(), propertyName, RelationshipType.ManyToOne)) { context.ApplyChangesOnProperties(item, visitationHelper); return; } // If it is a M-1 relation and item state is deleted, // set to unchanged and exit if (parent != null && (context.IsRelatedProperty(parent.GetType(), propertyName, RelationshipType.ManyToOne) && item.TrackingState == TrackingState.Deleted)) { try { context.Entry(item).State = EntityState.Unchanged; } catch (InvalidOperationException invalidOpEx) { throw new InvalidOperationException(Constants.ExceptionMessages.DeletedWithAddedChildren, invalidOpEx); } return; } // Set state to Added on parent only if (item.TrackingState == TrackingState.Added && (state == null || state == TrackingState.Added)) { context.Entry(item).State = EntityState.Added; context.ApplyChangesOnProperties(item, visitationHelper); return; } // Set state to Deleted on children and parent if (item.TrackingState == TrackingState.Deleted && (state == null || state == TrackingState.Deleted)) { context.SetChanges(item, EntityState.Unchanged, visitationHelper.Clone()); // Clone to avoid interference context.SetChanges(item, EntityState.Deleted, visitationHelper); return; } // Set entity state if (state == null || state == TrackingState.Unchanged || state == TrackingState.Modified || (state == TrackingState.Added && item.TrackingState != TrackingState.Deleted)) { // Set added state for reference or child properties context.ApplyChangesOnProperties(item, visitationHelper.Clone(), TrackingState.Added); // Clone to avoid interference // Set modified properties if (item.TrackingState == TrackingState.Modified && (state == null || state == TrackingState.Modified) && item.ModifiedProperties != null && item.ModifiedProperties.Count > 0) { // Mark modified properties context.Entry(item).State = item.TrackingState.ToEntityState(); //EntityState.Unchanged; foreach (var property in item.ModifiedProperties) { context.Entry(item).Property(property).IsModified = true; } } else { try { context.Entry(item).State = item.TrackingState.ToEntityState(); } catch (Exception) { if (item.TrackingState != TrackingState.Unchanged) { throw; } } } // Set other state for reference or child properties context.ApplyChangesOnProperties(item, visitationHelper.Clone(), TrackingState.Unchanged); // Clone to avoid interference context.ApplyChangesOnProperties(item, visitationHelper.Clone(), TrackingState.Modified); // Clone to avoid interference context.ApplyChangesOnProperties(item, visitationHelper, TrackingState.Deleted); } }
private async static Task LoadRelatedEntitiesOnPropertiesAsync(this DbContext context, ITrackable item, ObjectVisitationHelper visitationHelper, CancellationToken cancellationToken, bool loadAll) { // Recursively load related entities foreach (var navProp in item.GetNavigationProperties()) { // Apply changes to 1-1 and M-1 properties foreach (var refProp in navProp.AsReferenceProperty()) { await context.LoadRelatedEntitiesAsync(new[] { refProp.EntityReference }, item, visitationHelper, cancellationToken, loadAll).ConfigureAwait(false); } // Apply changes to 1-M and M-M properties foreach (var colProp in navProp.AsCollectionProperty()) { await context.LoadRelatedEntitiesAsync(colProp.EntityCollection, item, visitationHelper, cancellationToken, loadAll).ConfigureAwait(false); } } }
private static void ApplyChangesOnCollectionProperties(TrackingState stateFilter, bool includeState, EntityNavigationProperty navProp, EntityCollectionProperty <IList> colProp, DbContext context, ITrackable item, ObjectVisitationHelper visitationHelper, TrackingState?state = null) { // Apply changes to 1-M and M-M properties filtering by tracking state var count = colProp.EntityCollection.Count; for (int i = count - 1; i > -1; i--) { var trackableChild = colProp.EntityCollection[i] as ITrackable; if (trackableChild != null) { bool condition = includeState ? trackableChild.TrackingState == stateFilter : trackableChild.TrackingState != stateFilter; if (condition) { context.ApplyChanges(trackableChild, item, visitationHelper, navProp.Property.Name, state); } } } }
// TODO: Ensure entity and contract have same shape or do nothing; currently will fail otherwise private void SetContractFieldsInternal(object entity, object contract, ObjectVisitationHelper visitationHelper) { if (!visitationHelper.TryVisit(entity)) { return; } SetFields(entity, contract); foreach (var propertyInfo in entity.GetType().GetTypeInfo().GetProperties()) { if (propertyInfo.GetIndexParameters().Length > 0) { continue; } var propertyValueEntity = propertyInfo.GetValue(entity, null); if (propertyValueEntity == null) { continue; } var isEntitySingular = typeof(IBedrockEntity).GetTypeInfo().IsAssignableFrom(propertyInfo.PropertyType); var isEntityCollection = typeof(IEnumerable <IBedrockEntity>).GetTypeInfo().IsAssignableFrom(propertyInfo.PropertyType); if (isEntitySingular) { var contractPropertyInfo = contract.GetType().GetTypeInfo().GetProperties().FirstOrDefault(p => p.Name == propertyInfo.Name); if (contractPropertyInfo != null) { var propertyValueContract = contractPropertyInfo.GetValue(contract, null); if (propertyValueContract != null) { SetContractFieldsInternal(propertyValueEntity, propertyValueContract, visitationHelper); } } } else if (isEntityCollection) { var entities = propertyValueEntity as IEnumerable <IBedrockEntity>; var contractPropertyInfo = contract.GetType().GetTypeInfo().GetProperties().FirstOrDefault(p => p.Name == propertyInfo.Name); var contractPropertyCollection = contractPropertyInfo != null?contractPropertyInfo.GetValue(contract, null) as IEnumerable <IBedrockEntity> : null; if (contractPropertyCollection == null || (entities.Count() != contractPropertyCollection.Count())) { continue; } entities.Each((e, i) => { var propertyValueContract = contractPropertyCollection.ElementAt(i); if (propertyValueContract != null) { SetContractFieldsInternal(e, propertyValueContract, visitationHelper); } }); } } }