Search code examples
c#wcfentity-framework-6odatawcf-data-services-client

Entity Framework 6 - DataServiceContext Detect Has Changes


I have a WCF server application running Entity Framework 6.

My client application consumes OData from the server via a DataServiceContext, and in my client code I want to be able to call a HasChanges() method on the context to see if any data in it has changed.

I tried using the following extension method:

    public static bool HasChanges(this  DataServiceContext ctx)
    {
        // Return true if any Entities or links have changes
        return ctx.Entities.Any(ed => ed.State != EntityStates.Unchanged) || ctx.Links.Any(ld => ld.State != EntityStates.Unchanged);
    }

But it always returns false, even if an entity it is tracking does have changes.

For instance, given that I have a tracked entity named Customer, the following code always returns before calling SaveChanges().

    Customer.Address1 = "Fred"
    if not ctx.HasChanges() then return
    ctx.UpdateObject(Customer)
    ctx.SaveChanges()

If I comment out the if not ctx.HasChanges() then return line of code, the changes are saved successfully so I'm happy that the entity has received the change and is able to save it.

It seems that the change is getting tracked by the context, just that I can't determine that fact from my code.

Can anyone tell me how to determine HasChanges on a DataServiceContext?


Solution

  • Far out. I just read through DataServiceContext.UpdateObjectInternal(entity, failIfNotUnchanged), which is called directly from UpdateObject(entity) with a false argument.

    The logic reads like:

    • If already modified, return; (short-circuit)
    • If not unchanged, throw if failIfNotUnchanged; (true only from ChangeState())
    • Else set state to modified. (no data checks happened)

    So by the looks of it, UpdateObject doesn't care about/check the internal state of the entity, just the State enum. This makes updates feel a little inaccurate when there are no changes.

    However, I think your problem is then in the OP 2nd block of code, you check your extension HasChanges before calling UpdateObject. The entities are only glorified POCOs (as you can read in your Reference.cs (Show Hidden Files, then under the Service Reference)). They have the obvious properties and a few On- operations to notify about changing. What they do not do internally is track state. In fact, there is an EntityDescriptor associated to the entity, which is responsible for state-tracking in EntityTracker.TryGetEntityDescriptor(entity).

    Bottom line is operations actually work very simply, and I think you just need to make your code like

    Customer.Address1 = "Fred";
    ctx.UpdateObject(Customer);
    if (!ctx.HasChanges()) return;
    ctx.SaveChanges();
    

    Though as we know now, this will always report HasChanges == true, so you may as well skip the check.

    But don't despair! The partial classes provided by your service reference may be extended to do exactly what you want. It's totally boilerplate code, so you may want to write a .tt or some other codegen. Regardless, just tweak this to your entities:

    namespace ODataClient.ServiceReference1  // Match the namespace of the Reference.cs partial class
    {
        public partial class Books  // Your entity
        {
            public bool HasChanges { get; set; } = false;  // Your new property!
    
            partial void OnIdChanging(int value)  // Boilerplate
            {
                if (Id.Equals(value)) return;
                HasChanges = true;
            }
    
            partial void OnBookNameChanging(string value)  // Boilerplate
            {
                if (BookName == null || BookName.Equals(value)) return;
                HasChanges = true;
            }
            // etc, ad nauseam
        }
        // etc, ad nauseam
    }
    

    But now this works great and is similarly expressive to the OP:

    var book = context.Books.Where(x => x.Id == 2).SingleOrDefault();
    book.BookName = "W00t!";
    Console.WriteLine(book.HasChanges);
    

    HTH!