Home > Enterprise >  Entity Framework doesn't get original entity when comparing changes
Entity Framework doesn't get original entity when comparing changes

Time:09-17

I am working on History model where I get an entity, change it, then get the same entity again as original and compare it to record changes. But I end up with 0 changes and after some investigating, I have found that Entity Framework doesn't get the original entity. It gives me the same one. Here is my code. Here I make changes

public async Task<bool> AssignUserAsync(string userId, int ticketId, string changesAuthor)
        {
            
            var ticket = await GetByIdAsync(ticketId);
            ticket.AssignedUserId = userId;
            ticket.Status = Status.Assigned;
            var result = await Update(ticket, changesAuthor);
            return result;
        }

Here I call the Update History method

 public async Task<bool> Update(Ticket ticket, string user)
            {
            if (ticket == null)
                //throw exception
                return false;

            var ticketChanges = GetTicketChanges(await GetByIdAsync(ticket.Id), ticket, user);
            
            await UpdateHistory(ticketChanges);
            ticket.DateUpdated = DateTimeOffset.Now;

            var result = await _ticketRepository.UpdateAsync(ticket);
            return result;
        }

This is my GetById

public async Task<Ticket> GetByIdAsync(int id)
    { 
        return await _db.Tickets
            .Include(p => p.Project)
            .Include(t => t.Comments)
            .Include(p=>p.AssignedUser)
            .FirstOrDefaultAsync(p => p.Id == id);
    }

Here Original and ticket to compare are the same one. I checked the db and AssignedUserId was different.

  public List<History> GetTicketChanges(Ticket originalTicket, Ticket ticketToCompare, string changesAuthor)
        {
            List<History> ticketChanges = new();

            if (originalTicket == null || ticketToCompare == null)
                //throw exception
                return ticketChanges;

            var history = new History 
            { 
                DateCreated = DateTimeOffset.Now, 
                User = changesAuthor, 
                TicketId = ticketToCompare.Id 
            };

I removed the long part where I compare the two entities. What am I doing wrong, or how could I get the original?

CodePudding user response:

This code won't work as you expect because by default the DbContext is tracking the initial instance which you are updating, so telling the DbContext to retrieve the instance again will return the same reference you just updated.

For example:

using (var context = new AppDbContext())
{
    var ticket1 = context.Tickets.Single(ticketId);
    var ticket2 = context.Tickets.Single(ticketId);
    Assert.AreSame(ticket1, ticket2); // NUnit ReferenceEquals
}

This would assert true as the DbContext would return the same reference. Editing ticket1 would result in the changes being seen on ticket2.

Alternatively:

using (var context = new AppDbContext())
{
    var ticket1 = context.Tickets.AsNoTracking().Single(ticketId);
    var ticket2 = context.Tickets.AsNoTracking().Single(ticketId);
    Assert.AreSame(ticket1, ticket2); // NUnit ReferenceEquals
}

This assert would fail as the instance returned would not be tracked so you would receive different instances.

Actions like tracking changes can be done fairly easily by overriding the DbContext OnSaveChanges method and using the ChangeTracker to inspect what entities of interest might have been updated, added, or deleted, then inspecting their contents to build an audit record. This saves you the hassle of loading entities again to compare. However, the caveat of this approach is that it's only practical to record audit changes at a per-entity basis, not across a related graph of entities. What this means is that the change tracker for a ticket will let you record the deltas for changes made to the Ticket, but not inherently the child references of the ticket. (those would be tracked individually in the change tracker)

Your problem is compounded by the structure you are using, trying to isolate everything into reused methods. I had a stab at what minimal changes might be used to get it working, but honestly it ended up even more tangled as you would have to start detaching and re-attaching references. In general it is not a good idea to design around reloading multiple separate references to the same entity. Working with detached entities would solve this issue, but I generally don't recommend that approach as it can lead to complexity trying to attach and detach entire object graphs. (entities and their related references) I would recommend looking at doing the change tracking from the DbContext, and allow it to resolve the current user from session state, (if a web app) otherwise the currently logged in user rather than passing variables through various method chains.

  • Related