4

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?

Richard Moore
  • 1,133
  • 17
  • 25
  • Perhaps I don't understand the use case, but why not just call SaveChanges()? If there are no changes EF won't do anything. Presumably, EF does something similar internally, and you're just re-inventing the wheel. – Vlad274 Oct 15 '15 at 14:49
  • Thanks Vlad, I want to popup a dialogue to say "Are you sure you want to save changes" before actually saving the data. If there are no changes then I don't want to popup the dialogue. – Richard Moore Oct 15 '15 at 15:28

4 Answers4

3

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!

Todd Sprang
  • 2,899
  • 2
  • 23
  • 40
  • Todd you are awesome =) You zeroed me in on exactly where I needed to look. What I ended up with was creating a partial class with the `HasChanges()` property. In the constructor I created a delegate for PropertyChanged `PropertyChanged += Customer_PropertyChanged;` and implemented an event handler in my partial class as follows: `private void Customer_PropertyChanged(object sender, PropertyChangedEventArgs e) => HasChanges = true;`. After I have loaded the entity I set `HasChanges()` to false. And any property that fires the PropertyChanged event marks the entity as having changes. Thanks! – Richard Moore Oct 30 '15 at 15:16
  • 1
    **Additional:** Have just found a minor problem with the above solution. Creating a property `HasChanges()` in the partial class causes the entity to believe it has a database column named **HasChanges**. Instead of using a property I ended up using a private variable with a separate getter and setter method. Cheers. – Richard Moore Oct 30 '15 at 16:38
2

Is it possible you're not adding/editing your entities properly? MSDN states that you must use AddObject, UpdateObject, or DeleteObject to get change tracking to fire on the client (https://msdn.microsoft.com/en-us/library/gg602811(v=vs.110).aspx - see Managing Concurrency). Otherwise your extension method looks good.

Todd Sprang
  • 2,899
  • 2
  • 23
  • 40
  • Thanks Todd. The object in my context is loaded into the context via OData before it is accessed on the client. Something along the lines of `(from c in ctx.Customers where c.CustomerId == CustomerId select c).FirstOrDefault();` The context merge options left at defaults which is to enable change tracking. Changes are getting successfully sent back to the database, it just seems that I can't see the changes myself. – Richard Moore Oct 27 '15 at 16:50
  • Do you need to Attach()? – Todd Sprang Oct 27 '15 at 17:00
  • I hadn't seen the `Attach()` method before, thanks for the suggestion, but not sure it would help me as it looks like it's aimed at getting disconnected entities into the context so they can be updated instead of being inserted. My entity is definitely in the context and being tracked and updated fine. – Richard Moore Oct 27 '15 at 19:08
  • Todd is right. I had a look through WCF Data service code. UpdateObject is the place where the state of the Entity is set to Modified. So you ve got to call UpdateObject. I didn't thing any tracking of the previous state of the entity that would allow to check for différences. I wanted to make his answer and get the Bounty, but he was faster, and desserves it ! :-) – Emmanuel DURIN Oct 28 '15 at 21:54
  • @EmmanuelDURIN I tried calling `UpdateObject()` prior to checking for changes - this indeed sets the state of the entity to Modified. But it sets it to Modified regardless of whether the entity actually has any changes. In other words, if my entity has no changes, and I call `UpdateObject()` for it, my entity is then marked with a state of Modified when it hasn't actually been modified. Problem still unsolved I'm afraid. – Richard Moore Oct 28 '15 at 23:39
  • @RichardMoore I think it's something on your end. I built a super-trivial example with extension method, and it works fine. On a WebForms button click: `var context = new Database1Entities(new Uri("http://localhost:44033/BooksWcfDataService.svc")); var book = context.Books.Where(x => x.Id == 2).SingleOrDefault(); book.BookName = "Hello World"; var hasChanges = context.HasChanges(); Console.WriteLine(hasChanges); // False context.UpdateObject(book); hasChanges = context.HasChanges(); Console.WriteLine(hasChanges); // True context.SaveChanges();` – Todd Sprang Oct 29 '15 at 18:13
  • Thanks @ToddSprang - remove the line `book.BookName = "Hello World";` and run it again. Your final call to `HasChanges()` will be true even though there aren't any changes. – Richard Moore Oct 29 '15 at 22:20
0

In order for this to work, automatic change tracking must be enabled. You can find this setting in

ctx.Configuration.AutoDetectChangesEnabled

All the entity objects must also be tracked by the context ctx. This means that they must be returned by one of ctx's methods or explicitly added to the context.

It also means that they must be tracked by the same instance of DataServiceContext. Are you somehow creating more than one context?

The model must also be configured correctly. Perhaps Customer.Address1 is not mapped to a database column. In that case, EF will not detect changes to the column.

Jørgen Fogh
  • 7,516
  • 2
  • 36
  • 46
  • Thanks Jørgen. Configuration.AutoDetectChangesEnabled is not a property of DataServiceContext. My context is setup to do change tracking, this is working when I modify the data and call SaveChanges. I am only using once instance of the context also. All mappings are good, as I said the code is reading and writing data fine, I just can't detect the changes myself. – Richard Moore Oct 27 '15 at 16:43
0

I doubt that the datacontext in client is not the same one.so the changes is always is false.

You must be sure the Datacontext is the same one(instance), for every changes of the Datacontext. Then to detect the changes is meaningful.

Another way ,you must tracked the changes by yourself.simply using the Trackable Entities to help you tracking the changes of entities in the datacontext.

BTW. I Use the Code ' ctx.ChangeTracker.HasChanges()' to detect the changes of DataContext.

    public bool IsContextDirty(DataServiceContext ctx)
    {
#if DEBUG
        var changed = ctx.ChangeTracker.Entries().Where(t => t.State != EntityState.Unchanged).ToList();
        changed.ForEach(
            (t) => Debug.WriteLine("entity Type:{0}", t.Entity.GetType()));
#endif
        return ctx != null && ctx.ChangeTracker.HasChanges();
    }
huoxudong125
  • 1,966
  • 2
  • 26
  • 42
  • Thanks huoxudong125. The context I am using is the same instance that was used to load the entity, changes are successfully saved when `SaveChanges()` is called. I am creating the DataServiceContext on an OData client, and `ChangeTracker` is not a member of DataServiceContext in that scenario. – Richard Moore Oct 29 '15 at 10:43