1 //--------------------------------------------------------------------- 2 // <copyright file="RelationshipManager.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 // 6 // @owner Microsoft 7 // @backupOwner Microsoft 8 //--------------------------------------------------------------------- 9 namespace System.Data.Objects.DataClasses 10 { 11 using System.Collections; 12 using System.Collections.Generic; 13 using System.ComponentModel; 14 using System.Data.Common.Utils; 15 using System.Data.Mapping; 16 using System.Data.Metadata.Edm; 17 using System.Data.Objects.Internal; 18 using System.Diagnostics; 19 using System.Linq; 20 using System.Runtime.Serialization; 21 22 /// <summary> 23 /// Container for the lazily created relationship navigation 24 /// property objects (collections and refs). 25 /// </summary> 26 [Serializable] 27 public class RelationshipManager 28 { 29 // ------------ 30 // Constructors 31 // ------------ 32 33 // This method is private in order to force all creation of this 34 // object to occur through the public static Create method. 35 // See comments on that method for more details. RelationshipManager()36 private RelationshipManager() 37 { 38 } 39 40 // ------ 41 // Fields 42 // ------ 43 44 // The following fields are serialized. Adding or removing a serialized field is considered 45 // a breaking change. This includes changing the field type or field name of existing 46 // serialized fields. If you need to make this kind of change, it may be possible, but it 47 // will require some custom serialization/deserialization code. 48 49 // Note that this field should no longer be used directly. Instead, use the _wrappedOwner 50 // field. This field is retained only for compatibility with the serialization format introduced in v1. 51 private IEntityWithRelationships _owner; 52 53 private List<RelatedEnd> _relationships; 54 55 [NonSerialized] 56 private bool _nodeVisited; 57 58 [NonSerialized] 59 private IEntityWrapper _wrappedOwner; 60 61 // ---------- 62 // Properties 63 // ---------- 64 65 /// <summary> 66 /// Returns a defensive copy of all the known relationships. The copy is defensive because 67 /// new items may get added to the collection while the caller is iterating over it. Without 68 /// the copy this would cause an exception for concurrently modifying the collection. 69 /// </summary> 70 internal IEnumerable<RelatedEnd> Relationships 71 { 72 get 73 { 74 EnsureRelationshipsInitialized(); 75 return _relationships.ToArray(); 76 } 77 } 78 79 /// <summary> 80 /// Lazy initialization of the _relationships collection. 81 /// </summary> EnsureRelationshipsInitialized()82 private void EnsureRelationshipsInitialized() 83 { 84 if (null == _relationships) 85 { 86 _relationships = new List<RelatedEnd>(); 87 } 88 } 89 90 /// <summary> 91 /// this flag is used to keep track of nodes which have 92 /// been visited. Currently used for Exclude operation. 93 /// </summary> 94 internal bool NodeVisited 95 { 96 get 97 { 98 return _nodeVisited; 99 } 100 set 101 { 102 _nodeVisited = value; 103 } 104 } 105 106 /// <summary> 107 /// Provides access to the entity that owns this manager in its wrapped form. 108 /// </summary> 109 internal IEntityWrapper WrappedOwner 110 { 111 get 112 { 113 if (_wrappedOwner == null) 114 { 115 _wrappedOwner = EntityWrapperFactory.CreateNewWrapper(_owner, null); 116 } 117 return _wrappedOwner; 118 } 119 } 120 121 // ------- 122 // Methods 123 // ------- 124 125 /// <summary> 126 /// Factory method to create a new RelationshipManager object. 127 /// 128 /// Used by data classes that support relationships. If the change tracker 129 /// requests the RelationshipManager property and the data class does not 130 /// already have a reference to one of these objects, it calls this method 131 /// to create one, then saves a reference to that object. On subsequent accesses 132 /// to that property, the data class should return the saved reference. 133 /// 134 /// The reason for using a factory method instead of a public constructor is to 135 /// emphasize that this is not something you would normally call outside of a data class. 136 /// By requiring that these objects are created via this method, developers should 137 /// give more thought to the operation, and will generally only use it when 138 /// they explicitly need to get an object of this type. It helps define the intended usage. 139 /// </summary> 140 /// <param name="owner">Reference to the entity that is calling this method</param> 141 /// <exception cref="ArgumentNullException"><paramref name="owner"/> is null</exception> 142 /// <returns>A new or existing RelationshipManager for the given entity</returns> Create(IEntityWithRelationships owner)143 public static RelationshipManager Create(IEntityWithRelationships owner) 144 { 145 EntityUtil.CheckArgumentNull(owner, "owner"); 146 RelationshipManager rm = new RelationshipManager(); 147 rm._owner = owner; 148 return rm; 149 } 150 151 /// <summary> 152 /// Factory method that creates a new, uninitialized RelationshipManager. This should only be 153 /// used to create a RelationshipManager for an IEntityWrapper for an entity that does not 154 /// implement IEntityWithRelationships. For entities that implement IEntityWithRelationships, 155 /// the Create(IEntityWithRelationships) method should be used instead. 156 /// </summary> 157 /// <returns>The new RelationshipManager</returns> Create()158 internal static RelationshipManager Create() 159 { 160 return new RelationshipManager(); 161 } 162 163 /// <summary> 164 /// Replaces the existing wrapped owner with one that potentially contains more information, 165 /// such as an entity key. Both must wrap the same entity. 166 /// </summary> SetWrappedOwner(IEntityWrapper wrappedOwner, object expectedOwner)167 internal void SetWrappedOwner(IEntityWrapper wrappedOwner, object expectedOwner) 168 { 169 _wrappedOwner = wrappedOwner; 170 Debug.Assert(_owner != null || !(wrappedOwner.Entity is IEntityWithRelationships), "_owner should only be null if entity is not IEntityWithRelationships"); 171 // We need to check that the RelationshipManager created by the entity has the correct owner set, 172 // since the entity can pass any value into RelationshipManager.Create(). 173 if (_owner != null && !Object.ReferenceEquals(expectedOwner, _owner)) 174 { 175 throw EntityUtil.InvalidRelationshipManagerOwner(); 176 } 177 178 if (null != _relationships) 179 { 180 // Not using defensive copy here since SetWrappedOwner should not cause change in underlying 181 // _relationships collection. 182 foreach (RelatedEnd relatedEnd in _relationships) 183 { 184 relatedEnd.SetWrappedOwner(wrappedOwner); 185 } 186 } 187 } 188 189 /// <summary> 190 /// Get the collection of entities related to the current entity using the specified 191 /// combination of relationship name, source role name, and target role name 192 /// </summary> 193 /// <typeparam name="TSourceEntity">Type of the entity in the source role (same as the type of this)</typeparam> 194 /// <typeparam name="TTargetEntity">Type of the entity in the target role</typeparam> 195 /// <param name="relationshipName">CSpace-qualified name of the relationship to navigate</param> 196 /// <param name="sourceRoleName">Name of the source role for the navigation. Indicates the direction of navigation across the relationship.</param> 197 /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param> 198 /// <param name="sourcePropertyName">Name of the property on the source of the navigation.</param> 199 /// <param name="targetPropertyName">Name of the property on the target of the navigation.</param> 200 /// <param name="sourceRoleMultiplicity">Multiplicity of the source role. RelationshipMultiplicity.OneToOne and RelationshipMultiplicity.Zero are both 201 /// accepted for a reference end, and RelationshipMultiplicity.Many is accepted for a collection</param> 202 /// <returns>Collection of related entities of type TTargetEntity</returns> 203 internal EntityCollection<TTargetEntity> GetRelatedCollection<TSourceEntity, TTargetEntity>(string relationshipName, 204 string sourceRoleName, string targetRoleName, NavigationPropertyAccessor sourceAccessor, NavigationPropertyAccessor targetAccessor, 205 RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd) 206 where TSourceEntity : class 207 where TTargetEntity : class 208 { 209 EntityCollection<TTargetEntity> collection; 210 RelatedEnd relatedEnd; TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd)211 TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd); 212 213 if (existingRelatedEnd == null) 214 { 215 if (relatedEnd != null) 216 { 217 collection = relatedEnd as EntityCollection<TTargetEntity>; 218 // Because this is a private method that will only be called for target roles that actually have a 219 // multiplicity that works with EntityReference, this should never be null. If the user requests 220 // a collection or reference and it doesn't match the target role multiplicity, it will be detected 221 // in the public GetRelatedCollection<T> or GetRelatedReference<T> 222 Debug.Assert(collection != null, "should never receive anything but an EntityCollection here"); 223 return collection; 224 } 225 else 226 { 227 RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); 228 return CreateRelatedEnd<TSourceEntity, TTargetEntity>(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as EntityCollection<TTargetEntity>; 229 } 230 } 231 else 232 { 233 // There is no need to supress events on the existingRelatedEnd because setting events on a disconnected 234 // EntityCollection is an InvalidOperation 235 Debug.Assert(existingRelatedEnd._onAssociationChanged == null, "Disconnected RelatedEnd had events"); 236 237 if (relatedEnd != null) 238 { 239 Debug.Assert(_relationships != null, "Expected _relationships to be non-null."); 240 _relationships.Remove(relatedEnd); 241 } 242 243 RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); 244 collection = CreateRelatedEnd<TSourceEntity, TTargetEntity>(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as EntityCollection<TTargetEntity>; 245 246 if (collection != null) 247 { 248 bool doCleanup = true; 249 try 250 { 251 RemergeCollections(relatedEnd as EntityCollection<TTargetEntity>, collection); 252 doCleanup = false; 253 } 254 finally 255 { 256 // An error occured so we need to put the previous relatedEnd back into the RelationshipManager 257 if (doCleanup && relatedEnd != null) 258 { 259 Debug.Assert(_relationships != null, "Expected _relationships to be non-null."); 260 _relationships.Remove(collection); 261 _relationships.Add(relatedEnd); 262 } 263 } 264 } 265 return collection; 266 } 267 } 268 269 /// <summary> 270 /// Re-merge items from collection so that relationship fixup is performed. 271 /// Ensure that any items in previous collection are excluded from the re-merge 272 /// </summary> 273 /// <typeparam name="TTargetEntity"></typeparam> 274 /// <param name="previousCollection">The previous EntityCollection containing items that have already had fixup performed</param> 275 /// <param name="collection">The new EntityCollection</param> 276 private void RemergeCollections<TTargetEntity>(EntityCollection<TTargetEntity> previousCollection, 277 EntityCollection<TTargetEntity> collection) 278 where TTargetEntity : class 279 { 280 Debug.Assert(collection != null, "collection is null"); 281 // If there is a previousCollection, we only need to merge the items that are 282 // in the collection but not in the previousCollection 283 // Ensure that all of the items in the previousCollection are already in the new collection 284 285 int relatedEntityCount = 0; 286 287 // We will be modifing the collection's enumerator, so we need to make a copy of it 288 List<IEntityWrapper> tempEntities = new List<IEntityWrapper>(collection.CountInternal); 289 foreach (IEntityWrapper wrappedEntity in collection.GetWrappedEntities()) 290 { 291 tempEntities.Add(wrappedEntity); 292 } 293 294 // Iterate through the entities that require merging 295 // If the previousCollection already contained the entity, no additional work is needed 296 // If the previousCollection did not contain the entity, 297 // then remove it from the collection and re-add it to force relationship fixup 298 foreach (IEntityWrapper wrappedEntity in tempEntities) 299 { 300 bool requiresMerge = true; 301 if (previousCollection != null) 302 { 303 // There is no need to merge and do fixup if the entity was already in the previousCollection because 304 // fixup would have already taken place when it was added to the previousCollection 305 if (previousCollection.ContainsEntity(wrappedEntity)) 306 { 307 relatedEntityCount++; 308 requiresMerge = false; 309 } 310 } 311 312 if (requiresMerge) 313 { 314 // Remove and re-add the item to the collections to force fixup 315 collection.Remove(wrappedEntity, false); 316 collection.Add(wrappedEntity); 317 } 318 } 319 320 // Ensure that all of the items in the previousCollection are already in the new collection 321 if (previousCollection != null && relatedEntityCount != previousCollection.CountInternal) 322 { 323 throw EntityUtil.CannotRemergeCollections(); 324 } 325 } 326 327 /// <summary> 328 /// Get the entity reference of a related entity using the specified 329 /// combination of relationship name, source role name, and target role name 330 /// </summary> 331 /// <param name="relationshipName">CSpace-qualified name of the relationship to navigate</param> 332 /// <param name="sourceRoleName">Name of the source role for the navigation. Indicates the direction of navigation across the relationship.</param> 333 /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param> 334 /// <param name="sourcePropertyName">Name of the property on the source of the navigation.</param> 335 /// <param name="targetPropertyName">Name of the property on the target of the navigation.</param> 336 /// <param name="sourceRoleMultiplicity">Multiplicity of the source role. RelationshipMultiplicity.OneToOne and RelationshipMultiplicity.Zero are both 337 /// accepted for a reference end, and RelationshipMultiplicity.Many is accepted for a collection</param> 338 /// <returns>Reference for related entity of type TTargetEntity</returns> 339 internal EntityReference<TTargetEntity> GetRelatedReference<TSourceEntity, TTargetEntity>(string relationshipName, 340 string sourceRoleName, string targetRoleName, NavigationPropertyAccessor sourceAccessor, NavigationPropertyAccessor targetAccessor, 341 RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd) 342 where TSourceEntity : class 343 where TTargetEntity : class 344 { 345 EntityReference<TTargetEntity> entityRef; 346 RelatedEnd relatedEnd; 347 348 if (TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd)) 349 { 350 entityRef = relatedEnd as EntityReference<TTargetEntity>; 351 // Because this is a private method that will only be called for target roles that actually have a 352 // multiplicity that works with EntityReference, this should never be null. If the user requests 353 // a collection or reference and it doesn't match the target role multiplicity, it will be detected 354 // in the public GetRelatedCollection<T> or GetRelatedReference<T> 355 Debug.Assert(entityRef != null, "should never receive anything but an EntityReference here"); 356 return entityRef; 357 } 358 else 359 { 360 RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); 361 return CreateRelatedEnd<TSourceEntity, TTargetEntity>(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.One, existingRelatedEnd) as EntityReference<TTargetEntity>; 362 } 363 } 364 365 /// <summary> 366 /// Internal version of GetRelatedEnd that works with the o-space navigation property 367 /// name rather than the c-space relationship name and end name. 368 /// </summary> 369 /// <param name="navigationProperty">the name of the property to lookup</param> 370 /// <returns>the related end for the given property</returns> GetRelatedEnd(string navigationProperty, bool throwArgumentException = false)371 internal RelatedEnd GetRelatedEnd(string navigationProperty, bool throwArgumentException = false) 372 { 373 IEntityWrapper wrappedOwner = WrappedOwner; 374 Debug.Assert(wrappedOwner.Entity != null, "Entity is null"); 375 Debug.Assert(wrappedOwner.Context != null, "Context is null"); 376 Debug.Assert(wrappedOwner.Context.MetadataWorkspace != null, "MetadataWorkspace is null"); 377 Debug.Assert(wrappedOwner.Context.Perspective != null, "Perspective is null"); 378 379 EntityType entityType = wrappedOwner.Context.MetadataWorkspace.GetItem<EntityType>(wrappedOwner.IdentityType.FullName, DataSpace.OSpace); 380 EdmMember member; 381 if (!wrappedOwner.Context.Perspective.TryGetMember(entityType, navigationProperty, false, out member) || 382 !(member is NavigationProperty)) 383 { 384 var message = System.Data.Entity.Strings.RelationshipManager_NavigationPropertyNotFound(navigationProperty); 385 throw throwArgumentException ? (Exception)new ArgumentException(message) : (Exception)new InvalidOperationException(message); 386 } 387 NavigationProperty navProp = (NavigationProperty)member; 388 return GetRelatedEndInternal(navProp.RelationshipType.FullName, navProp.ToEndMember.Name); 389 } 390 391 /// <summary> 392 /// Returns either an EntityCollection or EntityReference of the correct type for the specified target role in a relationship 393 /// This is intended to be used in scenarios where the user doesn't have full metadata, including the static type 394 /// information for both ends of the relationship. This metadata is specified in the EdmRelationshipRoleAttribute 395 /// on each entity type in the relationship, so the metadata system can retrieve it based on the supplied relationship 396 /// name and target role name. 397 /// </summary> 398 /// <param name="relationshipName">Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not.</param> 399 /// <param name="targetRoleName">Target role to use to retrieve the other end of relationshipName</param> 400 /// <returns>IRelatedEnd representing the EntityCollection or EntityReference that was retrieved</returns> GetRelatedEnd(string relationshipName, string targetRoleName)401 public IRelatedEnd GetRelatedEnd(string relationshipName, string targetRoleName) 402 { 403 return GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName); 404 } 405 406 // Internal version of GetRelatedEnd which returns the RelatedEnd as a RelatedEnd rather than an IRelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName)407 internal RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName) 408 { 409 EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); 410 EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); 411 412 IEntityWrapper wrappedOwner = WrappedOwner; 413 if (wrappedOwner.Context == null && wrappedOwner.RequiresRelationshipChangeTracking) 414 { 415 throw new InvalidOperationException(System.Data.Entity.Strings.RelationshipManager_CannotGetRelatEndForDetachedPocoEntity); 416 } 417 418 RelatedEnd relatedEnd = null; 419 420 // Try to get the AssociationType from metadata. This will contain all of the ospace metadata for this relationship 421 AssociationType associationType = null; 422 if (!TryGetRelationshipType(wrappedOwner, wrappedOwner.IdentityType, relationshipName, out associationType)) 423 { 424 if (_relationships != null) 425 { 426 // Look for the RelatedEnd in the list that has already been retrieved 427 relatedEnd = (from RelatedEnd end in _relationships 428 where end.RelationshipName == relationshipName && 429 end.TargetRoleName == targetRoleName 430 select end).FirstOrDefault(); 431 } 432 433 if (relatedEnd == null && !EntityProxyFactory.TryGetAssociationTypeFromProxyInfo(wrappedOwner, relationshipName, targetRoleName, out associationType)) 434 { 435 // If the end still cannot be found, throw an exception 436 throw UnableToGetMetadata(WrappedOwner, relationshipName); 437 } 438 } 439 440 if (relatedEnd == null) 441 { 442 Debug.Assert(associationType != null, "associationType is null"); 443 relatedEnd = GetRelatedEndInternal(relationshipName, targetRoleName, /*existingRelatedEnd*/ null, associationType); 444 } 445 446 return relatedEnd; 447 } 448 GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship)449 private RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship) 450 { 451 return GetRelatedEndInternal(relationshipName, targetRoleName, existingRelatedEnd, relationship, true); 452 } 453 GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship, bool throwOnError)454 private RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship, bool throwOnError) 455 { 456 Debug.Assert(relationshipName != null, "null relationshipNameFromUser"); 457 Debug.Assert(targetRoleName != null, "null targetRoleName"); 458 // existingRelatedEnd can be null if we are not trying to initialize an existing end 459 Debug.Assert(relationship != null, "null relationshipType"); 460 461 AssociationEndMember sourceEnd; 462 AssociationEndMember targetEnd; 463 Debug.Assert(relationship.AssociationEndMembers.Count == 2, "Only 2-way relationships are currently supported"); 464 465 RelatedEnd result = null; 466 467 // There can only be two ends because we don't support n-way relationships -- figure out which end is the target and which is the source 468 // If we want to support n-way relationships in the future, we will need a different overload of GetRelatedEnd that takes the source role name as well 469 targetEnd = relationship.AssociationEndMembers[1]; 470 if (targetEnd.Identity != targetRoleName) 471 { 472 sourceEnd = targetEnd; 473 targetEnd = relationship.AssociationEndMembers[0]; 474 if (targetEnd.Identity != targetRoleName) 475 { 476 if (throwOnError) 477 { 478 throw EntityUtil.InvalidTargetRole(relationshipName, targetRoleName, "targetRoleName"); 479 } 480 else 481 { 482 return result; 483 } 484 } 485 } 486 else 487 { 488 sourceEnd = relationship.AssociationEndMembers[0]; 489 } 490 491 // Validate that the source type matches the type of the owner 492 EntityType sourceEntityType = MetadataHelper.GetEntityTypeForEnd(sourceEnd); 493 Debug.Assert(sourceEntityType.DataSpace == DataSpace.OSpace && sourceEntityType.ClrType != null, "sourceEntityType must contain an ospace type"); 494 Type sourceType = sourceEntityType.ClrType; 495 IEntityWrapper wrappedOwner = WrappedOwner; 496 if (!(sourceType.IsAssignableFrom(wrappedOwner.IdentityType))) 497 { 498 if (throwOnError) 499 { 500 throw EntityUtil.OwnerIsNotSourceType(wrappedOwner.IdentityType.FullName, sourceType.FullName, sourceEnd.Name, relationshipName); 501 } 502 } 503 else if (VerifyRelationship(relationship, sourceEnd.Name, throwOnError)) 504 { 505 // Call a dynamic method that will call either GetRelatedCollection<T, T> or GetRelatedReference<T, T> for this relationship 506 result = LightweightCodeGenerator.GetRelatedEnd(this, sourceEnd, targetEnd, existingRelatedEnd); 507 } 508 return result; 509 } 510 511 /// <summary> 512 /// Takes an existing EntityReference that was created with the default constructor and initializes it using the provided relationship and target role names. 513 /// This method is designed to be used during deserialization only, and will throw an exception if the provided EntityReference has already been initialized, 514 /// if the relationship manager already contains a relationship with this name and target role, or if the relationship manager is already attached to a ObjectContext. 515 /// </summary> 516 /// <typeparam name="TTargetEntity">Type of the entity represented by targetRoleName</typeparam> 517 /// <param name="relationshipName"></param> 518 /// <param name="targetRoleName"></param> 519 /// <param name="entityReference"></param> 520 [Browsable(false)] 521 [EditorBrowsable(EditorBrowsableState.Never)] 522 public void InitializeRelatedReference<TTargetEntity>(string relationshipName, string targetRoleName, EntityReference<TTargetEntity> entityReference) 523 where TTargetEntity : class 524 { EntityUtil.CheckArgumentNull(relationshipName, R)525 EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); EntityUtil.CheckArgumentNull(targetRoleName, R)526 EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); EntityUtil.CheckArgumentNull(entityReference, R)527 EntityUtil.CheckArgumentNull(entityReference, "entityReference"); 528 529 if (entityReference.WrappedOwner.Entity != null) 530 { 531 throw EntityUtil.ReferenceAlreadyInitialized(); 532 } 533 534 IEntityWrapper wrappedOwner = WrappedOwner; 535 if (wrappedOwner.Context != null && wrappedOwner.MergeOption != MergeOption.NoTracking) 536 { 537 throw EntityUtil.RelationshipManagerAttached(); 538 } 539 540 // We need the CSpace-qualified name in order to determine if this relationship already exists, so look it up. 541 // If the relationship doesn't exist, we will use this type information to determine how to initialize the reference 542 relationshipName = PrependNamespaceToRelationshipName(relationshipName); 543 AssociationType relationship = GetRelationshipType(wrappedOwner.IdentityType, relationshipName); 544 545 RelatedEnd relatedEnd; 546 if (TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd)) 547 { 548 // For some serialization scenarios, we have to allow replacing a related end that we already know about, but in those scenarios 549 // the end is always empty, so we can further restrict the user calling method method directly by doing this extra validation 550 if (!relatedEnd.IsEmpty()) 551 { 552 entityReference.InitializeWithValue(relatedEnd); 553 } 554 Debug.Assert(_relationships != null, "Expected _relationships to be non-null."); 555 _relationships.Remove(relatedEnd); 556 } 557 558 EntityReference<TTargetEntity> reference = GetRelatedEndInternal(relationshipName, targetRoleName, entityReference, relationship) as EntityReference<TTargetEntity>; 559 if (reference == null) 560 { 561 throw EntityUtil.ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName); 562 } 563 } 564 565 /// <summary> 566 /// Takes an existing EntityCollection that was created with the default constructor and initializes it using the provided relationship and target role names. 567 /// This method is designed to be used during deserialization only, and will throw an exception if the provided EntityCollection has already been initialized, 568 /// or if the relationship manager is already attached to a ObjectContext. 569 /// </summary> 570 /// <typeparam name="TTargetEntity">Type of the entity represented by targetRoleName</typeparam> 571 /// <param name="relationshipName"></param> 572 /// <param name="targetRoleName"></param> 573 /// <param name="entityCollection"></param> 574 [Browsable(false)] 575 [EditorBrowsable(EditorBrowsableState.Never)] 576 public void InitializeRelatedCollection<TTargetEntity>(string relationshipName, string targetRoleName, EntityCollection<TTargetEntity> entityCollection) 577 where TTargetEntity : class 578 { EntityUtil.CheckArgumentNull(relationshipName, R)579 EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); EntityUtil.CheckArgumentNull(targetRoleName, R)580 EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); EntityUtil.CheckArgumentNull(entityCollection, R)581 EntityUtil.CheckArgumentNull(entityCollection, "entityCollection"); 582 583 if (entityCollection.WrappedOwner.Entity != null) 584 { 585 throw EntityUtil.CollectionAlreadyInitialized(); 586 } 587 588 IEntityWrapper wrappedOwner = WrappedOwner; 589 if (wrappedOwner.Context != null && wrappedOwner.MergeOption != MergeOption.NoTracking) 590 { 591 throw EntityUtil.CollectionRelationshipManagerAttached(); 592 } 593 594 // We need the CSpace-qualified name in order to determine if this relationship already exists, so look it up. 595 // If the relationship doesn't exist, we will use this type information to determine how to initialize the reference 596 relationshipName = PrependNamespaceToRelationshipName(relationshipName); 597 AssociationType relationship = GetRelationshipType(wrappedOwner.IdentityType, relationshipName); 598 599 EntityCollection<TTargetEntity> collection = GetRelatedEndInternal(relationshipName, targetRoleName, entityCollection, relationship) as EntityCollection<TTargetEntity>; 600 if (collection == null) 601 { 602 throw EntityUtil.ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName); 603 } 604 } 605 606 /// <summary> 607 /// Given a relationship name that may or may not be qualified with a namespace name, this method 608 /// attempts to lookup a namespace using the entity type that owns this RelationshipManager as a 609 /// source and adds that namespace to the front of the relationship name. If the namespace 610 /// can't be found, then the relationshipName is returned untouched and the expectation is that 611 /// other validations will fail later in the code paths that use this. 612 /// This method should only be used at the imediate top-level public surface since all internal 613 /// calls are expected to use fully qualified names already. 614 /// </summary> PrependNamespaceToRelationshipName(string relationshipName)615 private string PrependNamespaceToRelationshipName(string relationshipName) 616 { 617 EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); 618 619 if (!relationshipName.Contains('.')) 620 { 621 string identityName = WrappedOwner.IdentityType.FullName; 622 ObjectItemCollection objectItemCollection = GetObjectItemCollection(WrappedOwner); 623 EdmType entityType = null; 624 if (objectItemCollection != null) 625 { 626 objectItemCollection.TryGetItem<EdmType>(identityName, out entityType); 627 } 628 else 629 { 630 Dictionary<string, EdmType> types = ObjectItemCollection.LoadTypesExpensiveWay(WrappedOwner.IdentityType.Assembly); 631 if (types != null) 632 { 633 types.TryGetValue(identityName, out entityType); 634 } 635 636 } 637 ClrEntityType clrEntityType = entityType as ClrEntityType; 638 if (clrEntityType != null) 639 { 640 string ns = clrEntityType.CSpaceNamespaceName; 641 Debug.Assert(!string.IsNullOrEmpty(ns), "Expected non-empty namespace for type."); 642 643 return ns + "." + relationshipName; 644 } 645 } 646 return relationshipName; 647 } 648 649 /// <summary> 650 /// Trys to get an ObjectItemCollection and returns null if it can;t be found. 651 /// </summary> GetObjectItemCollection(IEntityWrapper wrappedOwner)652 private static ObjectItemCollection GetObjectItemCollection(IEntityWrapper wrappedOwner) 653 { 654 if (wrappedOwner.Context != null && wrappedOwner.Context.MetadataWorkspace != null) 655 { 656 return (ObjectItemCollection)wrappedOwner.Context.MetadataWorkspace.GetItemCollection(DataSpace.OSpace); 657 } 658 return null; 659 } 660 661 /// <summary> 662 /// Trys to get the EntityType metadata and returns false if it can't be found. 663 /// </summary> TryGetOwnerEntityType(out EntityType entityType)664 private bool TryGetOwnerEntityType(out EntityType entityType) 665 { 666 DefaultObjectMappingItemCollection mappings; 667 Map map; 668 if (TryGetObjectMappingItemCollection(WrappedOwner, out mappings) && 669 mappings.TryGetMap(WrappedOwner.IdentityType.FullName, DataSpace.OSpace, out map)) 670 { 671 ObjectTypeMapping objectMap = (ObjectTypeMapping)map; 672 if (Helper.IsEntityType(objectMap.EdmType)) 673 { 674 entityType = (EntityType)objectMap.EdmType; 675 return true; 676 } 677 } 678 679 entityType = null; 680 return false; 681 } 682 683 /// <summary> 684 /// Trys to get an DefaultObjectMappingItemCollection and returns false if it can't be found. 685 /// </summary> TryGetObjectMappingItemCollection(IEntityWrapper wrappedOwner, out DefaultObjectMappingItemCollection collection)686 private static bool TryGetObjectMappingItemCollection(IEntityWrapper wrappedOwner, out DefaultObjectMappingItemCollection collection) 687 { 688 if (wrappedOwner.Context != null && wrappedOwner.Context.MetadataWorkspace != null) 689 { 690 collection = (DefaultObjectMappingItemCollection)wrappedOwner.Context.MetadataWorkspace.GetItemCollection(DataSpace.OCSpace); 691 return collection != null; 692 } 693 694 collection = null; 695 return false; 696 } 697 698 699 TryGetRelationshipType(IEntityWrapper wrappedOwner, Type entityClrType, string relationshipName, out AssociationType associationType)700 internal static bool TryGetRelationshipType(IEntityWrapper wrappedOwner, Type entityClrType, string relationshipName, out AssociationType associationType) 701 { 702 ObjectItemCollection objectItemCollection = GetObjectItemCollection(wrappedOwner); 703 if (objectItemCollection != null) 704 { 705 associationType = objectItemCollection.GetRelationshipType(entityClrType, relationshipName); 706 } 707 else 708 { 709 associationType = ObjectItemCollection.GetRelationshipTypeExpensiveWay(entityClrType, relationshipName); 710 } 711 712 return (associationType != null); 713 } 714 GetRelationshipType(Type entityClrType, string relationshipName)715 private AssociationType GetRelationshipType(Type entityClrType, string relationshipName) 716 { 717 AssociationType associationType = null; 718 if (!TryGetRelationshipType(WrappedOwner, entityClrType, relationshipName, out associationType)) 719 { 720 throw UnableToGetMetadata(WrappedOwner, relationshipName); 721 } 722 return associationType; 723 } 724 UnableToGetMetadata(IEntityWrapper wrappedOwner, string relationshipName)725 internal static Exception UnableToGetMetadata(IEntityWrapper wrappedOwner, string relationshipName) 726 { 727 ArgumentException argException = EntityUtil.UnableToFindRelationshipTypeInMetadata(relationshipName, "relationshipName"); 728 if (EntityProxyFactory.IsProxyType(wrappedOwner.Entity.GetType())) 729 { 730 return EntityUtil.ProxyMetadataIsUnavailable(wrappedOwner.IdentityType, argException); 731 } 732 else 733 { 734 return argException; 735 } 736 } 737 GetAllTargetEnds(EntityType ownerEntityType, EntitySet ownerEntitySet)738 private IEnumerable<AssociationEndMember> GetAllTargetEnds(EntityType ownerEntityType, EntitySet ownerEntitySet) 739 { 740 foreach (AssociationSet assocSet in MetadataHelper.GetAssociationsForEntitySet(ownerEntitySet)) 741 { 742 EntityType end2EntityType = ((AssociationType)assocSet.ElementType).AssociationEndMembers[1].GetEntityType(); 743 if (end2EntityType.IsAssignableFrom(ownerEntityType)) 744 { 745 yield return ((AssociationType)assocSet.ElementType).AssociationEndMembers[0]; 746 } 747 // not "else" because of associations between the same entity sets 748 EntityType end1EntityType = ((AssociationType)assocSet.ElementType).AssociationEndMembers[0].GetEntityType(); 749 if (end1EntityType.IsAssignableFrom(ownerEntityType)) 750 { 751 yield return ((AssociationType)assocSet.ElementType).AssociationEndMembers[1]; 752 } 753 } 754 } 755 756 /// <summary> 757 /// Retrieves the AssociationEndMembers that corespond to the target end of a relationship 758 /// given a specific CLR type that exists on the source end of a relationship 759 /// Note: this method can be very expensive if this RelationshipManager is not attached to an 760 /// ObjectContext because no OSpace Metadata is available 761 /// </summary> 762 /// <param name="entityClrType">A CLR type that is on the source role of the relationship</param> 763 /// <returns>The OSpace EntityType that represents this CLR type</returns> GetAllTargetEnds(Type entityClrType)764 private IEnumerable<AssociationEndMember> GetAllTargetEnds(Type entityClrType) 765 { 766 ObjectItemCollection objectItemCollection = GetObjectItemCollection(WrappedOwner); 767 768 IEnumerable<AssociationType> associations = null; 769 if (objectItemCollection != null) 770 { 771 // Metadata is available 772 associations = objectItemCollection.GetItems<AssociationType>(); 773 } 774 else 775 { 776 // No metadata is available, attempt to load the metadata on the fly to retrieve the AssociationTypes 777 associations = ObjectItemCollection.GetAllRelationshipTypesExpensiveWay(entityClrType.Assembly); 778 } 779 780 foreach (AssociationType association in associations) 781 { 782 // Check both ends for the presence of the source CLR type 783 RefType referenceType = association.AssociationEndMembers[0].TypeUsage.EdmType as RefType; 784 if (referenceType != null && referenceType.ElementType.ClrType.IsAssignableFrom(entityClrType)) 785 { 786 // Return the target end 787 yield return association.AssociationEndMembers[1]; 788 } 789 790 referenceType = association.AssociationEndMembers[1].TypeUsage.EdmType as RefType; 791 if (referenceType != null && referenceType.ElementType.ClrType.IsAssignableFrom(entityClrType)) 792 { 793 // Return the target end 794 yield return association.AssociationEndMembers[0]; 795 } 796 } 797 yield break; 798 } 799 800 VerifyRelationship(AssociationType relationship, string sourceEndName, bool throwOnError)801 private bool VerifyRelationship(AssociationType relationship, string sourceEndName, bool throwOnError) 802 { 803 IEntityWrapper wrappedOwner = WrappedOwner; 804 if (wrappedOwner.Context == null) 805 { 806 return true;// if not added to cache, can not decide- for now 807 } 808 809 EntityKey ownerKey = null; 810 ownerKey = wrappedOwner.EntityKey; 811 812 if (null == (object)ownerKey) 813 { 814 return true; // if not added to cache, can not decide- for now 815 } 816 817 TypeUsage associationTypeUsage; 818 AssociationSet association = null; 819 bool isVerified = true; 820 821 // First, get the CSpace association type from the relationship name, since the helper method looks up 822 // association set in the CSpace, since there is no Entity Container in the OSpace 823 if (wrappedOwner.Context.Perspective.TryGetTypeByName(relationship.FullName, false/*ignoreCase*/, out associationTypeUsage)) 824 { 825 //Get the entity container first 826 EntityContainer entityContainer = wrappedOwner.Context.MetadataWorkspace.GetEntityContainer( 827 ownerKey.EntityContainerName, DataSpace.CSpace); 828 EntitySet entitySet; 829 830 // Get the association set from the entity container, given the association type it refers to, and the entity set 831 // name that the source end refers to 832 association = MetadataHelper.GetAssociationsForEntitySetAndAssociationType(entityContainer, ownerKey.EntitySetName, 833 (AssociationType)associationTypeUsage.EdmType, sourceEndName, out entitySet); 834 835 if (association == null) 836 { 837 if (throwOnError) 838 { 839 throw EntityUtil.NoRelationshipSetMatched(relationship.FullName); 840 } 841 else 842 { 843 isVerified = false; 844 } 845 } 846 else 847 { 848 Debug.Assert(association.AssociationSetEnds[sourceEndName].EntitySet == entitySet, "AssociationSetEnd does have the matching EntitySet"); 849 } 850 } 851 return isVerified; 852 } 853 854 /// <summary> 855 /// Get the collection of a related entity using the specified 856 /// combination of relationship name, and target role name. 857 /// Only supports 2-way relationships. 858 /// </summary> 859 /// <param name="relationshipName">Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not.</param> 860 /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param> 861 /// <returns>Collection of entities of type TTargetEntity</returns> 862 public EntityCollection<TTargetEntity> GetRelatedCollection<TTargetEntity>(string relationshipName, string targetRoleName) 863 where TTargetEntity : class 864 { 865 EntityCollection<TTargetEntity> collection = GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as EntityCollection<TTargetEntity>; 866 if (collection == null) 867 { 868 throw EntityUtil.ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName); 869 } 870 return collection; 871 } 872 873 /// <summary> 874 /// Get the entity reference of a related entity using the specified 875 /// combination of relationship name, and target role name. 876 /// Only supports 2-way relationships. 877 /// </summary> 878 /// <param name="relationshipName">Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not.</param> 879 /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param> 880 /// <returns>Reference for related entity of type TTargetEntity</returns> 881 public EntityReference<TTargetEntity> GetRelatedReference<TTargetEntity>(string relationshipName, string targetRoleName) 882 where TTargetEntity : class 883 { 884 EntityReference<TTargetEntity> reference = GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as EntityReference<TTargetEntity>; 885 if (reference == null) 886 { 887 throw EntityUtil.ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName); 888 } 889 return reference; 890 } 891 892 /// <summary> 893 /// Gets collection or ref of related entity for a particular navigation. 894 /// </summary> 895 /// <param name="navigation"> 896 /// Describes the relationship and navigation direction 897 /// </param> 898 /// <param name="relationshipFixer"> 899 /// Encapsulates information about the other end's type and cardinality, 900 /// and knows how to create the other end 901 /// </param> 902 /// <returns></returns> GetRelatedEnd(RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)903 internal RelatedEnd GetRelatedEnd(RelationshipNavigation navigation, IRelationshipFixer relationshipFixer) 904 { 905 RelatedEnd relatedEnd; 906 907 if (TryGetCachedRelatedEnd(navigation.RelationshipName, navigation.To, out relatedEnd)) 908 { 909 return relatedEnd; 910 } 911 else 912 { 913 relatedEnd = relationshipFixer.CreateSourceEnd(navigation, this); 914 Debug.Assert(null != relatedEnd, "CreateSourceEnd should always return a valid RelatedEnd"); 915 916 return relatedEnd; 917 } 918 } 919 920 /// <summary> 921 /// Factory method for creating new related ends 922 /// </summary> 923 /// <typeparam name="TSourceEntity">Type of the source end</typeparam> 924 /// <typeparam name="TTargetEntity">Type of the target end</typeparam> 925 /// <param name="navigation">RelationshipNavigation to be set on the new RelatedEnd</param> 926 /// <param name="sourceRoleMultiplicity">Multiplicity of the source role</param> 927 /// <param name="targetRoleMultiplicity">Multiplicity of the target role</param> 928 /// <param name="existingRelatedEnd">An existing related end to initialize instead of creating a new one</param> 929 /// <returns>new EntityCollection or EntityReference, depending on the specified target multiplicity</returns> 930 internal RelatedEnd CreateRelatedEnd<TSourceEntity, TTargetEntity>(RelationshipNavigation navigation, RelationshipMultiplicity sourceRoleMultiplicity, RelationshipMultiplicity targetRoleMultiplicity, RelatedEnd existingRelatedEnd) 931 where TSourceEntity : class 932 where TTargetEntity : class 933 { 934 IRelationshipFixer relationshipFixer = new RelationshipFixer<TSourceEntity, TTargetEntity>(sourceRoleMultiplicity, targetRoleMultiplicity); 935 RelatedEnd relatedEnd = null; 936 IEntityWrapper wrappedOwner = WrappedOwner; 937 switch (targetRoleMultiplicity) 938 { 939 case RelationshipMultiplicity.ZeroOrOne: 940 case RelationshipMultiplicity.One: 941 if (existingRelatedEnd != null) 942 { 943 Debug.Assert(wrappedOwner.Context == null || wrappedOwner.MergeOption == MergeOption.NoTracking, "Expected null context when initializing an existing related end"); 944 existingRelatedEnd.InitializeRelatedEnd(wrappedOwner, navigation, relationshipFixer); 945 relatedEnd = existingRelatedEnd; 946 } 947 else 948 { 949 relatedEnd = new EntityReference<TTargetEntity>(wrappedOwner, navigation, relationshipFixer); 950 } 951 break; 952 case RelationshipMultiplicity.Many: 953 if (existingRelatedEnd != null) 954 { 955 Debug.Assert(wrappedOwner.Context == null || wrappedOwner.MergeOption == MergeOption.NoTracking, "Expected null context or NoTracking when initializing an existing related end"); 956 existingRelatedEnd.InitializeRelatedEnd(wrappedOwner, navigation, relationshipFixer); 957 relatedEnd = existingRelatedEnd; 958 } 959 else 960 { 961 relatedEnd = new EntityCollection<TTargetEntity>(wrappedOwner, navigation, relationshipFixer); 962 } 963 break; 964 default: 965 throw EntityUtil.InvalidEnumerationValue(typeof(RelationshipMultiplicity), (int)targetRoleMultiplicity); 966 } 967 968 // Verify that we can attach the context successfully before adding to our list of relationships 969 if (wrappedOwner.Context != null) 970 { 971 relatedEnd.AttachContext(wrappedOwner.Context, wrappedOwner.MergeOption); 972 } 973 EnsureRelationshipsInitialized()974 EnsureRelationshipsInitialized(); _relationships.Add(relatedEnd)975 _relationships.Add(relatedEnd); 976 977 return relatedEnd; 978 } 979 980 /// <summary> 981 /// Returns an enumeration of all the related ends. The enumeration 982 /// will be empty if the relationships have not been populated. 983 /// </summary> GetAllRelatedEnds()984 public IEnumerable<IRelatedEnd> GetAllRelatedEnds() 985 { 986 IEntityWrapper wrappedOwner = WrappedOwner; 987 988 EntityType entityType; 989 if (wrappedOwner.Context != null && wrappedOwner.Context.MetadataWorkspace != null && TryGetOwnerEntityType(out entityType)) 990 { 991 // For attached scenario: 992 // MEST: This returns RelatedEnds representing AssociationTypes which belongs to AssociationSets 993 // which have one end of EntitySet of wrappedOwner.Entity's EntitySet 994 Debug.Assert(wrappedOwner.EntityKey != null, "null entityKey on a attached entity"); 995 EntitySet entitySet = wrappedOwner.Context.GetEntitySet(wrappedOwner.EntityKey.EntitySetName, wrappedOwner.EntityKey.EntityContainerName); 996 foreach (AssociationEndMember endMember in GetAllTargetEnds(entityType, entitySet)) 997 { 998 yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name); 999 } 1000 } 1001 else 1002 { 1003 // Disconnected scenario 1004 // MEST: this returns RelatedEnds representing all AssociationTypes which have one end of type of wrappedOwner.Entity's type. 1005 // The returned collection of RelatedEnds is a superset of RelatedEnds which can make sense for a single entity, because 1006 // an entity can belong only to one EntitySet. Note that the ideal would be to return the same collection as for attached scenario, 1007 // but it's not possible because we don't know to which EntitySet the wrappedOwner.Entity belongs. 1008 if (wrappedOwner.Entity != null) 1009 { 1010 foreach (AssociationEndMember endMember in GetAllTargetEnds(wrappedOwner.IdentityType)) 1011 { 1012 yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name); 1013 } 1014 } 1015 } 1016 yield break; 1017 } 1018 1019 [EditorBrowsable(EditorBrowsableState.Never)] 1020 [Browsable(false)] 1021 [OnSerializingAttribute] OnSerializing(StreamingContext context)1022 public void OnSerializing(StreamingContext context) 1023 { 1024 IEntityWrapper wrappedOwner = WrappedOwner; 1025 if (!(wrappedOwner.Entity is IEntityWithRelationships)) 1026 { 1027 throw new InvalidOperationException(System.Data.Entity.Strings.RelatedEnd_CannotSerialize("RelationshipManager")); 1028 } 1029 // If we are attached to a context we need to go fixup the detached entity key on any EntityReferences 1030 if (wrappedOwner.Context != null && wrappedOwner.MergeOption != MergeOption.NoTracking) 1031 { 1032 foreach (RelatedEnd relatedEnd in GetAllRelatedEnds()) 1033 { 1034 EntityReference reference = relatedEnd as EntityReference; 1035 if (reference != null && reference.EntityKey != null) 1036 { 1037 reference.DetachedEntityKey = reference.EntityKey; 1038 } 1039 } 1040 } 1041 } 1042 1043 // ---------------- 1044 // Internal Methods 1045 // ---------------- 1046 1047 internal bool HasRelationships 1048 { 1049 get { return _relationships != null; } 1050 } 1051 1052 /// <summary> 1053 /// Add the rest of the graph, attached to this owner, to ObjectStateManager 1054 /// </summary> 1055 /// <param name="doAttach">if TRUE, the rest of the graph is attached directly as Unchanged 1056 /// without calling AcceptChanges()</param> AddRelatedEntitiesToObjectStateManager(bool doAttach)1057 internal void AddRelatedEntitiesToObjectStateManager(bool doAttach) 1058 { 1059 if (null != _relationships) 1060 { 1061 bool doCleanup = true; 1062 try 1063 { 1064 // Create a copy of this list because with self references, the set of relationships can change 1065 foreach (RelatedEnd relatedEnd in Relationships) 1066 { 1067 relatedEnd.Include(/*addRelationshipAsUnchanged*/false, doAttach); 1068 } 1069 doCleanup = false; 1070 } 1071 finally 1072 { 1073 // If error happens, while attaching entity graph to context, clean-up 1074 // is done on the Owner entity and all its relating entities. 1075 if (doCleanup) 1076 { 1077 IEntityWrapper wrappedOwner = WrappedOwner; 1078 Debug.Assert(wrappedOwner.Context != null && wrappedOwner.Context.ObjectStateManager != null, "Null context or ObjectStateManager"); 1079 1080 TransactionManager transManager = wrappedOwner.Context.ObjectStateManager.TransactionManager; 1081 1082 // The graph being attached is connected to graph already existing in the OSM only through "promoted" relationships 1083 // (relationships which originally existed only in OSM between key entries and entity entries but later were 1084 // "promoted" to normal relationships in EntityRef/Collection when the key entries were promoted). 1085 // The cleanup code traverse all the graph being added to the OSM, so we have to disconnect it from the graph already 1086 // existing in the OSM by degrading promoted relationships. 1087 wrappedOwner.Context.ObjectStateManager.DegradePromotedRelationships(); 1088 1089 NodeVisited = true; 1090 RemoveRelatedEntitiesFromObjectStateManager(wrappedOwner); 1091 1092 EntityEntry entry; 1093 1094 Debug.Assert(doAttach == (transManager.IsAttachTracking), "In attach the recovery collection should be not null"); 1095 1096 if (transManager.IsAttachTracking && 1097 transManager.PromotedKeyEntries.TryGetValue(wrappedOwner.Entity, out entry)) 1098 { 1099 // This is executed only in the cleanup code from ObjectContext.AttachTo() 1100 // If the entry was promoted in AttachTo(), it has to be degraded now instead of being deleted. 1101 entry.DegradeEntry(); 1102 } 1103 else 1104 { 1105 RelatedEnd.RemoveEntityFromObjectStateManager(wrappedOwner); 1106 } 1107 } 1108 } 1109 } 1110 } 1111 1112 // Method is used to remove all entities and relationships, of a given entity 1113 // graph, from ObjectStateManager. This method is used when adding entity graph, 1114 // or a portion of it, raise exception. RemoveRelatedEntitiesFromObjectStateManager(IEntityWrapper wrappedEntity)1115 internal static void RemoveRelatedEntitiesFromObjectStateManager(IEntityWrapper wrappedEntity) 1116 { 1117 Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); 1118 foreach (RelatedEnd relatedEnd in wrappedEntity.RelationshipManager.Relationships) 1119 { 1120 // only some of the related ends may have gotten attached, so just skip the ones that weren't 1121 if (relatedEnd.ObjectContext != null) 1122 { 1123 Debug.Assert(!relatedEnd.UsingNoTracking, "Shouldn't be touching the state manager with entities that were retrieved with NoTracking"); 1124 relatedEnd.Exclude(); 1125 relatedEnd.DetachContext(); 1126 } 1127 } 1128 } 1129 1130 // Remove entity from its relationships and do cascade delete if required. 1131 // All removed relationships are marked for deletion and all cascade deleted 1132 // entitites are also marked for deletion. RemoveEntityFromRelationships()1133 internal void RemoveEntityFromRelationships() 1134 { 1135 if (null != _relationships) 1136 { 1137 foreach (RelatedEnd relatedEnd in Relationships) 1138 { 1139 relatedEnd.RemoveAll(); 1140 } 1141 } 1142 } 1143 1144 /// <summary> 1145 /// Traverse the relationships and find all the dependent ends that contain FKs, then attempt 1146 /// to null all of those FKs. 1147 /// </summary> NullAllFKsInDependentsForWhichThisIsThePrincipal()1148 internal void NullAllFKsInDependentsForWhichThisIsThePrincipal() 1149 { 1150 if (_relationships != null) 1151 { 1152 // Build a list of the dependent RelatedEnds because with overlapping FKs we could 1153 // end up removing a relationship before we have suceeded in nulling all the FK values 1154 // for that relationship. 1155 var dependentEndsToProcess = new List<EntityReference>(); 1156 foreach (RelatedEnd relatedEnd in Relationships) 1157 { 1158 if (relatedEnd.IsForeignKey) 1159 { 1160 foreach (IEntityWrapper dependent in relatedEnd.GetWrappedEntities()) 1161 { 1162 var dependentEnd = relatedEnd.GetOtherEndOfRelationship(dependent); 1163 if (dependentEnd.IsDependentEndOfReferentialConstraint(checkIdentifying: false)) 1164 { 1165 Debug.Assert(dependentEnd is EntityReference, "Dependent end in FK relationship should always be a reference."); 1166 dependentEndsToProcess.Add((EntityReference)dependentEnd); 1167 } 1168 } 1169 } 1170 } 1171 foreach (EntityReference dependentEnd in dependentEndsToProcess) 1172 { 1173 dependentEnd.NullAllForeignKeys(); 1174 } 1175 } 1176 } 1177 1178 // Removes entity from its relationships. 1179 // Relationship entries are removed from ObjectStateManager if owner is in Added state 1180 // or when owner is "many" end of the relationship DetachEntityFromRelationships(EntityState ownerEntityState)1181 internal void DetachEntityFromRelationships(EntityState ownerEntityState) 1182 { 1183 if (null != _relationships) 1184 { 1185 foreach (RelatedEnd relatedEnd in Relationships) 1186 { 1187 relatedEnd.DetachAll(ownerEntityState); 1188 } 1189 } 1190 } 1191 1192 //For a given relationship removes passed in entity from owners relationship RemoveEntity(string toRole, string relationshipName, IEntityWrapper wrappedEntity)1193 internal void RemoveEntity(string toRole, string relationshipName, IEntityWrapper wrappedEntity) 1194 { 1195 Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); 1196 RelatedEnd relatedEnd; 1197 if (TryGetCachedRelatedEnd(relationshipName, toRole, out relatedEnd)) 1198 { 1199 relatedEnd.Remove(wrappedEntity, false); 1200 } 1201 } 1202 ClearRelatedEndWrappers()1203 internal void ClearRelatedEndWrappers() 1204 { 1205 if (_relationships != null) 1206 { 1207 foreach (IRelatedEnd relatedEnd in Relationships) 1208 { 1209 ((RelatedEnd)relatedEnd).ClearWrappedValues(); 1210 } 1211 } 1212 } 1213 1214 // Method used to retrieve properties from principal entities. 1215 // Parameter includeOwnValues means that values from current entity should be also added to "properties" 1216 // includeOwnValues is false only when this method is called from ObjectStateEntry.AcceptChanges() 1217 // Parmeter "visited" is a set containig entities which were already visited during traversing the graph. 1218 // If _owner already exists in the set, it means that there is a cycle in the graph of relationships with RI Constraints. RetrieveReferentialConstraintProperties(out Dictionary<string, KeyValuePair<object, IntBox>> properties, HashSet<object> visited, bool includeOwnValues)1219 internal void RetrieveReferentialConstraintProperties(out Dictionary<string, KeyValuePair<object, IntBox>> properties, HashSet<object> visited, bool includeOwnValues) 1220 { 1221 IEntityWrapper wrappedOwner = WrappedOwner; 1222 Debug.Assert(wrappedOwner.Entity != null); 1223 Debug.Assert(visited != null); 1224 1225 // Dictionary< propertyName, <propertyValue, counter>> 1226 properties = new Dictionary<string, KeyValuePair<object, IntBox>>(); 1227 1228 EntityKey ownerKey = wrappedOwner.EntityKey; 1229 Debug.Assert((object)ownerKey != null); 1230 1231 // If the key is temporary, get values of referential constraint properties from principal entities 1232 if (ownerKey.IsTemporary) 1233 { 1234 // Find property names which should be retrieved 1235 List<string> propertiesToRetrieve; 1236 bool propertiesToPropagateExist; // not used 1237 1238 this.FindNamesOfReferentialConstraintProperties(out propertiesToRetrieve, out propertiesToPropagateExist, skipFK: false); 1239 1240 if (propertiesToRetrieve != null) 1241 { 1242 // At first try to retrieve properties from entities which are in collections or references. 1243 // This is the most common scenario. 1244 // Only if properties couldn't be retrieved this way, try to retrieve properties from related stubs. 1245 1246 if (_relationships != null) 1247 { 1248 // Not using defensive copy here since RetrieveReferentialConstraintProperties should not cause change in underlying 1249 // _relationships collection. 1250 foreach (RelatedEnd relatedEnd in _relationships) 1251 { 1252 // NOTE: If the following call throws UnableToRetrieveReferentialConstraintProperties, 1253 // it means that properties couldn't be found in indirectly related entities, 1254 // so it doesn't make sense to search for properties in directly related stubs, 1255 // so exception is not being caught here. 1256 relatedEnd.RetrieveReferentialConstraintProperties(properties, visited); 1257 } 1258 } 1259 1260 // Check if all properties were retrieved. 1261 // There are 3 scenarios in which not every expected property can be retrieved: 1262 // 1. There is no related entity from which the property is supposed to be retrieved. 1263 // 2. Related entity which supposed to contains the property doesn't have fixed entity key. 1264 // 3. Property should be retrieved from related key entry 1265 1266 if (!CheckIfAllPropertiesWereRetrieved(properties, propertiesToRetrieve)) 1267 { 1268 // Properties couldn't be found in entities in collections or refrences. 1269 // Try to find missing properties in related key entries. 1270 // This process is slow but it is not a common case. 1271 EntityEntry entry = wrappedOwner.Context.ObjectStateManager.FindEntityEntry(ownerKey); 1272 Debug.Assert(entry != null, "Owner entry not found in the object state manager"); 1273 entry.RetrieveReferentialConstraintPropertiesFromKeyEntries(properties); 1274 1275 // Check again if all properties were retrieved. 1276 if (!CheckIfAllPropertiesWereRetrieved(properties, propertiesToRetrieve)) 1277 { 1278 throw EntityUtil.UnableToRetrieveReferentialConstraintProperties(); 1279 } 1280 } 1281 } 1282 } 1283 1284 // 1. If key is temporary, properties from principal entities were retrieved above. 1285 // The other key properties are properties which are not Dependent end of some Referential Constraint. 1286 // 2. If key is not temporary and this method was not called from AcceptChanges() - all key values 1287 // of the current entity are added to 'properties'. 1288 if (!ownerKey.IsTemporary || includeOwnValues) 1289 { 1290 // NOTE this part is never executed when the method is called from ObjectStateManager.AcceptChanges(), 1291 // so we don't try to "retrieve" properties from the the same (callers) entity. 1292 EntityEntry entry = wrappedOwner.Context.ObjectStateManager.FindEntityEntry(ownerKey); 1293 Debug.Assert(entry != null, "Owner entry not found in the object state manager"); 1294 entry.GetOtherKeyProperties(properties); 1295 } 1296 } 1297 1298 // properties dictionary contains name of property, its value and coutner saying how many times this property was retrieved from principal entities CheckIfAllPropertiesWereRetrieved(Dictionary<string, KeyValuePair<object, IntBox>> properties, List<string> propertiesToRetrieve)1299 private static bool CheckIfAllPropertiesWereRetrieved(Dictionary<string, KeyValuePair<object, IntBox>> properties, List<string> propertiesToRetrieve) 1300 { 1301 Debug.Assert(properties != null); 1302 Debug.Assert(propertiesToRetrieve != null); 1303 1304 bool isSuccess = true; 1305 1306 List<int> countersCopy = new List<int>(); 1307 ICollection<KeyValuePair<object, IntBox>> values = properties.Values; 1308 1309 // Create copy of counters (needed in case of failure) 1310 foreach (KeyValuePair<object, IntBox> valueCounterPair in values) 1311 { 1312 countersCopy.Add(valueCounterPair.Value.Value); 1313 } 1314 1315 foreach (string name in propertiesToRetrieve) 1316 { 1317 if (!properties.ContainsKey(name)) 1318 { 1319 isSuccess = false; 1320 break; 1321 } 1322 1323 KeyValuePair<object, IntBox> valueCounterPair = properties[name]; 1324 valueCounterPair.Value.Value = valueCounterPair.Value.Value - 1; 1325 if (valueCounterPair.Value.Value < 0) 1326 { 1327 isSuccess = false; 1328 break; 1329 } 1330 } 1331 1332 // Check if all the coutners equal 0 1333 if (isSuccess) 1334 { 1335 foreach (KeyValuePair<object, IntBox> valueCounterPair in values) 1336 { 1337 if (valueCounterPair.Value.Value != 0) 1338 { 1339 isSuccess = false; 1340 break; 1341 } 1342 } 1343 } 1344 1345 // Restore counters in case of failure 1346 if (!isSuccess) 1347 { 1348 IEnumerator<int> enumerator = countersCopy.GetEnumerator(); 1349 foreach (KeyValuePair<object, IntBox> valueCounterPair in values) 1350 { 1351 enumerator.MoveNext(); 1352 valueCounterPair.Value.Value = enumerator.Current; 1353 } 1354 } 1355 1356 return isSuccess; 1357 } 1358 1359 1360 // Check consistency between properties of current entity and Principal entities 1361 // If some of Principal entities don't exist or some property cannot be checked - this is violation of RI Constraints CheckReferentialConstraintProperties(EntityEntry ownerEntry)1362 internal void CheckReferentialConstraintProperties(EntityEntry ownerEntry) 1363 { 1364 Debug.Assert(ownerEntry != null); 1365 1366 List<string> propertiesToRetrieve; // used to check if the owner is a dependent end of some RI Constraint 1367 bool propertiesToPropagateExist; // used to check if the owner is a principal end of some RI Constraint 1368 this.FindNamesOfReferentialConstraintProperties(out propertiesToRetrieve, out propertiesToPropagateExist, skipFK: false); 1369 1370 if ((propertiesToRetrieve != null || propertiesToPropagateExist) && 1371 _relationships != null) 1372 { 1373 // Not using defensive copy here since CheckReferentialConstraintProperties should not cause change in underlying 1374 // _relationships collection. 1375 foreach (RelatedEnd relatedEnd in _relationships) 1376 { 1377 if (!relatedEnd.CheckReferentialConstraintProperties(ownerEntry)) 1378 { 1379 throw EntityUtil.InconsistentReferentialConstraintProperties(); 1380 } 1381 } 1382 } 1383 } 1384 1385 // ---------------- 1386 // Private Methods 1387 // ---------------- 1388 1389 // This method is required to maintain compatibility with the v1 binary serialization format. 1390 // In particular, it recreates a entity wrapper from the serialized owner. 1391 // Note that this is only expected to work for non-POCO entities, since serialization of POCO 1392 // entities will not result in serialization of the RelationshipManager or its related objects. 1393 [EditorBrowsable(EditorBrowsableState.Never)] 1394 [Browsable(false)] 1395 [OnDeserialized()] OnDeserialized(StreamingContext context)1396 public void OnDeserialized(StreamingContext context) 1397 { 1398 // Note that when deserializing, the context is always null since we never serialize 1399 // the context with the entity. 1400 _wrappedOwner = EntityWrapperFactory.WrapEntityUsingContext(_owner, null); 1401 } 1402 1403 /// <summary> 1404 /// Searches the list of relationships for an entry with the specified relationship name and role names 1405 /// </summary> 1406 /// <param name="relationshipName">CSpace-qualified name of the relationship</param> 1407 /// <param name="targetRoleName">name of the target role</param> 1408 /// <param name="relatedEnd">the RelatedEnd if found, otherwise null</param> 1409 /// <returns>true if the entry found, false otherwise</returns> TryGetCachedRelatedEnd(string relationshipName, string targetRoleName, out RelatedEnd relatedEnd)1410 private bool TryGetCachedRelatedEnd(string relationshipName, string targetRoleName, out RelatedEnd relatedEnd) 1411 { 1412 relatedEnd = null; 1413 if (null != _relationships) 1414 { 1415 // Not using defensive copy here since loop should not cause change in underlying 1416 // _relationships collection. 1417 foreach (RelatedEnd end in _relationships) 1418 { 1419 RelationshipNavigation relNav = end.RelationshipNavigation; 1420 if (relNav.RelationshipName == relationshipName && relNav.To == targetRoleName) 1421 { 1422 relatedEnd = end; 1423 return true; 1424 } 1425 } 1426 } 1427 return false; 1428 } 1429 1430 // Find properties which are Dependent/Principal ends of some referential constraint 1431 // Returned lists are never null. 1432 // NOTE This method will be removed when bug 505935 is solved 1433 // Returns true if any FK relationships were skipped so that they can be checked again after fixup FindNamesOfReferentialConstraintProperties(out List<string> propertiesToRetrieve, out bool propertiesToPropagateExist, bool skipFK)1434 internal bool FindNamesOfReferentialConstraintProperties(out List<string> propertiesToRetrieve, out bool propertiesToPropagateExist, bool skipFK) 1435 { 1436 IEntityWrapper wrappedOwner = WrappedOwner; 1437 Debug.Assert(wrappedOwner.Entity != null); 1438 EntityKey ownerKey = wrappedOwner.EntityKey; 1439 EntityUtil.CheckEntityKeyNull(ownerKey); 1440 1441 propertiesToRetrieve = null; 1442 propertiesToPropagateExist = false; 1443 1444 EntityUtil.CheckContextNull(wrappedOwner.Context); 1445 EntitySet entitySet = ownerKey.GetEntitySet(wrappedOwner.Context.MetadataWorkspace); 1446 Debug.Assert(entitySet != null, "Unable to find entity set"); 1447 1448 // Get association types in which current entity's type is one of the ends. 1449 List<AssociationSet> associations = MetadataHelper.GetAssociationsForEntitySet(entitySet); 1450 1451 bool skippedFK = false; 1452 // Find key property names which are part of referential integrity constraints 1453 foreach (AssociationSet association in associations) 1454 { 1455 // NOTE ReferentialConstraints collection currently can contain 0 or 1 element 1456 if (skipFK && association.ElementType.IsForeignKey) 1457 { 1458 skippedFK = true; 1459 } 1460 else 1461 { 1462 foreach (ReferentialConstraint constraint in association.ElementType.ReferentialConstraints) 1463 { 1464 if (constraint.ToRole.TypeUsage.EdmType == entitySet.ElementType.GetReferenceType()) 1465 { 1466 // lazy creation of the list 1467 propertiesToRetrieve = propertiesToRetrieve ?? new List<string>(); 1468 foreach (EdmProperty property in constraint.ToProperties) 1469 { 1470 propertiesToRetrieve.Add(property.Name); 1471 } 1472 } 1473 // There are schemas, in which relationship has the same entitySet on both ends 1474 // that is why following 'if' statement is not inside of 'else' of previous 'if' statement 1475 if (constraint.FromRole.TypeUsage.EdmType == entitySet.ElementType.GetReferenceType()) 1476 { 1477 propertiesToPropagateExist = true; 1478 } 1479 } 1480 } 1481 } 1482 return skippedFK; 1483 } 1484 1485 /// <summary> 1486 /// Helper method to validate consistency of RelationshipManager instances 1487 /// </summary> 1488 /// <param name="entity">entity to compare against</param> 1489 /// <returns>True if entity is the owner of this RelationshipManager, otherwise false</returns> IsOwner(IEntityWrapper wrappedEntity)1490 internal bool IsOwner(IEntityWrapper wrappedEntity) 1491 { 1492 IEntityWrapper wrappedOwner = WrappedOwner; 1493 Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); 1494 return Object.ReferenceEquals(wrappedEntity.Entity, wrappedOwner.Entity); 1495 } 1496 1497 /// <summary> 1498 /// Calls AttachContext on each RelatedEnd referenced by this manager. 1499 /// </summary> AttachContextToRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)1500 internal void AttachContextToRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption) 1501 { 1502 Debug.Assert(null != context, "context"); 1503 Debug.Assert(null != entitySet, "entitySet"); 1504 if (null != _relationships) 1505 { 1506 // If GetAllRelatedEnds was called while the entity was not attached to the context 1507 // then _relationships may contain RelatedEnds that do not belong in based on the 1508 // entity set that the owner ultimately was attached to. This means that when attaching 1509 // we need to trim the list to get rid of those RelatedEnds. 1510 // It is possible that the RelatedEnds may have been obtained explicitly rather than through 1511 // GetAllRelatedEnds. If this is the case, then we prune anyway unless the RelatedEnd actually 1512 // has something attached to it, in which case we try to attach the context which will cause 1513 // an exception to be thrown. This is all a bit messy, but it's the best we could do given that 1514 // GetAllRelatedEnds was implemented in 3.5sp1 without taking MEST into account. 1515 // Note that the Relationships property makes a copy so we can modify the list while iterating 1516 foreach (RelatedEnd relatedEnd in Relationships) 1517 { 1518 EdmType relationshipType; 1519 RelationshipSet relationshipSet; 1520 relatedEnd.FindRelationshipSet(context, entitySet, out relationshipType, out relationshipSet); 1521 if (relationshipSet != null || !relatedEnd.IsEmpty()) 1522 { 1523 relatedEnd.AttachContext(context, entitySet, mergeOption); 1524 } 1525 else 1526 { 1527 _relationships.Remove(relatedEnd); 1528 } 1529 } 1530 } 1531 } 1532 1533 /// <summary> 1534 /// Calls AttachContext on each RelatedEnd referenced by this manager and also on all the enties 1535 /// referenced by that related end. 1536 /// </summary> ResetContextOnRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)1537 internal void ResetContextOnRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption) 1538 { 1539 Debug.Assert(null != context, "context"); 1540 Debug.Assert(null != entitySet, "entitySet"); 1541 if (null != _relationships) 1542 { 1543 foreach (RelatedEnd relatedEnd in Relationships) 1544 { 1545 relatedEnd.AttachContext(context, entitySet, mergeOption); 1546 foreach (IEntityWrapper wrappedEntity in relatedEnd.GetWrappedEntities()) 1547 { 1548 wrappedEntity.ResetContext(context, relatedEnd.GetTargetEntitySetFromRelationshipSet(), mergeOption); 1549 } 1550 } 1551 } 1552 } 1553 1554 /// <summary> 1555 /// Calls DetachContext on each RelatedEnd referenced by this manager. 1556 /// </summary> DetachContextFromRelatedEnds()1557 internal void DetachContextFromRelatedEnds() 1558 { 1559 if (null != _relationships) 1560 { 1561 // Not using defensive copy here since DetachContext should not cause change in underlying 1562 // _relationships collection. 1563 foreach (RelatedEnd relatedEnd in _relationships) 1564 { 1565 relatedEnd.DetachContext(); 1566 } 1567 } 1568 } 1569 1570 // -------------------- 1571 // Internal definitions 1572 // -------------------- 1573 1574 [Conditional("DEBUG")] VerifyIsNotRelated()1575 internal void VerifyIsNotRelated() 1576 { 1577 if (this._relationships != null) 1578 { 1579 foreach (var r in this._relationships) 1580 { 1581 if (!r.IsEmpty()) 1582 { 1583 Debug.Assert(false, "Cannot change a state of a Deleted entity if the entity has other than deleted relationships with other entities."); 1584 } 1585 } 1586 } 1587 } 1588 } 1589 } 1590