/// <summary> /// Make a shallow copy of column values without copying references of the source entity /// </summary> /// <param name="source">the source entity that will have it's values copied</param> /// <returns></returns> public static LINQEntityBase ShallowCopy(LINQEntityBase source) { PropertyInfo[] sourcePropInfos = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); PropertyInfo[] destinationPropInfos = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); // create an object to copy values into Type entityType = source.GetType(); LINQEntityBase destination; destination = Activator.CreateInstance(entityType) as LINQEntityBase; foreach (PropertyInfo sourcePropInfo in sourcePropInfos) { if (Attribute.GetCustomAttribute(sourcePropInfo, typeof(ColumnAttribute), false) != null) { PropertyInfo destPropInfo = destinationPropInfos.Where(pi => pi.Name == sourcePropInfo.Name).First(); destPropInfo.SetValue(destination, sourcePropInfo.GetValue(source, null), null); } } destination.LINQEntityState = EntityState.Original; destination.LINQEntityGUID = source.LINQEntityGUID; return(destination); }
/// <summary> /// Returns true if two entities have the same property values (does not traverse releationships). /// </summary> /// <param name="source"></param> /// <returns></returns> public static bool ShallowCompare(LINQEntityBase entity1, LINQEntityBase entity2) { if (!object.ReferenceEquals(entity1.GetType(), entity2.GetType())) { return(false); } PropertyInfo[] entity1PropInfos = entity1.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); PropertyInfo[] entity2PropInfos = entity2.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); // Find if there are any properties that do not match that are custom attributes var compareResults = from pi1 in entity1PropInfos.Where(p1 => Attribute.GetCustomAttribute(p1, typeof(ColumnAttribute), false) != null) join pi2 in entity2PropInfos.Where(p2 => Attribute.GetCustomAttribute(p2, typeof(ColumnAttribute), false) != null) on pi1.Name equals pi2.Name into pij from pi2 in pij.DefaultIfEmpty() select new { Match = object.Equals(pi1.GetValue(entity1, null), pi2.GetValue(entity2, null)) }; return(compareResults.Where(cr => cr.Match == false).Count() == 0); }
/// <summary> /// Handles the property changing event sent from child object. /// </summary> /// <param name="sender">child object</param> /// <param name="e">Property Changing arguements</param> private void PropertyChanging(object sender, PropertyChangingEventArgs e) { // Ignore events if syncronising with DB if (_isSyncronisingWithDB == true) { return; } // Do a check here to make sure that the entity is not change if it is supposed to be deleted if (this.LINQEntityState == EntityState.Deleted || this.LINQEntityState == EntityState.CancelNew) { throw new ApplicationException("You cannot modify a deleted entity"); } // If it's a change tracked object thats in "Original" state // grab a copy of the object incase it's going to be modified if (this.LINQEntityState == EntityState.Original && LINQEntityKeepOriginal == true && LINQEntityOriginalValue == null) { _originalEntityValueTemp = LINQEntityBase.ShallowCopy(this); } }
/// <summary> /// Indicates that the entity will Update the database. /// </summary> /// <param name="OriginalValue"> /// Sets/Overrides the original value of the entity. /// The entity value passed in should be an earlier shallow copy of the entity. /// This value can be set to null to indicate if the original entity value should be removed if it exists from a previous modification. /// </param> public void SetAsUpdateOnSubmit(LINQEntityBase OriginalValue) { if (this.LINQEntityState == EntityState.Detached) { throw new ApplicationException("You cannot change the Entity State from 'Detached' to 'Modified'"); } if (this.LINQEntityState == EntityState.NotTracked) { throw new ApplicationException("You cannot change the Entity State when the Entity is not change tracked"); } if (OriginalValue != null) { this._originalEntityValue = LINQEntityBase.ShallowCopy(this); } else { this._originalEntityValue = null; } this.LINQEntityState = EntityState.Modified; }
/// <summary> /// Syncronises this EntityBase and all sub objects /// with a data context. /// </summary> /// <param name="targetDataContext">The data context that will apply the changes</param> /// <param name="cascadeDelete">Whether or not casade deletes is allowed</param> public void SynchroniseWithDataContext(DataContext targetDataContext, bool cascadeDelete) { // Before doing anything, check to make sure that the new datacontext // doesn't try any deferred (lazy) loading if (targetDataContext.DeferredLoadingEnabled == true) { throw new ApplicationException("Syncronisation requires that the Deferred loading is disabled on the Target DataContext"); } // Also Make sure this entity is the change tracking root if (this.IsRoot == false) { throw new ApplicationException("You cannot syncronise an entity that is not the change tracking root"); } List <LINQEntityBase> entities = this.ToEntityTree().Distinct().ToList(); List <LINQEntityBase> entitiesDeleted = new List <LINQEntityBase>(); try { // Tell each entity that syncronisation is occuring foreach (LINQEntityBase entity in entities) { entity._isSyncronisingWithDB = true; } // For entities which have been Cancelled (Added as new then deleted before submission to DB) // detach these entities by removing it's references so that they can be garbage collected. foreach (LINQEntityBase entity in entities) { if (entity.LINQEntityState == EntityState.CancelNew) { foreach (PropertyInfo propInfo in _cacheAssociationFKProperties[entity.GetType()].Values) { propInfo.SetValue(entity, null, null); } } } // Loop through all the entities, attaching as appropriate to the data context foreach (LINQEntityBase entity in entities) { if (entity.LINQEntityState == EntityState.Original) { targetDataContext.GetTable(entity.GetEntityType()).Attach(entity, false); } else if (entity.LINQEntityState == EntityState.New) { // If the entity's state is new, LINQ to SQL Tries to attach an FK references as "New" as well. // To avoid this, attach all FK references first as unmodified (unless they were intended to be new anyway) // then attach the new record. foreach (PropertyInfo fkPropInfo in _cacheAssociationFKProperties[entity.GetType()].Values) { LINQEntityBase fkProp = fkPropInfo.GetValue(entity, null) as LINQEntityBase; if (fkProp != null && fkProp.LINQEntityState != EntityState.New) { try { targetDataContext.GetTable(fkProp.GetType()).Attach(fkProp, false); } catch { // do nothing as the entity was already attached. } } } targetDataContext.GetTable(entity.GetEntityType()).InsertOnSubmit(entity); } else if (entity.LINQEntityState == EntityState.Modified || entity.LINQEntityState == EntityState.Detached) { if (entity.LINQEntityOriginalValue != null) { targetDataContext.GetTable(entity.GetEntityType()).Attach(entity, entity.LINQEntityOriginalValue); } else { targetDataContext.GetTable(entity.GetEntityType()).Attach(entity, true); } } if (entity.LINQEntityState == EntityState.Deleted && !entitiesDeleted.Contains(entity)) { // Check to see if cascading deletes is allowed if (cascadeDelete) { // Grab the entity tree and reverse it so that this entity is deleted last List <LINQEntityBase> entityTreeReversed = entity.ToEntityTree(); entityTreeReversed.Reverse(); // Cascade delete children and then this object foreach (LINQEntityBase toDelete in entityTreeReversed) { // Before we try and delete, make sure the entity hasn't been marked to be deleted already // through another relationship linkng this entity in the same sub-tree that is being deleted. if (!entitiesDeleted.Contains(toDelete)) { // Mark for deletion toDelete.SetAsDeleteOnSubmit(); targetDataContext.GetTable(toDelete.GetEntityType()).Attach(toDelete); targetDataContext.GetTable(toDelete.GetEntityType()).DeleteOnSubmit(toDelete); //add deleted entity to a list to make sure we don't delete them twice. entitiesDeleted.Add(toDelete); } } } else { // Mark for deletion targetDataContext.GetTable(entity.GetEntityType()).Attach(entity); targetDataContext.GetTable(entity.GetEntityType()).DeleteOnSubmit(entity); //add deleted entity to a list to make sure we don't delete them twice. entitiesDeleted.Add(entity); } // if this is the root object, there's no need to do more processing // so just quit the loop if (this == entity) { break; } } } // Reset this entity as the change tracking root, getting a new copy of all objects this.SetAsChangeTrackingRoot(this.LINQEntityKeepOriginal); } finally { // Tell each entity that syncronisation is occuring foreach (LINQEntityBase entity in entities) { entity._isSyncronisingWithDB = false; } } }
/// <summary> /// Handles the property changed event sent from child object. /// </summary> /// <param name="sender">child object</param> /// <param name="e">Property Changed arguements</param> private void PropertyChanged(object sender, PropertyChangedEventArgs e) { // Ignore events if syncronising with DB if (_isSyncronisingWithDB == true) { return; } PropertyInfo propInfo = null; // if this object isn't change tracked yet, but it's parent // is, this means it's a new object if (this.LINQEntityState == EntityState.NotTracked) { // Check to see if the parent object is change tracked // If there is, set the new flag, and tell this new object it's tracked if (_cacheAssociationFKProperties[this.GetType()].ContainsKey(e.PropertyName)) { if (_cacheAssociationFKProperties[this.GetType()].TryGetValue(e.PropertyName, out propInfo)) { if (propInfo != null) { LINQEntityBase parentEntity = (LINQEntityBase)propInfo.GetValue(this, null); if (parentEntity != null && parentEntity.LINQEntityState != EntityState.NotTracked) { // loop through this entity and child entities and track them aswell foreach (LINQEntityBase entity in this.ToEntityTree()) { entity.LINQEntityState = EntityState.New; } } } } } } //if the object is not new.... if (LINQEntityState != EntityState.New) { // only go into this section if it's change tracked if (this.LINQEntityState != EntityState.NotTracked) { if (!_cacheAssociationProperties[this.GetType()].ContainsKey(e.PropertyName)) { if (_cacheAssociationFKProperties[this.GetType()].ContainsKey(e.PropertyName)) { if (_cacheAssociationFKProperties[this.GetType()].TryGetValue(e.PropertyName, out propInfo)) { // Parent FK has been set to null, object is now detached. if ((propInfo != null) && (propInfo.GetValue(this, null) == null)) { this._originalEntityValue = this._originalEntityValueTemp; LINQEntityState = EntityState.Detached; } else if (LINQEntityState != EntityState.Modified && LINQEntityState != EntityState.Detached) { this._originalEntityValue = this._originalEntityValueTemp; LINQEntityState = EntityState.Modified; } } } else { // if a db generated column has been modified // do nothing bool isDbGenerated = _cacheDBGeneratedProperties[this.GetType()].TryGetValue(e.PropertyName, out propInfo); if (isDbGenerated) { return; } // if the object is already modified and the property values have reverted back to their // original values, set the state back to "Original" if (LINQEntityState == EntityState.Modified && this._originalEntityValue != null) { if (ShallowCompare(this, this._originalEntityValue)) { LINQEntityState = EntityState.Original; this._originalEntityValue = null; } } // if the object isn't already modified or detached // set it as modified else if (LINQEntityState != EntityState.Modified && LINQEntityState != EntityState.Detached) { this._originalEntityValue = this._originalEntityValueTemp; LINQEntityState = EntityState.Modified; } } } } } this._originalEntityValueTemp = null; }