diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 04520295..6c35fb6f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -129,7 +129,7 @@ steps: versionEnvVar: Build.BuildNumber packDestination: '$(Build.ArtifactStagingDirectory)' continueOnError: true - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) +# condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) - task: CopyFiles@2 displayName: 'Copy VS Extension: $(build.artifactstagingdirectory)' diff --git a/src/OpenRiaServices.Client/Framework/Entity.cs b/src/OpenRiaServices.Client/Framework/Entity.cs index 8757f4f3..ac9a8933 100644 --- a/src/OpenRiaServices.Client/Framework/Entity.cs +++ b/src/OpenRiaServices.Client/Framework/Entity.cs @@ -250,8 +250,8 @@ private set EntitySet entitySet = this.LastSet; if (entitySet != null) { - bool isInteresting = this._entityState != EntityState.Unmodified - && this._entityState != EntityState.Detached; + bool isInteresting = this._entityState is not EntityState.Unmodified + and not EntityState.Detached; entitySet.TrackAsInteresting(this, isInteresting); } @@ -302,12 +302,9 @@ internal void OnChildUpdate() } } - if (this.EntitySet != null) - { - // when a child has been changed in any way, the parent becomes - // interesting - this.EntitySet.TrackAsInteresting(this, true); - } + // when a child has been changed in any way, the parent becomes + // interesting + this.EntitySet?.TrackAsInteresting(this, true); } /// @@ -526,8 +523,7 @@ private set this._isMerging = value; foreach (MetaMember metaMember in MetaType.DataMembers.Where(f => f.IsComplex && !f.IsCollection)) { - ComplexObject propertyValue = metaMember.GetValue(this) as ComplexObject; - if (propertyValue != null) + if (metaMember.GetValue(this) is ComplexObject propertyValue) { propertyValue.IsMergingState = this._isMerging; } @@ -647,10 +643,7 @@ internal void Reset() this._editSession = null; this._trackChanges = false; UndoAllEntityActions(throwIfSubmitting: true); - if (this._validationErrors != null) - { - this._validationErrors.Clear(); - } + this._validationErrors?.Clear(); this.EntityConflict = null; this.EntitySet = null; this._lastSet = null; @@ -682,8 +675,8 @@ internal void StopTracking() /// protected void AcceptChanges() { - if (this.EntityState == EntityState.Unmodified || - this.EntityState == EntityState.Detached) + if (this.EntityState is EntityState.Unmodified or + EntityState.Detached) { // if we're detached or have no changes, noop after // closing any in progress edit session @@ -696,11 +689,7 @@ protected void AcceptChanges() // Accept any child changes. Note, we must accept child changes before setting our own // state to Unmodified. This avoids a situation where we get notifications from child // entities that cause our state to flip back to Modified. - if (entitySet != null) - { - // accept any child changes - entitySet.EntityContainer.CompleteChildChanges(this, true); - } + entitySet?.EntityContainer.CompleteChildChanges(this, true); if (this.EntityState == EntityState.New) { @@ -730,19 +719,13 @@ protected void AcceptChanges() else if (this.EntityState == EntityState.Deleted) { this.StopTracking(); - if (entitySet != null) - { - entitySet.RemoveFromCache(this); - } + entitySet?.RemoveFromCache(this); // move back to the default state this.EntityState = EntityState.Detached; } - if (entitySet != null) - { - // remove from the interesting entities set - entitySet.TrackAsInteresting(this, false); - } + // remove from the interesting entities set + entitySet?.TrackAsInteresting(this, false); // need to end any in progress edit session this._editSession = null; @@ -750,10 +733,7 @@ protected void AcceptChanges() // clear all custom method invocations this.UndoAllEntityActions(throwIfSubmitting: false); - if (this._validationErrors != null) - { - this._validationErrors.Clear(); - } + this._validationErrors?.Clear(); this.EntityConflict = null; this.IsInferred = false; this._hasChildChanges = false; @@ -769,8 +749,8 @@ protected void AcceptChanges() /// protected void RejectChanges() { - if (this.EntityState == EntityState.Unmodified || - this.EntityState == EntityState.Detached) + if (this.EntityState is EntityState.Unmodified or + EntityState.Detached) { // if we're detached or have no changes, noop after // closing any in progress edit session @@ -783,10 +763,7 @@ protected void RejectChanges() // Reject any child changes. Note, we must reject child changes before setting our own // state to Unmodified. This avoids a situation where we get notifications from child // entities that cause our state to flip back to Modified. - if (entitySet != null) - { - entitySet.EntityContainer.CompleteChildChanges(this, false); - } + entitySet?.EntityContainer.CompleteChildChanges(this, false); if (this._entityState == EntityState.Modified || this.Parent != null) { @@ -830,10 +807,7 @@ protected void RejectChanges() UndoAllEntityActions(); // Empty out the error collections - if (this._validationErrors != null) - { - this._validationErrors.Clear(); - } + this._validationErrors?.Clear(); this.EntityConflict = null; this._hasChildChanges = false; Debug.Assert(!this.HasChanges, "Entity.HasChanges should be false"); @@ -1845,7 +1819,7 @@ private EditSession(Entity entity) { this._entity = entity; this._lastState = entity.EntityState; - this._customMethodInvocations = entity._customMethodInvocations != null ? entity._customMethodInvocations.ToArray() : null; + this._customMethodInvocations = entity._customMethodInvocations?.ToArray(); this._validationErrors = entity.ValidationErrors.ToArray(); this._modifiedProperties = new List(); } diff --git a/src/OpenRiaServices.Client/Framework/EntitySet.cs b/src/OpenRiaServices.Client/Framework/EntitySet.cs index 000a0082..9780c714 100644 --- a/src/OpenRiaServices.Client/Framework/EntitySet.cs +++ b/src/OpenRiaServices.Client/Framework/EntitySet.cs @@ -27,9 +27,9 @@ public abstract class EntitySet : IEnumerable, ICollection, INotifyCollectionCha // backing entity list private IList _list; // set of entities, for fast lookup - private HashSet _set; - private Dictionary _identityCache; - private readonly HashSet _interestingEntities; + private readonly HashSet _set = new(); + private readonly Dictionary _identityCache = new(); + private readonly HashSet _interestingEntities = new(); private NotifyCollectionChangedEventHandler _collectionChangedEventHandler; /// @@ -49,8 +49,6 @@ internal EntitySet(Type entityType) } this._entityType = entityType; - this._identityCache = new Dictionary(); - this._interestingEntities = new HashSet(); } /// @@ -58,9 +56,9 @@ internal EntitySet(Type entityType) /// public Type EntityType { - get - { - return this._entityType; + get + { + return this._entityType; } } @@ -117,10 +115,10 @@ public void Clear() entity.Reset(); } - this._identityCache = new Dictionary(); + this._identityCache.Clear(); this._interestingEntities.Clear(); this._list = this.CreateList(); - this._set = new HashSet(); + this._set.Clear(); this.OnCollectionChanged(NotifyCollectionChangedAction.Reset, clearedEntities, -1); @@ -198,29 +196,22 @@ internal void TrackAsInteresting(Entity entity, bool isInteresting) { if (isInteresting) { - if (!this.InterestingEntities.Contains(entity)) + if (this._interestingEntities.Add(entity) + && this._interestingEntities.Count == 1) { - this._interestingEntities.Add(entity); - if (this._interestingEntities.Count == 1) - { - // this is the first interesting entity in this set - // so raise the change notifications - this.RaisePropertyChanged(nameof(HasChanges)); - } + // this is the first interesting entity in this set + // so raise the change notifications + this.RaisePropertyChanged(nameof(HasChanges)); } } else { - if (this._interestingEntities != null) + if (this._interestingEntities.Remove(entity) + && this._interestingEntities.Count == 0) { - int prevCount = this._interestingEntities.Count; - this._interestingEntities.Remove(entity); - if (this._interestingEntities.Count == 0 && prevCount == 1) - { - // if the last interesting entity has been removed, this set - // no longer has changes, so raise the change notifications - this.RaisePropertyChanged(nameof(HasChanges)); - } + // if the last interesting entity has been removed, this set + // no longer has changes, so raise the change notifications + this.RaisePropertyChanged(nameof(HasChanges)); } } } @@ -274,7 +265,6 @@ internal void Initialize(EntityContainer container, EntitySetOperations operatio this._supportedOperations = operationsToSupport; this._list = this.CreateList(); - this._set = new HashSet(); } /// @@ -317,8 +307,7 @@ internal void UpdateRelatedAssociations(Entity entity, string propertyName) /// True if the callback is being registered, false if it is being unregistered internal void RegisterAssociationCallback(AssociationAttribute association, Action callback, bool register) { - Action del = null; - this._associationUpdateCallbackMap.TryGetValue(association, out del); + this._associationUpdateCallbackMap.TryGetValue(association, out Action del); if (register) { this._associationUpdateCallbackMap[association] = (Action)Delegate.Combine(del, callback); @@ -364,7 +353,7 @@ internal bool IsAttached(Entity entity) // An entity is considered attached if it is in this set or // if it is "know" by this set, for example having been deleted - return entity.EntitySet == this || this.InterestingEntities.Contains(entity); + return entity.EntitySet == this || this._interestingEntities.Contains(entity); } /// @@ -404,9 +393,10 @@ internal void AddInternal(Entity entity) // - scenarios where the entity instance itself is the one already cached (for infer attach // state transition scenarios) object identity = entity.GetIdentity(); - Entity cachedEntity = null; - if (identity != null && this._identityCache.TryGetValue(identity, out cachedEntity) && - cachedEntity.EntityState != EntityState.Deleted && !(object.ReferenceEquals(entity, cachedEntity))) + if (identity != null + && this._identityCache.TryGetValue(identity, out Entity cachedEntity) + && cachedEntity.EntityState != EntityState.Deleted + && !object.ReferenceEquals(entity, cachedEntity)) { throw new InvalidOperationException(Resource.EntitySet_DuplicateIdentity); } @@ -421,10 +411,9 @@ internal void AddInternal(Entity entity) entity.UndoDelete(); } - if (!this._set.Contains(entity)) + if (this._set.Add(entity)) { int idx = this._list.Add(entity); - this._set.Add(entity); entity.EntitySet = this; this.OnCollectionChanged(NotifyCollectionChangedAction.Add, entity, idx); } @@ -494,7 +483,7 @@ public void Remove(Entity entity) internal bool Contains(Entity entity) { - return this._set != null && this._set.Contains(entity); + return this._set.Contains(entity); } /// @@ -745,11 +734,10 @@ internal Entity LoadEntity(Entity entity, LoadBehavior loadBehavior) cachedEntity = entity; int idx = 0; - bool isInList = this._set.Contains(entity); - if (!isInList) + bool isAdded = this._set.Add(entity); + if (isAdded) { idx = this._list.Add(entity); - this._set.Add(entity); } entity.MarkUnmodified(); @@ -764,7 +752,7 @@ internal Entity LoadEntity(Entity entity, LoadBehavior loadBehavior) entity.OnLoaded(true); - if (!isInList) + if (isAdded) { this.OnCollectionChanged(NotifyCollectionChangedAction.Add, entity, idx); } @@ -936,23 +924,23 @@ protected virtual void OnCollectionChanged(NotifyCollectionChangedAction action, } } -#region IEnumerable Members + #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } -#endregion + #endregion -#region ICollection Members + #region ICollection Members bool ICollection.IsSynchronized { get { return false; } } object ICollection.SyncRoot { get { return _list.SyncRoot; } } void ICollection.CopyTo(Array array, int index) { this._list.CopyTo(array, index); } -#endregion + #endregion -#region INotifyCollectionChanged Members + #region INotifyCollectionChanged Members /// /// Event raised when the collection has changed, or the collection is reset. @@ -969,18 +957,18 @@ public event NotifyCollectionChangedEventHandler CollectionChanged } } -#endregion + #endregion -#region IRevertibleChangeTracking Members + #region IRevertibleChangeTracking Members void IRevertibleChangeTracking.RejectChanges() { this.RejectChanges(); } -#endregion + #endregion -#region IChangeTracking Members + #region IChangeTracking Members bool IChangeTracking.IsChanged { @@ -995,9 +983,9 @@ void IChangeTracking.AcceptChanges() this.AcceptChanges(); } -#endregion + #endregion -#region INotifyPropertyChanged Members + #region INotifyPropertyChanged Members /// /// Event raised when a property has changed. /// @@ -1017,7 +1005,7 @@ protected void RaisePropertyChanged(string propertyName) } } -#endregion + #endregion /// /// Visitor used to traverse all associations in a graph and infer @@ -1025,7 +1013,7 @@ protected void RaisePropertyChanged(string propertyName) /// private class AddAttachInferrer : EntityVisitor { - private readonly Dictionary _visited = new Dictionary(); + private readonly HashSet _visited = new HashSet(); private readonly EntityContainer _container; private bool _isTopLevel = true; private readonly Action _action; @@ -1056,22 +1044,32 @@ private AddAttachInferrer(EntityContainer container, Action a public override void Visit(Entity entity) { - // avoid cycles - if (this._visited.ContainsKey(entity)) + // avoid cycles, stop if entity has already been visisted + if (!this._visited.Add(entity)) { return; } EntitySet set = this._container.GetEntitySet(entity.GetType()); - if (!this._isTopLevel && !set.IsAttached(entity)) + if (!this._isTopLevel) { - // infer for all detached entities except the root - entity.IsInferred = true; - this._action(set, entity); + if (!set.IsAttached(entity)) + { + // infer for all detached entities except the root + entity.IsInferred = true; + this._action(set, entity); + } + else + { + // Entity is attached so state must anything but Detached: New, Unmodified, Modified, Deleted + // * For New, Unchanged, Modified then all changes to EntityRef/EntityCollections are tracked and corresponding entities are + // added directly to the EntitySet, so any changes there will have been previously processed + // * Deleted => Are not tracked (just as for Detached handled above) so they might point to new entities to discover + if (entity.EntityState != EntityState.Deleted) + return; + } } - this._visited.Add(entity, true); - this._isTopLevel = false; base.Visit(entity); @@ -1210,14 +1208,14 @@ protected override void VisitEntityRef(IEntityRef entityRef, Entity parent, Meta } } } - + /// /// Represents a collection of instances, providing change tracking and other services. /// /// The type of this set will contain public sealed class EntitySet : EntitySet, IEntityCollection #if HAS_COLLECTIONVIEW - , ICollectionViewFactory + , ICollectionViewFactory #endif where TEntity : Entity { @@ -1342,14 +1340,14 @@ protected override void OnCollectionChanged(NotifyCollectionChangedAction action base.OnCollectionChanged(action, affectedObject, index); } -#region IEnumerable Members + #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } -#endregion + #endregion -#region ICollection Members + #region ICollection Members void ICollection.CopyTo(TEntity[] array, int arrayIndex) { ((IList)List).CopyTo(array, arrayIndex); @@ -1377,9 +1375,9 @@ bool ICollection.Remove(TEntity item) throw; } } -#endregion + #endregion -#region ICollectionViewFactory + #region ICollectionViewFactory #if HAS_COLLECTIONVIEW /// /// Returns a custom view for specialized sorting, filtering, grouping, and currency. @@ -1388,7 +1386,7 @@ bool ICollection.Remove(TEntity item) ICollectionView ICollectionViewFactory.CreateView() { // We use the CollectionViewSource to obtain a ListCollectionView, a type internal to Silverlight - return new CollectionViewSource() { Source = new ListCollectionViewProxy(this) } .View; + return new CollectionViewSource() { Source = new ListCollectionViewProxy(this) }.View; } /// @@ -1413,7 +1411,7 @@ internal ListCollectionViewProxy(EntitySet source) WeakCollectionChangedListener.CreateIfNecessary(this._source, this); } -#region IList + #region IList public int Add(object value) { @@ -1425,8 +1423,12 @@ public int Add(object value) nameof(value)); } + int countBefore = this.Source.Count; this.Source.Add(entity); - return this.IndexOf(entity); + int countAfter = this.Source.Count; + + // If count increased then the item was added last, otherwise return -1 for "not added" + return (countAfter > countBefore) ? countBefore : -1; } public void Clear() @@ -1436,7 +1438,7 @@ public void Clear() public bool Contains(object value) { - return this.IndexOf(value) >= 0; + return (value is Entity e) && this.Source.Contains(e); } public int IndexOf(object value) @@ -1462,13 +1464,10 @@ public bool IsReadOnly public void Remove(object value) { - T entity = (T)value; - if (entity == null) + if (value is T entity) { - return; + this.Source.Remove(entity); } - - this.Source.Remove(entity); } public void RemoveAt(int index) @@ -1480,10 +1479,6 @@ public object this[int index] { get { - if ((index < 0) || (index >= this.Source.Count)) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } return this.Source.List[index]; } set @@ -1528,9 +1523,9 @@ private EntitySet Source get { return this._source; } } -#endregion + #endregion -#region INotifyCollectionChanged + #region INotifyCollectionChanged public event NotifyCollectionChangedEventHandler CollectionChanged; @@ -1544,18 +1539,18 @@ private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEve this.OnCollectionChanged(e); } -#endregion + #endregion -#region ICollectionChangedListener + #region ICollectionChangedListener void ICollectionChangedListener.OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { this.OnSourceCollectionChanged(sender, e); } -#endregion + #endregion } #endif -#endregion + #endregion } } diff --git a/src/OpenRiaServices.Client/Framework/Internal/MetaType.cs b/src/OpenRiaServices.Client/Framework/Internal/MetaType.cs index 4df035b2..cb13c341 100644 --- a/src/OpenRiaServices.Client/Framework/Internal/MetaType.cs +++ b/src/OpenRiaServices.Client/Framework/Internal/MetaType.cs @@ -31,6 +31,7 @@ public sealed class MetaType private readonly ReadOnlyCollection _dataMembers; private readonly ReadOnlyCollection _validationAttributes; private readonly Dictionary _customUpdateMethods; + private readonly MetaMember[] _associationMembers; /// /// Returns the MetaType for the specified Type. @@ -115,6 +116,7 @@ select attribute // for identity purposes, we need to make sure values are always ordered KeyMembers = new ReadOnlyCollection(_metaMembers.Values.Where(m => m.IsKeyMember).OrderBy(m => m.Name).ToArray()); _dataMembers = new ReadOnlyCollection(_metaMembers.Values.Where(m => m.IsDataMember).ToArray()); + _associationMembers = _metaMembers.Values.Where(m => m.IsAssociationMember).ToArray(); if (!_requiresValidation && HasComplexMembers) { @@ -219,13 +221,7 @@ public IEnumerable GetEntityActions() /// /// Gets the collection of association members for this entity Type. /// - public IEnumerable AssociationMembers - { - get - { - return this._metaMembers.Values.Where(m => m.IsAssociationMember); - } - } + public IEnumerable AssociationMembers => _associationMembers; /// /// Gets the collection of child types this entity Type composes. diff --git a/src/OpenRiaServices.Client/Test/Client.Test/Data/EntityContainerTests.cs b/src/OpenRiaServices.Client/Test/Client.Test/Data/EntityContainerTests.cs index e2d670cf..87cd05e3 100644 --- a/src/OpenRiaServices.Client/Test/Client.Test/Data/EntityContainerTests.cs +++ b/src/OpenRiaServices.Client/Test/Client.Test/Data/EntityContainerTests.cs @@ -3832,6 +3832,49 @@ public void EntitySet_Detach() Assert.IsTrue(ec.GetChanges().IsEmpty); } + + [TestMethod] + public void EntitySet_Remove_Inferred_Entities() + { + TestEntityContainer ec = new TestEntityContainer(); + EntitySet products = ec.GetEntitySet(); + + // attach a product + Product product = new Product + { + ProductID = 1, + Name = "Choco Crisp", + }; + // EntityCollection + var order = new PurchaseOrder { PurchaseOrderID = 1 }; + var detail1 = new PurchaseOrderDetail() { PurchaseOrder = order, PurchaseOrderID = 1, PurchaseOrderDetailID = 1 }; + product.PurchaseOrderDetails.Add(detail1); + products.Attach(product); + + // Delete the entity, it should be marked for deletion + products.Remove(product); + Assert.AreEqual(EntityState.Deleted, product.EntityState); + Assert.AreEqual(EntityState.Unmodified, detail1.EntityState); + Assert.AreEqual(EntityState.Unmodified, order.EntityState); + + var detail2 = new PurchaseOrderDetail() { PurchaseOrderDetailID = 2, Product = product, PurchaseOrderID = order.PurchaseOrderID }; + var detail3 = new PurchaseOrderDetail() { PurchaseOrderDetailID = 3, Product = product, PurchaseOrderID = order.PurchaseOrderID }; + + product.PurchaseOrderDetails.Add(detail2); + product.PurchaseOrderDetails.Add(detail3); + + Assert.AreEqual(EntityState.Deleted, product.EntityState); + Assert.AreEqual(EntityState.Detached, detail2.EntityState); + Assert.AreEqual(EntityState.Detached, detail3.EntityState); + + // Add detail2, which should trigger detail3 to be discovered via product (which should still be deleted) + order.PurchaseOrderDetails.Add(detail2); + + Assert.AreEqual(EntityState.Deleted, product.EntityState); + Assert.AreEqual(EntityState.New, detail2.EntityState); + Assert.AreEqual(EntityState.New, detail3.EntityState); + } + [TestMethod] public void EntitySet_CollectionChangedEvents() {