This repository has been archived by the owner on Nov 29, 2017. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 54
/
ObjectStateManager.cs
4020 lines (3642 loc) · 203 KB
/
ObjectStateManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace System.Data.Entity.Core.Objects
{
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Entity.Core.Common;
using System.Data.Entity.Core.Common.Utils;
using System.Data.Entity.Core.Mapping;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects.DataClasses;
using System.Data.Entity.Core.Objects.Internal;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Resources;
using System.Data.Entity.Utilities;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
/// <summary>
/// Maintains object state and identity management for entity type instances and relationship instances.
/// </summary>
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
public class ObjectStateManager : IEntityStateManager
{
// This is the initial capacity used for lists of entries. We use this rather than the default because
// perf testing showed we were almost always increasing the capacity which can be quite a slow operation.
private const int InitialListSize = 16;
// dictionaries (one for each entity state) that store cache entries that represent entities
// these are only non-null when there is an entity in respective state, must always check for null before using
private Dictionary<EntityKey, EntityEntry> _addedEntityStore;
private Dictionary<EntityKey, EntityEntry> _modifiedEntityStore;
private Dictionary<EntityKey, EntityEntry> _deletedEntityStore;
private Dictionary<EntityKey, EntityEntry> _unchangedEntityStore;
private Dictionary<object, EntityEntry> _keylessEntityStore;
// dictionaries (one for each entity state) that store cache entries that represent relationships
// these are only non-null when there is an relationship in respective state, must always check for null before using
private Dictionary<RelationshipWrapper, RelationshipEntry> _addedRelationshipStore;
private Dictionary<RelationshipWrapper, RelationshipEntry> _deletedRelationshipStore;
private Dictionary<RelationshipWrapper, RelationshipEntry> _unchangedRelationshipStore;
// mapping from EdmType or EntitySetQualifiedType to StateManagerTypeMetadata
private readonly Dictionary<EdmType, StateManagerTypeMetadata> _metadataStore;
private readonly Dictionary<EntitySetQualifiedType, StateManagerTypeMetadata> _metadataMapping;
private readonly MetadataWorkspace _metadataWorkspace;
// delegate for notifying changes in collection
private CollectionChangeEventHandler onObjectStateManagerChangedDelegate;
private CollectionChangeEventHandler onEntityDeletedDelegate;
// Flag to indicate if we are in the middle of relationship fixup.
// This is set and cleared only during ResetEntityKey, because only in that case
// do we allow setting a value on a non-null EntityKey property
private bool _inRelationshipFixup;
private bool _isDisposed;
// materializer instance that can be used to create complex types with just a metadata workspace
private ComplexTypeMaterializer _complexTypeMaterializer;
private readonly Dictionary<EntityKey, HashSet<EntityEntry>> _danglingForeignKeys =
new Dictionary<EntityKey, HashSet<EntityEntry>>();
private HashSet<EntityEntry> _entriesWithConceptualNulls;
private readonly EntityWrapperFactory _entityWrapperFactory;
#region Private Fields for ObjectStateEntry change tracking
private bool _detectChangesNeeded;
#endregion
internal ObjectStateManager()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:System.Data.Entity.Core.Objects.ObjectStateManager" /> class.
/// </summary>
/// <param name="metadataWorkspace">
/// The <see cref="T:System.Data.Entity.Core.Metadata.Edm.MetadataWorkspace" />, which supplies mapping and metadata information.
/// </param>
[CLSCompliant(false)]
public ObjectStateManager(MetadataWorkspace metadataWorkspace)
{
Check.NotNull(metadataWorkspace, "metadataWorkspace");
_metadataWorkspace = metadataWorkspace;
_metadataStore = new Dictionary<EdmType, StateManagerTypeMetadata>();
_metadataMapping = new Dictionary<EntitySetQualifiedType, StateManagerTypeMetadata>(EntitySetQualifiedType.EqualityComparer);
_isDisposed = false;
_entityWrapperFactory = new EntityWrapperFactory();
TransactionManager = new TransactionManager();
}
#region Internal Properties for ObjectStateEntry change tracking
internal virtual object ChangingObject { get; set; }
internal virtual string ChangingEntityMember { get; set; }
internal virtual string ChangingMember { get; set; }
internal virtual EntityState ChangingState { get; set; }
internal virtual bool SaveOriginalValues { get; set; }
internal virtual object ChangingOldValue { get; set; }
// Used by ObjectStateEntry to determine if it's safe to set a value
// on a non-null IEntity.EntityKey property
internal virtual bool InRelationshipFixup
{
get { return _inRelationshipFixup; }
}
internal virtual ComplexTypeMaterializer ComplexTypeMaterializer
{
get
{
if (_complexTypeMaterializer == null)
{
_complexTypeMaterializer = new ComplexTypeMaterializer(MetadataWorkspace);
}
return _complexTypeMaterializer;
}
}
#endregion
internal virtual TransactionManager TransactionManager { get; private set; }
internal virtual EntityWrapperFactory EntityWrapperFactory
{
get { return _entityWrapperFactory; }
}
/// <summary>
/// Gets the <see cref="T:System.Data.Entity.Core.Metadata.Edm.MetadataWorkspace" /> associated with this state manager.
/// </summary>
/// <returns>
/// The <see cref="T:System.Data.Entity.Core.Metadata.Edm.MetadataWorkspace" /> associated with this
/// <see
/// cref="T:System.Data.Entity.Core.Objects.ObjectStateManager" />
/// .
/// </returns>
[CLSCompliant(false)]
public virtual MetadataWorkspace MetadataWorkspace
{
get { return _metadataWorkspace; }
}
#region events ObjectStateManagerChanged / EntityDeleted
/// <summary>Occurs when entities are added to or removed from the state manager.</summary>
public event CollectionChangeEventHandler ObjectStateManagerChanged
{
add { onObjectStateManagerChangedDelegate += value; }
remove { onObjectStateManagerChangedDelegate -= value; }
}
internal event CollectionChangeEventHandler EntityDeleted
{
add { onEntityDeletedDelegate += value; }
remove { onEntityDeletedDelegate -= value; }
}
internal virtual void OnObjectStateManagerChanged(CollectionChangeAction action, object entity)
{
Debug.Assert(!(entity is IEntityWrapper), "Object is an IEntityWrapper instance instead of the raw entity.");
if (onObjectStateManagerChangedDelegate != null)
{
onObjectStateManagerChangedDelegate(this, new CollectionChangeEventArgs(action, entity));
}
}
private void OnEntityDeleted(CollectionChangeAction action, object entity)
{
Debug.Assert(!(entity is IEntityWrapper), "Object is an IEntityWrapper instance instead of the raw entity.");
if (onEntityDeletedDelegate != null)
{
onEntityDeletedDelegate(this, new CollectionChangeEventArgs(action, entity));
}
}
#endregion
/// <summary>
/// Adds an object stub to the cache.
/// </summary>
/// <param name="entityKey"> the key of the object to add </param>
/// <param name="entitySet"> the entity set of the given object </param>
internal virtual EntityEntry AddKeyEntry(EntityKey entityKey, EntitySet entitySet)
{
DebugCheck.NotNull((object)entityKey);
DebugCheck.NotNull(entitySet);
// We need to determine if an equivalent entry already exists;
// this is illegal in certain cases.
var entry = FindEntityEntry(entityKey);
if (entry != null)
{
throw new InvalidOperationException(Strings.ObjectStateManager_ObjectStateManagerContainsThisEntityKey);
}
// Get a StateManagerTypeMetadata for the entity type.
var typeMetadata = GetOrAddStateManagerTypeMetadata(entitySet.ElementType);
// Create a cache entry.
entry = new EntityEntry(entityKey, entitySet, this, typeMetadata);
// A new entity is being added.
AddEntityEntryToDictionary(entry, entry.State);
return entry;
}
/// <summary>
/// Validates that the proxy type being attached to the context matches the proxy type
/// that would be generated for the given CLR type for the currently loaded metadata.
/// This prevents a proxy for one set of metadata being incorrectly loaded into a context
/// which has different metadata.
/// </summary>
private void ValidateProxyType(IEntityWrapper wrappedEntity)
{
var identityType = wrappedEntity.IdentityType;
var actualType = wrappedEntity.Entity.GetType();
if (identityType != actualType)
{
var entityType = MetadataWorkspace.GetItem<ClrEntityType>(identityType.FullNameWithNesting(), DataSpace.OSpace);
var proxyTypeInfo = EntityProxyFactory.GetProxyType(entityType, MetadataWorkspace);
if (proxyTypeInfo == null
|| proxyTypeInfo.ProxyType != actualType)
{
throw new InvalidOperationException(Strings.EntityProxyTypeInfo_DuplicateOSpaceType(identityType.FullName));
}
}
}
/// <summary>
/// Adds an object to the ObjectStateManager.
/// </summary>
/// <param name="dataObject"> the object to add </param>
/// <param name="entitySet"> the entity set of the given object </param>
/// <param name="argumentName"> Name of the argument passed to a public method, for use in exceptions. </param>
/// <param name="isAdded"> Indicates whether the entity is added or unchanged. </param>
internal virtual EntityEntry AddEntry(
IEntityWrapper wrappedObject, EntityKey passedKey, EntitySet entitySet, string argumentName, bool isAdded)
{
DebugCheck.NotNull(wrappedObject);
DebugCheck.NotNull(wrappedObject.Entity);
DebugCheck.NotNull(wrappedObject.Context);
DebugCheck.NotNull(entitySet);
DebugCheck.NotNull(argumentName);
var entityKey = passedKey;
// Get a StateManagerTypeMetadata for the entity type.
var typeMetadata = GetOrAddStateManagerTypeMetadata(wrappedObject.IdentityType, entitySet);
ValidateProxyType(wrappedObject);
// dataObject's type should match to type that can be contained by the entityset
var entityEdmType = typeMetadata.CdmMetadata.EdmType;
//OC Mapping will make sure that non-abstract type in O space is always mapped to a non-abstract type in C space
Debug.Assert(!entityEdmType.Abstract, "non-abstract type in O space is mapped to abstract type in C space");
if ((isAdded)
&& !entitySet.ElementType.IsAssignableFrom(entityEdmType))
{
throw new ArgumentException(
Strings.ObjectStateManager_EntityTypeDoesnotMatchtoEntitySetType(
wrappedObject.Entity.GetType().Name, TypeHelpers.GetFullName(entitySet.EntityContainer.Name, entitySet.Name)),
argumentName);
}
EntityKey dataObjectEntityKey = null;
if (isAdded)
{
dataObjectEntityKey = wrappedObject.GetEntityKeyFromEntity();
}
else
{
dataObjectEntityKey = wrappedObject.EntityKey;
}
#if DEBUG
if ((object)dataObjectEntityKey != null
&& (object)entityKey != null)
{
Debug.Assert(dataObjectEntityKey == entityKey, "The passed key and the key on dataObject must match.");
}
#endif
if (null != (object)dataObjectEntityKey)
{
entityKey = dataObjectEntityKey;
// These two checks verify that entityWithKey.EntityKey implemented by the user on a (I)POCO entity returns what it was given.
if ((object)entityKey == null)
{
throw new InvalidOperationException(Strings.EntityKey_UnexpectedNull);
}
if (wrappedObject.EntityKey != entityKey)
{
throw new InvalidOperationException(Strings.EntityKey_DoesntMatchKeyOnEntity(wrappedObject.Entity.GetType().FullName));
}
}
if ((object)entityKey != null
&& !entityKey.IsTemporary
&& !isAdded)
{
// If the entity already has a permanent key, and we were invoked
// from the materializer, check that the key is correct. We don't check
// for temporary keys because temporary keys don't contain values.
CheckKeyMatchesEntity(wrappedObject, entityKey, entitySet, /*forAttach*/ false);
}
// We need to determine if an equivalent entry already exists; this is illegal
// in certain cases.
EntityEntry existingEntry;
if ((isAdded)
&&
((dataObjectEntityKey == null && (null != (existingEntry = FindEntityEntry(wrappedObject.Entity)))) ||
(dataObjectEntityKey != null && (null != (existingEntry = FindEntityEntry(dataObjectEntityKey))))))
{
if (existingEntry.Entity
!= wrappedObject.Entity)
{
throw new InvalidOperationException(Strings.ObjectStateManager_ObjectStateManagerContainsThisEntityKey);
}
// key does exist but entity is the same, it is being re-added ;
// no-op when Add(entity)
// NOTE we don't want to re-add entities in other then Added state
if (existingEntry.State
!= EntityState.Added) // (state == DataRowState.Unchanged && state == DataRowState.Modified)
{
throw new InvalidOperationException(
Strings.ObjectStateManager_DoesnotAllowToReAddUnchangedOrModifiedOrDeletedEntity(existingEntry.State));
}
// no-op
return null;
}
else
{
// Neither entityWithKey.EntityKey nor the passed entityKey were non-null, or
// If the entity doesn't already exist in the state manager
// and we intend to put the entity in the Added state (i.e.,
// AddEntry() was invoked from ObjectContext.AddObject()),
// the entity's key must be set to a new temp key.
if ((object)entityKey == null
|| (isAdded && !entityKey.IsTemporary))
{
// If the entity does not have a key, create and add a temporary key.
entityKey = new EntityKey(entitySet);
wrappedObject.EntityKey = entityKey;
}
if (!wrappedObject.OwnsRelationshipManager)
{
// When a POCO instance is added or attached, we need to ignore the contents
// of the RelationshipManager as it is out-of-date with the POCO nav props
wrappedObject.RelationshipManager.ClearRelatedEndWrappers();
}
// Create a cache entry.
var newEntry = new EntityEntry(
wrappedObject, entityKey, entitySet, this, typeMetadata, isAdded ? EntityState.Added : EntityState.Unchanged);
//Verify that the entityKey is set correctly--also checks entry.EK and entity.EK internally
Debug.Assert(entityKey == newEntry.EntityKey, "The key on the new entry was not set correctly");
// ObjectMaterializer will have already determined the existing entry doesn't exist
Debug.Assert(null == FindEntityEntry(entityKey), "should not have existing entry");
// A new entity is being added.
newEntry.AttachObjectStateManagerToEntity();
AddEntityEntryToDictionary(newEntry, newEntry.State);
// fire ColectionChanged event only when a new entity is added to cache
OnObjectStateManagerChanged(CollectionChangeAction.Add, newEntry.Entity);
// When adding, we do this in AddSingleObject since we don't want to do it before the context is attached.
if (!isAdded)
{
FixupReferencesByForeignKeys(newEntry);
}
return newEntry;
}
}
internal virtual void FixupReferencesByForeignKeys(EntityEntry newEntry, bool replaceAddedRefs = false)
{
// Perf optimization to avoid all this work if the entity doesn't participate in any FK relationships
if (!((EntitySet)newEntry.EntitySet).HasForeignKeyRelationships)
{
return;
}
// Look at the foreign keys contained in this entry and perform fixup to the entities that
// they reference, or add the key and this entry to the index of foreign keys that reference
// entities that we don't yet know about.
newEntry.FixupReferencesByForeignKeys(replaceAddedRefs);
// Lookup the key for this entry and find all other entries that reference this entry using
// foreign keys. Perform fixup between the two entries.
foreach (var foundEntry in GetNonFixedupEntriesContainingForeignKey(newEntry.EntityKey))
{
foundEntry.FixupReferencesByForeignKeys(replaceAddedRefs: false);
}
// Once we have done fixup for this entry we don't need the entries in the index anymore
RemoveForeignKeyFromIndex(newEntry.EntityKey);
}
/// <summary>
/// Adds an entry to the index of foreign keys that reference entities that we don't yet know about.
/// </summary>
/// <param name="foreignKey"> The foreign key found in the entry </param>
/// <param name="entry"> The entry that contains the foreign key that was found </param>
internal virtual void AddEntryContainingForeignKeyToIndex(EntityKey foreignKey, EntityEntry entry)
{
HashSet<EntityEntry> danglingEntries;
if (!_danglingForeignKeys.TryGetValue(foreignKey, out danglingEntries))
{
danglingEntries = new HashSet<EntityEntry>();
_danglingForeignKeys.Add(foreignKey, danglingEntries);
}
Debug.Assert(entry.ObjectStateManager != null, "Attempt to add detached state entry to dangling keys");
danglingEntries.Add(entry);
}
[Conditional("DEBUG")]
internal virtual void AssertEntryDoesNotExistInForeignKeyIndex(EntityEntry entry)
{
foreach (var dFkEntry in _danglingForeignKeys.SelectMany(kv => kv.Value))
{
if (!(dFkEntry.State == EntityState.Detached || entry.State == EntityState.Detached))
{
Debug.Assert(
dFkEntry.EntityKey == null || entry.EntityKey == null ||
(dFkEntry.EntityKey != entry.EntityKey && dFkEntry != entry),
string.Format(
CultureInfo.InvariantCulture, "The entry references {0} equal. dFkEntry={1}, entry={2}",
dFkEntry == entry ? "are" : "are not", dFkEntry.EntityKey.ConcatKeyValue(), entry.EntityKey.ConcatKeyValue()));
}
}
}
[Conditional("DEBUG")]
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults",
Justification = "This method is compiled only when the compilation symbol DEBUG is defined")]
internal virtual void AssertAllForeignKeyIndexEntriesAreValid()
{
if (_danglingForeignKeys.Count == 0)
{
return;
}
var validEntries = new HashSet<ObjectStateEntry>(GetObjectStateEntriesInternal(~EntityState.Detached));
foreach (var entry in _danglingForeignKeys.SelectMany(kv => kv.Value))
{
Debug.Assert(entry._cache != null, "found an entry in the _danglingForeignKeys collection that has been nulled out");
Debug.Assert(
validEntries.Contains(entry),
"The entry in the dangling foreign key store is no longer in the ObjectStateManager. Key="
+
(entry.State == EntityState.Detached ? "detached" : entry.EntityKey != null ? "null" : entry.EntityKey.ConcatKeyValue()));
Debug.Assert(
entry.State == EntityState.Detached || !ForeignKeyFactory.IsConceptualNullKey(entry.EntityKey),
"Found an entry with conceptual null Key=" + entry.EntityKey.ConcatKeyValue());
}
}
/// <summary>
/// Removes an entry to the index of foreign keys that reference entities that we don't yet know about.
/// This is typically done when the entity is detached from the context.
/// </summary>
/// <param name="foreignKey"> The foreign key found in the entry </param>
/// <param name="entry"> The entry that contains the foreign key that was found </param>
internal virtual void RemoveEntryFromForeignKeyIndex(EntityKey foreignKey, EntityEntry entry)
{
HashSet<EntityEntry> danglingEntries;
if (_danglingForeignKeys.TryGetValue(foreignKey, out danglingEntries))
{
danglingEntries.Remove(entry);
}
}
/// <summary>
/// Removes the foreign key from the index of those keys that have been found in entries
/// but for which it was not possible to do fixup because the entity that the foreign key
/// referenced was not in the state manager.
/// </summary>
/// <param name="foreignKey"> The key to lookup and remove </param>
internal virtual void RemoveForeignKeyFromIndex(EntityKey foreignKey)
{
_danglingForeignKeys.Remove(foreignKey);
}
/// <summary>
/// Gets all state entries that contain the given foreign key for which we have not performed
/// fixup because the state manager did not contain the entity to which the foreign key pointed.
/// </summary>
/// <param name="foreignKey"> The key to lookup </param>
/// <returns> The state entries that contain the key </returns>
internal virtual IEnumerable<EntityEntry> GetNonFixedupEntriesContainingForeignKey(EntityKey foreignKey)
{
HashSet<EntityEntry> foundEntries;
if (_danglingForeignKeys.TryGetValue(foreignKey, out foundEntries))
{
// these entries will be updated by the code consuming them, so
// create a stable container to iterate over.
return foundEntries.ToList();
}
return Enumerable.Empty<EntityEntry>();
}
/// <summary>
/// Adds to index of currently tracked entities that have FK values that are conceptually
/// null but not actually null because the FK properties are not nullable.
/// If this index is non-empty in AcceptAllChanges or SaveChanges, then we throw.
/// If AcceptChanges is called on an entity and that entity is in the index, then
/// we will throw.
/// Note that the index is keyed by EntityEntry reference because it's only ever used
/// when we have the EntityEntry and this makes it slightly faster than using key lookup.
/// </summary>
internal virtual void RememberEntryWithConceptualNull(EntityEntry entry)
{
if (_entriesWithConceptualNulls == null)
{
_entriesWithConceptualNulls = new HashSet<EntityEntry>();
}
_entriesWithConceptualNulls.Add(entry);
}
/// <summary>
/// Checks whether or not there is some entry in the context that has any conceptually but not
/// actually null FK values.
/// </summary>
internal virtual bool SomeEntryWithConceptualNullExists()
{
return _entriesWithConceptualNulls != null && _entriesWithConceptualNulls.Count != 0;
}
/// <summary>
/// Checks whether the given entry has conceptually but not actually null FK values.
/// </summary>
internal virtual bool EntryHasConceptualNull(EntityEntry entry)
{
return _entriesWithConceptualNulls != null && _entriesWithConceptualNulls.Contains(entry);
}
/// <summary>
/// Stops keeping track of an entity with conceptual nulls because the FK values have been
/// really set or because the entity is leaving the context or becoming deleted.
/// </summary>
internal virtual void ForgetEntryWithConceptualNull(EntityEntry entry, bool resetAllKeys)
{
if (!entry.IsKeyEntry
&& _entriesWithConceptualNulls != null
&& _entriesWithConceptualNulls.Remove(entry))
{
if (entry.RelationshipManager.HasRelationships)
{
foreach (var end in entry.RelationshipManager.Relationships)
{
var reference = end as EntityReference;
if (reference != null
&& ForeignKeyFactory.IsConceptualNullKey(reference.CachedForeignKey))
{
if (resetAllKeys)
{
reference.SetCachedForeignKey(null, null);
}
else
{
// This means that we thought we could remove because one FK was no longer conceptually
// null, but in fact we have to add the entry back because another FK is still conceptually null
_entriesWithConceptualNulls.Add(entry);
break;
}
}
}
}
}
}
// devnote: see comment to SQLBU 555615 in ObjectContext.AttachSingleObject()
internal virtual void PromoteKeyEntryInitialization(
ObjectContext contextToAttach,
EntityEntry keyEntry,
IEntityWrapper wrappedEntity,
bool replacingEntry)
{
DebugCheck.NotNull(keyEntry);
DebugCheck.NotNull(wrappedEntity);
// Future Enhancement: Fixup already has this information, don't rediscover it
var typeMetadata = GetOrAddStateManagerTypeMetadata(wrappedEntity.IdentityType, (EntitySet)keyEntry.EntitySet);
ValidateProxyType(wrappedEntity);
keyEntry.PromoteKeyEntry(wrappedEntity, typeMetadata);
AddEntryToKeylessStore(keyEntry);
if (replacingEntry)
{
// if we are replacing an existing entry, then clean the entity's change tracker
// so that it can be reset to this newly promoted entry
wrappedEntity.SetChangeTracker(null);
}
// A new entity is being added.
wrappedEntity.SetChangeTracker(keyEntry);
if (contextToAttach != null)
{
// The ObjectContext needs to be attached to the wrapper here because we need it to be attached to
// RelatedEnds for the snapshot change tracking that happens in TakeSnapshot. However, it
// cannot be attached in ObjectContext.AttachSingleObject before calling this method because this
// would attach it to RelatedEnds before SetChangeTracker is called, thereby breaking a legacy
// case for entities derived from EntityObject--see AttachSingleObject for details.
wrappedEntity.AttachContext(contextToAttach, (EntitySet)keyEntry.EntitySet, MergeOption.AppendOnly);
}
wrappedEntity.TakeSnapshot(keyEntry);
OnObjectStateManagerChanged(CollectionChangeAction.Add, keyEntry.Entity);
}
/// <summary>
/// Upgrades an entity key entry in the cache to a a regular entity
/// </summary>
/// <param name="keyEntry"> the key entry that exists in the state manager </param>
/// <param name="entity"> the object to add </param>
/// <param name="replacingEntry"> True if this promoted key entry is replacing an existing detached entry </param>
/// <param name="setIsLoaded"> Tells whether we should allow the IsLoaded flag to be set to true for RelatedEnds </param>
internal virtual void PromoteKeyEntry(
EntityEntry keyEntry,
IEntityWrapper wrappedEntity,
bool replacingEntry,
bool setIsLoaded,
bool keyEntryInitialized)
{
DebugCheck.NotNull(keyEntry);
DebugCheck.NotNull(wrappedEntity);
DebugCheck.NotNull(wrappedEntity.Entity);
DebugCheck.NotNull(wrappedEntity.Context);
if (!keyEntryInitialized)
{
// We pass null as the context here because, as asserted above, the context is already attached
// to the wrapper when it comes down this path.
PromoteKeyEntryInitialization(null, keyEntry, wrappedEntity, replacingEntry);
}
var doCleanup = true;
try
{
// We don't need to worry about the KeyEntry <-- Relationship --> KeyEntry because a key entry must
// reference a non-key entry. Fix up their other side of the relationship.
// Get all the relationships that currently exist for this key entry
foreach (var relationshipEntry in CopyOfRelationshipsByKey(keyEntry.EntityKey))
{
if (relationshipEntry.State
!= EntityState.Deleted)
{
// Find the association ends that correspond to the source and target
var sourceMember = keyEntry.GetAssociationEndMember(relationshipEntry);
var targetMember = MetadataHelper.GetOtherAssociationEnd(sourceMember);
// Find the other end of the relationship
var targetEntry = keyEntry.GetOtherEndOfRelationship(relationshipEntry);
// Here we are promoting based on a non-db retrieval so we use Append rules
AddEntityToCollectionOrReference(
MergeOption.AppendOnly,
wrappedEntity,
sourceMember,
targetEntry.WrappedEntity,
targetMember,
/*setIsLoaded*/ setIsLoaded,
/*relationshipAlreadyExists*/ true,
/*inKeyEntryPromotion*/ true);
}
}
FixupReferencesByForeignKeys(keyEntry);
doCleanup = false;
}
finally
{
if (doCleanup)
{
keyEntry.DetachObjectStateManagerFromEntity();
RemoveEntryFromKeylessStore(wrappedEntity);
keyEntry.DegradeEntry();
}
}
if (TransactionManager.IsAttachTracking)
{
TransactionManager.PromotedKeyEntries.Add(wrappedEntity.Entity, keyEntry);
}
}
internal virtual void TrackPromotedRelationship(RelatedEnd relatedEnd, IEntityWrapper wrappedEntity)
{
DebugCheck.NotNull(relatedEnd);
DebugCheck.NotNull(wrappedEntity);
Debug.Assert(wrappedEntity.Entity != null);
Debug.Assert(
TransactionManager.IsAttachTracking || TransactionManager.IsAddTracking,
"This method should be called only from ObjectContext.AttachTo/AddObject (indirectly)");
IList<IEntityWrapper> entities;
if (!TransactionManager.PromotedRelationships.TryGetValue(relatedEnd, out entities))
{
entities = new List<IEntityWrapper>();
TransactionManager.PromotedRelationships.Add(relatedEnd, entities);
}
entities.Add(wrappedEntity);
}
internal virtual void DegradePromotedRelationships()
{
Debug.Assert(
TransactionManager.IsAttachTracking || TransactionManager.IsAddTracking,
"This method should be called only from the cleanup code");
foreach (var pair in TransactionManager.PromotedRelationships)
{
foreach (var wrappedEntity in pair.Value)
{
if (pair.Key.RemoveFromCache(wrappedEntity, /*resetIsLoaded*/ false, /*preserveForeignKey*/ false))
{
pair.Key.OnAssociationChanged(CollectionChangeAction.Remove, wrappedEntity.Entity);
}
}
}
}
/// <summary>
/// Performs non-generic collection or reference fixup between two entities
/// This method should only be used in scenarios where we are automatically hooking up relationships for
/// the user, and not in cases where they are manually setting relationships.
/// </summary>
/// <param name="mergeOption"> The MergeOption to use to decide how to resolve EntityReference conflicts </param>
/// <param name="sourceEntity"> The entity instance on the source side of the relationship </param>
/// <param name="sourceMember"> The AssociationEndMember that contains the metadata for the source entity </param>
/// <param name="targetEntity"> The entity instance on the source side of the relationship </param>
/// <param name="targetMember"> The AssociationEndMember that contains the metadata for the target entity </param>
/// <param name="setIsLoaded"> Tells whether we should allow the IsLoaded flag to be set to true for RelatedEnds </param>
/// <param name="relationshipAlreadyExists"> Whether or not the relationship entry already exists in the cache for these entities </param>
/// <param name="inKeyEntryPromotion"> Whether this method is used in key entry promotion </param>
internal static void AddEntityToCollectionOrReference(
MergeOption mergeOption,
IEntityWrapper wrappedSource,
AssociationEndMember sourceMember,
IEntityWrapper wrappedTarget,
AssociationEndMember targetMember,
bool setIsLoaded,
bool relationshipAlreadyExists,
bool inKeyEntryPromotion)
{
// Call GetRelatedEnd to retrieve the related end on the source entity that points to the target entity
var relatedEnd = wrappedSource.RelationshipManager.GetRelatedEndInternal(sourceMember.DeclaringType.FullName, targetMember.Name);
// EntityReference can only have one value
if (targetMember.RelationshipMultiplicity
!= RelationshipMultiplicity.Many)
{
Debug.Assert(
relatedEnd is EntityReference, "If end is not Many multiplicity, then the RelatedEnd should be an EntityReference.");
var relatedReference = (EntityReference)relatedEnd;
switch (mergeOption)
{
case MergeOption.NoTracking:
// if using NoTracking, we have no way of determining identity resolution.
// Throw an exception saying the EntityReference is already populated and to try using
// a different MergeOption
Debug.Assert(
relatedEnd.IsEmpty(),
"This can occur when objects are loaded using a NoTracking merge option. Try using a different merge option when loading objects.");
break;
case MergeOption.AppendOnly:
// SQLBU 551031
// In key entry promotion case, detect that sourceEntity is already related to some entity in the context,
// so it cannot be related to another entity being attached (relation 1-1).
// Without this check we would throw exception from RelatedEnd.Add() but the exception message couldn't
// properly describe what has happened.
if (inKeyEntryPromotion
&&
!relatedReference.IsEmpty()
&&
!ReferenceEquals(relatedReference.ReferenceValue.Entity, wrappedTarget.Entity))
{
throw new InvalidOperationException(Strings.ObjectStateManager_EntityConflictsWithKeyEntry);
}
break;
case MergeOption.PreserveChanges:
case MergeOption.OverwriteChanges:
// Retrieve the current target entity and the relationship
var currentWrappedTarget = relatedReference.ReferenceValue;
// currentWrappedTarget may already be correct because we may already have done FK fixup as part of
// accepting changes in the overwrite code.
if (currentWrappedTarget != null
&& currentWrappedTarget.Entity != null
&& currentWrappedTarget != wrappedTarget)
{
// The source entity is already related to a different target, so before we hook it up to the new target,
// disconnect the existing related ends and detach the relationship entry
var relationshipEntry = relatedEnd.FindRelationshipEntryInObjectStateManager(currentWrappedTarget);
Debug.Assert(
relationshipEntry != null || relatedEnd.IsForeignKey,
"Could not find relationship entry for LAT relationship.");
relatedEnd.RemoveAll();
if (relationshipEntry != null)
{
Debug.Assert(relationshipEntry != null, "Could not find relationship entry.");
// If the relationship was Added prior to the above RemoveAll, it will have already been detached
// If it was Unchanged, it is now Deleted and should be detached
// It should never have been Deleted before now, because we just got currentTargetEntity from the related end
if (relationshipEntry.State
== EntityState.Deleted)
{
relationshipEntry.AcceptChanges();
}
Debug.Assert(relationshipEntry.State == EntityState.Detached, "relationshipEntry should be Detached");
}
}
break;
}
}
RelatedEnd targetRelatedEnd = null;
if (mergeOption == MergeOption.NoTracking)
{
targetRelatedEnd = relatedEnd.GetOtherEndOfRelationship(wrappedTarget);
if (targetRelatedEnd.IsLoaded)
{
// The EntityCollection has already been loaded as part of the query and adding additional
// entities would cause duplicate entries
throw new InvalidOperationException(
Strings.Collections_CannotFillTryDifferentMergeOption(
targetRelatedEnd.SourceRoleName, targetRelatedEnd.RelationshipName));
}
}
// we may have already retrieved the target end above, but if not, just get it now
if (targetRelatedEnd == null)
{
targetRelatedEnd = relatedEnd.GetOtherEndOfRelationship(wrappedTarget);
}
// Add the target entity
relatedEnd.Add(
wrappedTarget,
applyConstraints: true,
addRelationshipAsUnchanged: true,
relationshipAlreadyExists: relationshipAlreadyExists,
allowModifyingOtherEndOfRelationship: true,
forceForeignKeyChanges: true);
Debug.Assert(
!(inKeyEntryPromotion && wrappedSource.Context == null),
"sourceEntity has been just attached to the context in PromoteKeyEntry, so Context shouldn't be null");
Debug.Assert(
!(inKeyEntryPromotion &&
wrappedSource.Context.ObjectStateManager.TransactionManager.IsAttachTracking &&
(setIsLoaded || mergeOption == MergeOption.NoTracking)),
"This verifies that UpdateRelatedEnd is a no-op in a keyEntryPromotion case when the method is called indirectly from ObjectContext.AttachTo");
// If either end is an EntityReference, we may need to set IsLoaded or the DetachedEntityKey
UpdateRelatedEnd(relatedEnd, wrappedTarget, setIsLoaded, mergeOption);
UpdateRelatedEnd(targetRelatedEnd, wrappedSource, setIsLoaded, mergeOption);
// In case the method was called from ObjectContext.AttachTo, we have to track relationships which were "promoted"
// Tracked relationships are used in recovery code of AttachTo.
if (inKeyEntryPromotion && wrappedSource.Context.ObjectStateManager.TransactionManager.IsAttachTracking)
{
wrappedSource.Context.ObjectStateManager.TrackPromotedRelationship(relatedEnd, wrappedTarget);
wrappedSource.Context.ObjectStateManager.TrackPromotedRelationship(targetRelatedEnd, wrappedSource);
}
}
// devnote: This method should only be used in scenarios where we are automatically hooking up relationships for
// the user, and not in cases where they are manually setting relationships.
private static void UpdateRelatedEnd(
RelatedEnd relatedEnd, IEntityWrapper wrappedRelatedEntity, bool setIsLoaded, MergeOption mergeOption)
{
var endMember = (AssociationEndMember)(relatedEnd.ToEndMember);
if ((endMember.RelationshipMultiplicity == RelationshipMultiplicity.One ||
endMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne))
{
if (setIsLoaded)
{
relatedEnd.IsLoaded = true;
}
// else we just want to leave IsLoaded alone, not set it to false
// In NoTracking cases, we want to enable the EntityReference.EntityKey property, so we have to set the key
if (mergeOption == MergeOption.NoTracking)
{
var targetKey = wrappedRelatedEntity.EntityKey;
if ((object)targetKey == null)
{
throw new InvalidOperationException(Strings.EntityKey_UnexpectedNull);
}
// since endMember is not Many, relatedEnd must be an EntityReference
((EntityReference)relatedEnd).DetachedEntityKey = targetKey;
}
}
}
/// <summary>
/// Updates the relationships between a given source entity and a collection of target entities.
/// Used for full span and related end Load methods, where the following may be true:
/// (a) both sides of each relationship are always full entities and not stubs
/// (b) there could be multiple entities to process at once
/// (c) NoTracking queries are possible.
/// Not used for relationship span because although some of the logic is similar, the above are not true.
/// </summary>
/// <param name="context"> ObjectContext to use to look up existing relationships. Using the context here instead of ObjectStateManager because for NoTracking queries we shouldn't even touch the state manager at all, so we don't want to access it until we know we are not using NoTracking. </param>
/// <param name="mergeOption"> MergeOption to use when updating existing relationships </param>
/// <param name="associationSet"> AssociationSet for the relationships </param>
/// <param name="sourceMember"> Role of sourceEntity in associationSet </param>
/// <param name="sourceKey"> EntityKey for sourceEntity </param>
/// <param name="sourceEntity"> Source entity in the relationship </param>
/// <param name="targetMember"> Role of each targetEntity in associationSet </param>
/// <param name="targetEntities"> List of target entities to use to create relationships with sourceEntity </param>
/// <param name="setIsLoaded"> Tells whether we should allow the IsLoaded flag to be set to true for RelatedEnds </param>
internal virtual int UpdateRelationships(
ObjectContext context, MergeOption mergeOption, AssociationSet associationSet, AssociationEndMember sourceMember,
IEntityWrapper wrappedSource, AssociationEndMember targetMember, IList targets, bool setIsLoaded)
{
var count = 0;
var sourceKey = wrappedSource.EntityKey;
context.ObjectStateManager.TransactionManager.BeginGraphUpdate();
try
{
if (targets != null)
{
if (mergeOption == MergeOption.NoTracking)
{
var relatedEnd = wrappedSource.RelationshipManager.GetRelatedEndInternal(
sourceMember.DeclaringType.FullName, targetMember.Name);
if (!relatedEnd.IsEmpty())
{
// The RelatedEnd has already been filled as part of the query and adding additional
// entities would cause duplicate entries
throw new InvalidOperationException(
Strings.Collections_CannotFillTryDifferentMergeOption(
relatedEnd.SourceRoleName, relatedEnd.RelationshipName));
}
}
foreach (var someTarget in targets)
{
var wrappedTarget = someTarget as IEntityWrapper;
if (wrappedTarget == null)
{
wrappedTarget = EntityWrapperFactory.WrapEntityUsingContext(someTarget, context);
}
count++;
// If there is an existing relationship entry, update it based on its current state and the MergeOption, otherwise add a new one
EntityState newEntryState;
if (mergeOption == MergeOption.NoTracking)
{
// For NoTracking, we shouldn't touch the state manager, so no need to look for existing relationships to handle, just connect the two entities.
// We don't care if the relationship already exists in the state manager or not, so just pass relationshipAlreadyExists=true so it won't look for it
AddEntityToCollectionOrReference(
MergeOption.NoTracking,
wrappedSource,
sourceMember,
wrappedTarget,
targetMember,
setIsLoaded,
/*relationshipAlreadyExists*/ true,
/*inKeyEntryPromotion*/ false);
}
else
{
var manager = context.ObjectStateManager;
var targetKey = wrappedTarget.EntityKey;
if (
!TryUpdateExistingRelationships(
context, mergeOption, associationSet, sourceMember, sourceKey, wrappedSource, targetMember, targetKey,
setIsLoaded, out newEntryState))