9

I have a base class that I inherit from that has two zero to many relationships with other entities:

public abstract class WebObject
{
    public WebObject()
    {
        RelatedTags = new List<Tag>();
        RelatedWebObjects = new List<WebObject>();
    }

    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    public string MetaKeywords { get; set; }
    public string MetaDescription { get; set; }

    [InverseProperty("WebObjects")]
    public virtual WebSite WebSite { get; set; }

    [Required(ErrorMessage = "Every WebObject must be associated with a WebSite.")]
    public Guid WebSiteId { get; set; }

    public virtual ICollection<Tag> RelatedTags { get; set; }
    public IList<Guid> RelatedTagIds { get; set; }
    public virtual ICollection<WebObject> RelatedWebObjects { get; set; }
    public IList<Guid> RelatedWebObjectIds { get; set; }
}

I am having difficulty getting the original values for these relationships (RelatedWebObjects & RelatedTags) when looking at the entities using ChangeTracker during SaveChanges. I can see all of the scalar values before and after, and I can see the new relationships, but I cannot see the old ones. I've tried using the Member and Collection methods, but those only show me the current values; not the old. Also I don't like using those because it requires me to know the name of the navigation property, which isn't generic enough.

I can find the related objects whose relationship is being changed, but of course the values within those related objects isn't changing, so that isn't any help either.

Is there some clean way for me to track the previous relationships of an entity during SaveChanges with ChangeTracker?

Below is the section of code that I'm working on:

    public override int SaveChanges()
    {
        List<AuditObject> auditTrailList = new List<AuditObject>();

        foreach (DbEntityEntry entity in ChangeTracker.Entries().Where(obj => { return obj.State == EntityState.Added || obj.State == EntityState.Modified || obj.State == EntityState.Deleted; }))
        {
            if (!(entity.Entity is AuditObject))
            {
                AuditObject auditObject = new AuditObject();

                auditObject.Id = Guid.NewGuid();

                auditObject.RevisionStamp = DateTime.Now;

                auditObject.UserName = HttpContext.Current.User.Identity.Name;

                auditObject.EntityType = Utilities.GetCleanClassNameIfProxyClass(entity.Entity.GetType().Name);

                if (entity.State == EntityState.Added)
                    auditObject.Action = EntityState.Added.ToString();
                else if (entity.State == EntityState.Modified)
                    auditObject.Action = EntityState.Modified.ToString();
                else if (entity.State == EntityState.Deleted)
                    auditObject.Action = EntityState.Deleted.ToString();

                DbMemberEntry t1 = entity.Member("RelatedWebObjects");
                // cannot find original relationship collection...

                DbCollectionEntry t2 = entity.Collection("RelatedWebObjects");
                // cannot find original relationship collection...

                if (entity.State == EntityState.Added || entity.State == EntityState.Modified)
                {
                    XDocument currentValues = new XDocument(new XElement(auditObject.EntityType));

                    foreach (string propertyName in entity.CurrentValues.PropertyNames)
                    {
                        currentValues.Root.Add(new XElement(propertyName, entity.CurrentValues[propertyName]));
                    }

                    auditObject.NewData = Regex.Replace(currentValues.ToString(), @"\r\n+", " ");
                }

                if (entity.State == EntityState.Modified || entity.State == EntityState.Deleted)
                {
                    XDocument originalValues = new XDocument(new XElement(auditObject.EntityType));

                    foreach (string propertyName in entity.OriginalValues.PropertyNames)
                    {
                        originalValues.Root.Add(new XElement(propertyName, entity.OriginalValues[propertyName]));
                    }

                    auditObject.OldData = Regex.Replace(originalValues.ToString(), @"\r\n+", " ");
                }

                auditTrailList.Add(auditObject);
            }
        }

        foreach (var audit in auditTrailList)
            this.AuditObjects.Add(audit);

        return base.SaveChanges();
    }
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
DMC
  • 361
  • 4
  • 15
  • The related objects are being changed tracked by the ObjectStateManager as well and you should be able to get their object state entries just like how you get it for your main object. If you post the code that you are struggling with, I might be able to help. – Morteza Manavi Aug 17 '11 at 02:38
  • Thanks again Morteza... I posted the section of code that I'm working with right now - please excuse the sloppiness of it; it hasn't been refactored at all - just trying to get it to work. I can get the scalar properties of objects without any problem using the IEnumerable strings: entity.OriginalValues.PropertyNames & entity.CurrentValues.PropertyNames. I am having trouble with entity.Collection() and entity.Member(). Which should be used for what I am trying to accomplish? Is there a way to make this generic so that I don't have to hard-code the collection names? Reflection maybe? – DMC Aug 17 '11 at 14:01
  • i'm not in a position to try this until tomorrow... do either of you know how i can split points between two answers? because i think you both gave very valuable information. – DMC Aug 21 '11 at 15:29

2 Answers2

13

Well this is little bit difficult. First of all you have to differ two types of relationships offered by EF:

  • Independent association (all many-to-many relations and some one-to-many)
  • Foreign key association (all one-to-one relations and some one-to-many)

Now if you want to know previous value of foreign key association you just need to track the changes in dependent entity where you have exposed foreign key property - this is exactly the same as tracking any other property change.

If you want to track changes in an independent association the situation will become harder because DbContext API doesn't provide operations to track them. You must revert back to ObjectContext API and its ObjectStateManager.

ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
foreach (ObjectStateEntry entry = objectContext.ObjectStateManager
                                               .GetObjectStateEntries(~EntityState.Detached)
                                               .Where(e => e.IsRelationship))
{
    // Track changes here
}

Now you have access to ObjectStateEntry instances for the relationship. These instances should never have state Modified. They will be either Added, Deleted or Unchanged because "modification" is processed as deletion of old relation and adding a new one. ObjectStateEntry also contains CurrentValues and OriginalValues collections. These collections should also contain two items each representing EntityKey of entity on one side of the relation.

Community
  • 1
  • 1
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • thank you very much for your help! i can see that this works. also i learned about the bitwise ~ operator - never used that before. do you know if it is possible to assign points to both you and Morteza? his answer came in first and works also and i reached out to him specifically via his blog, but i want you both to get credit... – DMC Aug 22 '11 at 14:06
  • @DMC: You can mark only one answer as accepted but depending on your reputation you can also cast upvotes to as many answers as you want. – Ladislav Mrnka Aug 22 '11 at 14:35
  • sorry - gonna give it to Morteza because he got it in first and i went out of my way to contact him directly about this. but i did give you an upvote for this. – DMC Aug 22 '11 at 18:04
4

Because EF change tracks every object in your graph, you can always pass any instance in the graph to the change tracker and it will give you the change tracking values. For example, the following code will get the Original/Current values of the AuditObject's navigation property:

DbMemberEntry t1 = entity.Member("RelatedWebObjects");
// cannot find original relationship collection....

AuditObject currentAuditObject = (AuditObject) entity;
var currValues = this.Entry(currentAuditObject.RelatedWebObjects).CurrentValues;
var orgValues = this.Entry(currentAuditObject.RelatedWebObjects).OriginalValues;

Or you can apply the same trick when you are dealing with a collection type navigation property:

DbCollectionEntry t2 = entity.Collection("RelatedWebObjects");
// cannot find original relationship collection....

foreach (WebObject item in currentAuditObject.RelatedWebObjects)
{
    var currValues = this.Entry(item).CurrentValues;
}
Morteza Manavi
  • 33,026
  • 6
  • 100
  • 83
  • thanks yet again Morteza! i appreciate you getting back to me so quickly. also thanks again for the blog! – DMC Aug 22 '11 at 14:07