1 //---------------------------------------------------------------------
2 // <copyright file="RelationshipConstraintValidator.cs" company="Microsoft">
3 //      Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //
6 // @owner Microsoft
7 // @backupOwner Microsoft
8 //---------------------------------------------------------------------
9 
10 namespace System.Data.Mapping.Update.Internal
11 {
12     using System.Collections.Generic;
13     using System.Data.Common;
14     using System.Data.Common.Utils;
15     using System.Data.Entity;
16     using System.Data.Metadata.Edm;
17     using System.Diagnostics;
18     using System.Globalization;
19     using System.Linq;
20 
21     internal partial class UpdateTranslator
22     {
23         /// <summary>
24         /// Class validating relationship cardinality constraints. Only reasons about constraints that can be inferred
25         /// by examining change requests from the store.
26         /// (no attempt is made to ensure consistency of the store subsequently, since this would require pulling in all
27         /// values from the store).
28         /// </summary>
29         private class RelationshipConstraintValidator
30         {
31             #region Constructor
RelationshipConstraintValidator(UpdateTranslator updateTranslator)32             internal RelationshipConstraintValidator(UpdateTranslator updateTranslator)
33             {
34                 m_existingRelationships = new Dictionary<DirectionalRelationship, DirectionalRelationship>(EqualityComparer<DirectionalRelationship>.Default);
35                 m_impliedRelationships = new Dictionary<DirectionalRelationship, IEntityStateEntry>(EqualityComparer<DirectionalRelationship>.Default);
36                 m_referencingRelationshipSets = new Dictionary<EntitySet, List<AssociationSet>>(EqualityComparer<EntitySet>.Default);
37                 m_updateTranslator = updateTranslator;
38             }
39             #endregion
40 
41             #region Fields
42             /// <summary>
43             /// Relationships registered in the validator.
44             /// </summary>
45             private readonly Dictionary<DirectionalRelationship, DirectionalRelationship> m_existingRelationships;
46 
47             /// <summary>
48             /// Relationships the validator determines are required based on registered entities.
49             /// </summary>
50             private readonly Dictionary<DirectionalRelationship, IEntityStateEntry> m_impliedRelationships;
51 
52             /// <summary>
53             /// Cache used to store relationship sets with ends bound to entity sets.
54             /// </summary>
55             private readonly Dictionary<EntitySet, List<AssociationSet>> m_referencingRelationshipSets;
56 
57             /// <summary>
58             /// Update translator containing session context.
59             /// </summary>
60             private readonly UpdateTranslator m_updateTranslator;
61             #endregion
62 
63             #region Methods
64             /// <summary>
65             /// Add an entity to be tracked by the validator. Requires that the input describes an entity.
66             /// </summary>
67             /// <param name="stateEntry">State entry for the entity being tracked.</param>
RegisterEntity(IEntityStateEntry stateEntry)68             internal void RegisterEntity(IEntityStateEntry stateEntry)
69             {
70                 EntityUtil.CheckArgumentNull(stateEntry, "stateEntry");
71 
72                 if (EntityState.Added == stateEntry.State || EntityState.Deleted == stateEntry.State)
73                 {
74                     // We only track added and deleted entities because modifications to entities do not affect
75                     // cardinality constraints. Relationships are based on end keys, and it is not
76                     // possible to modify key values.
77                     Debug.Assert(null != (object)stateEntry.EntityKey, "entity state entry must have an entity key");
78                     EntityKey entityKey = EntityUtil.CheckArgumentNull(stateEntry.EntityKey, "stateEntry.EntityKey");
79                     EntitySet entitySet = (EntitySet)stateEntry.EntitySet;
80                     EntityType entityType = EntityState.Added == stateEntry.State ?
81                         GetEntityType(stateEntry.CurrentValues) :
82                         GetEntityType(stateEntry.OriginalValues);
83 
84                     // figure out relationship set ends that are associated with this entity set
85                     foreach (AssociationSet associationSet in GetReferencingAssocationSets(entitySet))
86                     {
87                         // describe unidirectional relationships in which the added entity is the "destination"
88                         var ends = associationSet.AssociationSetEnds;
89                         foreach (var fromEnd in ends)
90                         {
91                             foreach (var toEnd in ends)
92                             {
93                                 // end to itself does not describe an interesting relationship subpart
94                                 if (object.ReferenceEquals(toEnd.CorrespondingAssociationEndMember,
95                                     fromEnd.CorrespondingAssociationEndMember)) { continue; }
96 
97                                 // skip ends that don't target the current entity set
98                                 if (!toEnd.EntitySet.EdmEquals(entitySet)) { continue; }
99 
100                                 // skip ends that aren't required
101                                 if (0 == MetadataHelper.GetLowerBoundOfMultiplicity(
102                                     fromEnd.CorrespondingAssociationEndMember.RelationshipMultiplicity)) { continue; }
103 
104                                 // skip ends that don't target the current entity type
105                                 if (!MetadataHelper.GetEntityTypeForEnd(toEnd.CorrespondingAssociationEndMember)
106                                     .IsAssignableFrom(entityType)) { continue; }
107 
108                                 // register the relationship so that we know it's required
109                                 DirectionalRelationship relationship = new DirectionalRelationship(entityKey, fromEnd.CorrespondingAssociationEndMember,
110                                     toEnd.CorrespondingAssociationEndMember, associationSet, stateEntry);
111                                 m_impliedRelationships.Add(relationship, stateEntry);
112                             }
113                         }
114                     }
115                 }
116             }
117 
118             // requires: input is an IExtendedDataRecord representing an entity
119             // returns: entity type for the given record
GetEntityType(DbDataRecord dbDataRecord)120             private static EntityType GetEntityType(DbDataRecord dbDataRecord)
121             {
122                 IExtendedDataRecord extendedRecord = dbDataRecord as IExtendedDataRecord;
123                 Debug.Assert(extendedRecord != null);
124 
125                 Debug.Assert(BuiltInTypeKind.EntityType == extendedRecord.DataRecordInfo.RecordType.EdmType.BuiltInTypeKind);
126                 return (EntityType)extendedRecord.DataRecordInfo.RecordType.EdmType;
127             }
128 
129             /// <summary>
130             /// Add a relationship to be tracked by the validator.
131             /// </summary>
132             /// <param name="associationSet">Relationship set to which the given record belongs.</param>
133             /// <param name="record">Relationship record. Must conform to the type of the relationship set.</param>
134             /// <param name="stateEntry">State entry for the relationship being tracked</param>
RegisterAssociation(AssociationSet associationSet, IExtendedDataRecord record, IEntityStateEntry stateEntry)135             internal void RegisterAssociation(AssociationSet associationSet, IExtendedDataRecord record, IEntityStateEntry stateEntry)
136             {
137                 EntityUtil.CheckArgumentNull(associationSet, "relationshipSet");
138                 EntityUtil.CheckArgumentNull(record, "record");
139                 EntityUtil.CheckArgumentNull(stateEntry, "stateEntry");
140 
141                 Debug.Assert(associationSet.ElementType.Equals(record.DataRecordInfo.RecordType.EdmType));
142 
143                 // retrieve the ends of the relationship
144                 Dictionary<string, EntityKey> endNameToKeyMap = new Dictionary<string, EntityKey>(
145                     StringComparer.Ordinal);
146                 foreach (FieldMetadata field in record.DataRecordInfo.FieldMetadata)
147                 {
148                     string endName = field.FieldType.Name;
149                     EntityKey entityKey = (EntityKey)record.GetValue(field.Ordinal);
150                     endNameToKeyMap.Add(endName, entityKey);
151                 }
152 
153                 // register each unidirectional relationship subpart in the relationship instance
154                 var ends = associationSet.AssociationSetEnds;
155                 foreach (var fromEnd in ends)
156                 {
157                     foreach (var toEnd in ends)
158                     {
159                         // end to itself does not describe an interesting relationship subpart
160                         if (object.ReferenceEquals(toEnd.CorrespondingAssociationEndMember, fromEnd.CorrespondingAssociationEndMember))
161                         {
162                             continue;
163                         }
164 
165                         EntityKey toEntityKey = endNameToKeyMap[toEnd.CorrespondingAssociationEndMember.Name];
166                         DirectionalRelationship relationship = new DirectionalRelationship(toEntityKey, fromEnd.CorrespondingAssociationEndMember,
167                             toEnd.CorrespondingAssociationEndMember, associationSet, stateEntry);
168                         AddExistingRelationship(relationship);
169                     }
170                 }
171             }
172 
173             /// <summary>
174             /// Validates cardinality constraints for all added entities/relationships.
175             /// </summary>
ValidateConstraints()176             internal void ValidateConstraints()
177             {
178                 // ensure all expected relationships exist
179                 foreach (KeyValuePair<DirectionalRelationship, IEntityStateEntry> expected in m_impliedRelationships)
180                 {
181                     DirectionalRelationship expectedRelationship = expected.Key;
182                     IEntityStateEntry stateEntry = expected.Value;
183 
184                     // determine actual end cardinality
185                     int count = GetDirectionalRelationshipCountDelta(expectedRelationship);
186 
187                     if (EntityState.Deleted == stateEntry.State)
188                     {
189                         // our cardinality expectations are reversed for delete (cardinality of 1 indicates
190                         // we want -1 operation total)
191                         count = -count;
192                     }
193 
194                     // determine expected cardinality
195                     int minimumCount = MetadataHelper.GetLowerBoundOfMultiplicity(expectedRelationship.FromEnd.RelationshipMultiplicity);
196                     int? maximumCountDeclared = MetadataHelper.GetUpperBoundOfMultiplicity(expectedRelationship.FromEnd.RelationshipMultiplicity);
197                     int maximumCount = maximumCountDeclared.HasValue ? maximumCountDeclared.Value : count; // negative value
198                     // indicates unlimited cardinality
199 
200                     if (count < minimumCount || count > maximumCount)
201                     {
202                         // We could in theory "fix" the cardinality constraint violation by introducing surrogates,
203                         // but we risk doing work on behalf of the user they don't want performed (e.g., deleting an
204                         // entity or relationship the user has intentionally left untouched).
205                         throw EntityUtil.UpdateRelationshipCardinalityConstraintViolation(
206                             expectedRelationship.AssociationSet.Name, minimumCount, maximumCountDeclared,
207                             TypeHelpers.GetFullName(expectedRelationship.ToEntityKey.EntityContainerName, expectedRelationship.ToEntityKey.EntitySetName),
208                             count, expectedRelationship.FromEnd.Name,
209                             stateEntry);
210                     }
211                 }
212 
213                 // ensure actual relationships have required ends
214                 foreach (DirectionalRelationship actualRelationship in m_existingRelationships.Keys)
215                 {
216                     int addedCount;
217                     int deletedCount;
218                     actualRelationship.GetCountsInEquivalenceSet(out addedCount, out deletedCount);
219                     int absoluteCount = Math.Abs(addedCount - deletedCount);
220                     int minimumCount = MetadataHelper.GetLowerBoundOfMultiplicity(actualRelationship.FromEnd.RelationshipMultiplicity);
221                     int? maximumCount = MetadataHelper.GetUpperBoundOfMultiplicity(actualRelationship.FromEnd.RelationshipMultiplicity);
222 
223                     // Check that we haven't inserted or deleted too many relationships
224                     if (maximumCount.HasValue)
225                     {
226                         EntityState? violationType = default(EntityState?);
227                         int? violationCount = default(int?);
228                         if (addedCount > maximumCount.Value)
229                         {
230                             violationType = EntityState.Added;
231                             violationCount = addedCount;
232                         }
233                         else if (deletedCount > maximumCount.Value)
234                         {
235                             violationType = EntityState.Deleted;
236                             violationCount = deletedCount;
237                         }
238                         if (violationType.HasValue)
239                         {
240                             throw EntityUtil.Update(Strings.Update_RelationshipCardinalityViolation(maximumCount.Value,
241                                 violationType.Value, actualRelationship.AssociationSet.ElementType.FullName,
242                                 actualRelationship.FromEnd.Name, actualRelationship.ToEnd.Name, violationCount.Value),
243                                 null, actualRelationship.GetEquivalenceSet().Select(reln => reln.StateEntry));
244 
245                         }
246                     }
247 
248                     // We care about the case where there is a relationship but no entity when
249                     // the relationship and entity map to the same table. If there is a relationship
250                     // with 1..1 cardinality to the entity and the relationship is being added or deleted,
251                     // it is required that the entity is also added or deleted.
252                     if (1 == absoluteCount && 1 == minimumCount && 1 == maximumCount) // 1..1 relationship being added/deleted
253                     {
254                         bool isAdd = addedCount > deletedCount;
255 
256                         // Ensure the entity is also being added or deleted
257                         IEntityStateEntry entityEntry;
258 
259                         // Identify the following error conditions:
260                         // - the entity is not being modified at all
261                         // - the entity is being modified, but not in the way we expect (it's not being added or deleted)
262                         if (!m_impliedRelationships.TryGetValue(actualRelationship, out entityEntry) ||
263                             (isAdd && EntityState.Added != entityEntry.State) ||
264                             (!isAdd && EntityState.Deleted != entityEntry.State))
265                         {
266                             throw EntityUtil.UpdateEntityMissingConstraintViolation(actualRelationship.AssociationSet.Name,
267                                 actualRelationship.ToEnd.Name, actualRelationship.StateEntry);
268                         }
269                     }
270                 }
271             }
272 
273             /// <summary>
274             /// Determines the net change in relationship count.
275             /// For instance, if the directional relationship is added 2 times and deleted 3, the return value is -1.
276             /// </summary>
GetDirectionalRelationshipCountDelta(DirectionalRelationship expectedRelationship)277             private int GetDirectionalRelationshipCountDelta(DirectionalRelationship expectedRelationship)
278             {
279                 // lookup up existing relationship from expected relationship
280                 DirectionalRelationship existingRelationship;
281                 if (m_existingRelationships.TryGetValue(expectedRelationship, out existingRelationship))
282                 {
283                     int addedCount;
284                     int deletedCount;
285                     existingRelationship.GetCountsInEquivalenceSet(out addedCount, out deletedCount);
286                     return addedCount - deletedCount;
287                 }
288                 else
289                 {
290                     // no modifications to the relationship... return 0 (no net change)
291                     return 0;
292                 }
293             }
294 
AddExistingRelationship(DirectionalRelationship relationship)295             private void AddExistingRelationship(DirectionalRelationship relationship)
296             {
297                 DirectionalRelationship existingRelationship;
298                 if (m_existingRelationships.TryGetValue(relationship, out existingRelationship))
299                 {
300                     existingRelationship.AddToEquivalenceSet(relationship);
301                 }
302                 else
303                 {
304                     m_existingRelationships.Add(relationship, relationship);
305                 }
306             }
307 
308             /// <summary>
309             /// Determine which relationship sets reference the given entity set.
310             /// </summary>
311             /// <param name="entitySet">Entity set for which to identify relationships</param>
312             /// <returns>Relationship sets referencing the given entity set</returns>
GetReferencingAssocationSets(EntitySet entitySet)313             private IEnumerable<AssociationSet> GetReferencingAssocationSets(EntitySet entitySet)
314             {
315                 List<AssociationSet> relationshipSets;
316 
317                 // check if this information is cached
318                 if (!m_referencingRelationshipSets.TryGetValue(entitySet, out relationshipSets))
319                 {
320                     relationshipSets = new List<AssociationSet>();
321 
322                     // relationship sets must live in the same container as the entity sets they reference
323                     EntityContainer container = entitySet.EntityContainer;
324                     foreach (EntitySetBase extent in container.BaseEntitySets)
325                     {
326                         AssociationSet associationSet = extent as AssociationSet;
327 
328                         if (null != associationSet && !associationSet.ElementType.IsForeignKey)
329                         {
330                             foreach (var end in associationSet.AssociationSetEnds)
331                             {
332                                 if (end.EntitySet.Equals(entitySet))
333                                 {
334                                     relationshipSets.Add(associationSet);
335                                     break;
336                                 }
337                             }
338                         }
339                     }
340 
341                     // add referencing relationship information to the cache
342                     m_referencingRelationshipSets.Add(entitySet, relationshipSets);
343                 }
344 
345                 return relationshipSets;
346             }
347             #endregion
348 
349             #region Nested types
350             /// <summary>
351             /// An instance of an actual or expected relationship. This class describes one direction
352             /// of the relationship.
353             /// </summary>
354             private class DirectionalRelationship : IEquatable<DirectionalRelationship>
355             {
356                 /// <summary>
357                 /// Entity key for the entity being referenced by the relationship.
358                 /// </summary>
359                 internal readonly EntityKey ToEntityKey;
360 
361                 /// <summary>
362                 /// Name of the end referencing the entity key.
363                 /// </summary>
364                 internal readonly AssociationEndMember FromEnd;
365 
366                 /// <summary>
367                 /// Name of the end the entity key references.
368                 /// </summary>
369                 internal readonly AssociationEndMember ToEnd;
370 
371                 /// <summary>
372                 /// State entry containing this relationship.
373                 /// </summary>
374                 internal readonly IEntityStateEntry StateEntry;
375 
376                 /// <summary>
377                 /// Reference to the relationship set.
378                 /// </summary>
379                 internal readonly AssociationSet AssociationSet;
380 
381                 /// <summary>
382                 /// Reference to next 'equivalent' relationship in circular linked list.
383                 /// </summary>
384                 private DirectionalRelationship _equivalenceSetLinkedListNext;
385 
386                 private readonly int _hashCode;
387 
DirectionalRelationship(EntityKey toEntityKey, AssociationEndMember fromEnd, AssociationEndMember toEnd, AssociationSet associationSet, IEntityStateEntry stateEntry)388                 internal DirectionalRelationship(EntityKey toEntityKey, AssociationEndMember fromEnd, AssociationEndMember toEnd, AssociationSet associationSet, IEntityStateEntry stateEntry)
389                 {
390                     ToEntityKey = EntityUtil.CheckArgumentNull(toEntityKey, "toEntityKey");
391                     FromEnd = EntityUtil.CheckArgumentNull(fromEnd, "fromEnd");
392                     ToEnd = EntityUtil.CheckArgumentNull(toEnd, "toEnd");
393                     AssociationSet = EntityUtil.CheckArgumentNull(associationSet, "associationSet");
394                     StateEntry = EntityUtil.CheckArgumentNull(stateEntry, "stateEntry");
395                     _equivalenceSetLinkedListNext = this;
396 
397                     _hashCode = toEntityKey.GetHashCode() ^
398                         fromEnd.GetHashCode() ^
399                         toEnd.GetHashCode() ^
400                         associationSet.GetHashCode();
401                 }
402 
403                 /// <summary>
404                 /// Requires: 'other' must refer to the same relationship metadata and the same target entity and
405                 /// must not already be a part of an equivalent set.
406                 /// Adds the given relationship to linked list containing all equivalent relationship instances
407                 /// for this relationship (e.g. all orders associated with a specific customer)
408                 /// </summary>
AddToEquivalenceSet(DirectionalRelationship other)409                 internal void AddToEquivalenceSet(DirectionalRelationship other)
410                 {
411                     Debug.Assert(null != other, "other must not be null");
412                     Debug.Assert(this.Equals(other), "other must be another instance of the same relationship target");
413                     Debug.Assert(Object.ReferenceEquals(other._equivalenceSetLinkedListNext, other), "other must not be part of an equivalence set yet");
414                     DirectionalRelationship currentSuccessor = this._equivalenceSetLinkedListNext;
415                     this._equivalenceSetLinkedListNext = other;
416                     other._equivalenceSetLinkedListNext = currentSuccessor;
417                 }
418 
419                 /// <summary>
420                 /// Returns all relationships in equivalence set.
421                 /// </summary>
GetEquivalenceSet()422                 internal IEnumerable<DirectionalRelationship> GetEquivalenceSet()
423                 {
424                     // yield everything in circular linked list
425                     DirectionalRelationship current = this;
426                     do
427                     {
428                         yield return current;
429                         current = current._equivalenceSetLinkedListNext;
430                     }
431                     while (!object.ReferenceEquals(current, this));
432                 }
433 
434                 /// <summary>
435                 /// Determines the number of add and delete operations contained in this equivalence set.
436                 /// </summary>
GetCountsInEquivalenceSet(out int addedCount, out int deletedCount)437                 internal void GetCountsInEquivalenceSet(out int addedCount, out int deletedCount)
438                 {
439                     addedCount = 0;
440                     deletedCount = 0;
441                     // yield everything in circular linked list
442                     DirectionalRelationship current = this;
443                     do
444                     {
445                         if (current.StateEntry.State == EntityState.Added)
446                         {
447                             addedCount++;
448                         }
449                         else if (current.StateEntry.State == EntityState.Deleted)
450                         {
451                             deletedCount++;
452                         }
453                         current = current._equivalenceSetLinkedListNext;
454                     }
455                     while (!object.ReferenceEquals(current, this));
456                 }
457 
GetHashCode()458                 public override int GetHashCode()
459                 {
460                     return _hashCode;
461                 }
462 
Equals(DirectionalRelationship other)463                 public bool Equals(DirectionalRelationship other)
464                 {
465                     if (object.ReferenceEquals(this, other)) { return true; }
466                     if (null == other) { return false; }
467                     if (ToEntityKey != other.ToEntityKey) { return false; }
468                     if (AssociationSet != other.AssociationSet) { return false; }
469                     if (ToEnd != other.ToEnd) { return false; }
470                     if (FromEnd != other.FromEnd) { return false; }
471                     return true;
472                 }
473 
Equals(object obj)474                 public override bool Equals(object obj)
475                 {
476                     Debug.Fail("use only typed Equals method");
477                     return Equals(obj as DirectionalRelationship);
478                 }
479 
ToString()480                 public override string ToString()
481                 {
482                     return String.Format(CultureInfo.InvariantCulture, "{0}.{1}-->{2}: {3}",
483                         AssociationSet.Name, FromEnd.Name, ToEnd.Name, StringUtil.BuildDelimitedList(ToEntityKey.EntityKeyValues, null, null));
484                 }
485             }
486             #endregion
487         }
488     }
489 }
490