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