After more debugging, I'm heading to bed, but this is what I've found
out: our change tracking isn't tracking changes. :-)
Specifically, when you have code like this (using something
NUnit-testable instead of from NerdDinner):
Category c = GetExistingCategory();
Product p = new Product {...};
c.Products.Add(p);
db.SubmitChanges();
the newly added product isn't actually added to the database. The
problem appears to be because of MemberModificationHandler: it only
tracks INotifyPropertyChanged and not INotifyPropertyChanging (see
MemberModificationHandler.RegisterNotification()).
So, what happens is c is a tracked object (yay). As such, it is
registered with the EntityTracker (double yay). However, for change
tracking to work, MemberModificationHandler uses the
INotifyPropertyChanged interface. In this case, c.Products.Add(p)
doesn't actually invoke
MemberModificationHandler.OnPropertyChangedEvent(), because it isn't
registered.
Specifically, the registered Category fires the
INotifyPropertyChanging.PropertyChanging event, because (as far as it
knows) c.Products will be changing. However, c doesn't know what, if
anything, will change. c.Products.Add(p) eventually calls into
Category.attach_Products(), which then effectively does 'p.Category =
c'. This last expression is what fires the
INotifyPropertyChanged.PropertyChanged event, but only on p, which
hasn't been registered with anything, and thus there are no handlers
registered.
Oops.
The only solution I can think of is that MemberModificationHandler needs
to listen to the INotifyPropertyChanging.PropertyChanging event (if
available), and if it's fired it needs to check for changes on all
properties of the changing object.
So I did this, and it still fails (see attached patch, which includes
the unit test). It properly detects that 'c' may have changed, but
QueryRunner.Update() doesn't do a "deep" update; that is, it only
updates the Category properties (CategoryName, Description, etc.) but
not the values of anything attached to it (like the Category.Products
collection).
Any ideas on how to resolve this?
Thanks,
- Jon
On Sun, 2009-05-10 at 22:02 -0400, Jonathan Pryor wrote:
> I already fixed this, and it has been committed to svn.
> DataContext.SetEntitySetQueries() was being called because of
> DataContext._GetOrRegisterEntity(), which in turn was called by
> DataContext.Register(), which was called by
> DataContext.SubmitChanges(). (Just check the callstack in the
> original message...) The problem was twofold: (1) DbLinq's
> EntitySet<T> was screwy (though I blame a lot of that screwiness
> on .NET's EntitySet<T> -- see the new EntitySetTest.cs file, and laugh
> hysterically when you see when the EntitySet<T>.ListChanged event is
> raised), and (2) calling dinner.RSVPs.Add(rsvp) caused the
> EntitySet<RSVP> source to be created, and EntitySet<T>.SetSource()
> cannot be called again once EntitySet<T>.HasLoadedOrAssignedValues is
> true. Since DataContext.SetEntitySetsQueries() tried to do just that,
> it threw an exception.
>
> Now I'm working on a related bug: subsequent updates aren't tracked.
> To wit:
>
> Dinner dinner = new Dinner {...};
> RSVP rsvp = new RSVP();
> rsvp.AttendeeName = User.Identity.Name;
> dinner.RSVPs.Add(rsvp);
> db.Dinners.InsertOnSubmit(dinner);
> db.SubmitChanges();
>
> // The above now works. Now...
>
> RSVP rsvp2 = new RSVP();
> rsvp2.AttendeeName = "whatever";
> dinner.RSVPs.Add(rsvp2);
> db.SubmitChanges();
> // rsvp2 is NOT submitted to the database.
>
> I'm still tracking down why this happens. The attached patch file
> recreates this same scenario within the unit tests, and also
> (thankfully) fails.
>
> What I would appreciate is if you could look into the cache issues
> that I'm seeing. :-)
>
> Thanks,
> - Jon
>
> On Sun, 2009-05-10 at 23:41 +0100, Giacomo Tesio wrote:
>
> > I think this is due to the "dinner.RSVPs.Add(rsvp);" line where you
> > actually fill the EntitySet.
> >
> > The DataContext.SetEntitySetQueries() should not run on just
> > inserted entity, since they are yet filled.
> >
> >
> > Tomorrow I'll try to fix this...
> >
> >
> > Giacomo
> >
> >
> > On Fri, May 8, 2009 at 4:59 AM, Jonathan Pryor <[email protected]>
> > wrote:
> >
> > Still continuing with my "NerdDinner on Mono" effort, and
> > hitting the following issue:
> >
> > When I attempt to add a new Dinner, NerdDinner effectively
> > does:
> >
> > Dinner dinner = new Dinner {...};
> > RSVP rsvp = new RSVP();
> > rsvp.AttendeeName = User.Identity.Name;
> > dinner.RSVPs.Add(rsvp);
> > db.Dinners.InsertOnSubmit(dinner);
> > db.SubmitChanges();
> >
> > This fails, with:
> >
> > System.Reflection.TargetInvocationException: Exception has
> > been thrown by the target of an invocation. --->
> > System.InvalidOperationException: The EntitySet is already loaded and the
> > source cannot be changed.
> > at
> > System.Data.Linq.EntitySet`1[NerdDinner.Models.RSVP].SetSource
> > (IEnumerable`1 entitySource) [0x00000]
> > at (wrapper managed-to-native)
> > System.Reflection.MonoMethod:InternalInvoke
> > (object,object[],System.Exception&)
> > at System.Reflection.MonoMethod.Invoke (System.Object
> > obj, BindingFlags invokeAttr, System.Reflection.Binder binder,
> > System.Object[] parameters, System.Globalization.CultureInfo culture)
> > [0x000ca] in
> > /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MonoMethod.cs:169
> >
> > --- End of inner exception stack trace ---
> > at System.Reflection.MonoMethod.Invoke (System.Object
> > obj, BindingFlags invokeAttr, System.Reflection.Binder binder,
> > System.Object[] parameters, System.Globalization.CultureInfo culture)
> > [0x000e5] in
> > /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MonoMethod.cs:179
> >
> > at System.Reflection.MethodBase.Invoke (System.Object
> > obj, System.Object[] parameters) [0x00000] in
> > /home/jon/Development/mono-HEAD/mcs/class/corlib/System.Reflection/MethodBase.cs:111
> >
> > at System.Data.Linq.DataContext.SetEntitySetsQueries
> > (System.Object entity) [0x00173] in
> > /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:608
> >
> > at System.Data.Linq.DataContext._GetOrRegisterEntity
> > (System.Object entity) [0x00015] in
> > /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:468
> >
> > at System.Data.Linq.DataContext.Register (System.Object
> > entity) [0x0000d] in
> > /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:675
> >
> > at System.Data.Linq.DataContext.SubmitChanges
> > (ConflictMode failureMode) [0x000a6] in
> > /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:378
> >
> > at System.Data.Linq.DataContext.SubmitChanges ()
> > [0x00000] in
> > /home/jon/Development/mono-HEAD/mcs/class/System.Data.Linq/src/DbLinq/Data/Linq/DataContext.cs:339
> >
> > at NerdDinner.Models.DinnerRepository.Save () [0x00000]
> > at NerdDinner.Controllers.DinnersController.Create
> > (NerdDinner.Models.Dinner dinner) [0x00000]
> >
> > Suffice it to say, this doesn't happen under .NET's
> > System.Data.Linq. I'm wondering if anyone has seen a
> > similar error to this before, and/or knows what an
> > appropriate fix would be.
> >
> > Thanks,
> > - Jon
> >
> >
> >
> >
> >
> >
> >
> >
>
>
> >
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"DbLinq" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to
[email protected]
For more options, visit this group at
http://groups.google.com/group/dblinq?hl=en
-~----------~----~----~----~------~----~------~--~---
Index: src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs
===================================================================
--- src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs (revision 1062)
+++ src/DbLinq/Data/Linq/Implementation/MemberModificationHandler.cs (working copy)
@@ -178,10 +178,18 @@
return;
modifiedProperties[entity] = new Dictionary<string, MemberInfo>();
- if (entity is INotifyPropertyChanged)
+ var entityChanged = entity as INotifyPropertyChanged;
+ if (entityChanged != null)
{
- ((INotifyPropertyChanged)entity).PropertyChanged += (OnPropertyChangedEvent);
+ entityChanged.PropertyChanged += OnPropertyChangedEvent;
}
+
+ var entityChanging = entity as INotifyPropertyChanging;
+ if (entityChanging != null)
+ {
+ entityChanging.PropertyChanging += OnPropertyChangingEvent;
+ }
+
// then check all properties, and note them as changed if they already did
if (!ReferenceEquals(entity, entityOriginalState)) // only if we specified another original entity
{
@@ -208,6 +216,12 @@
SetPropertyChanged(sender, e.PropertyName);
}
+ private void OnPropertyChangingEvent(object entity, PropertyChangingEventArgs e)
+ {
+ foreach (var p in entity.GetType().GetProperties())
+ modifiedProperties[entity][p.Name] = p;
+ }
+
/// <summary>
/// Unregisters an entity.
/// This is useful when it is switched from update to delete list
Index: src/DbLinq/Test/Providers/WriteTest.cs
===================================================================
--- src/DbLinq/Test/Providers/WriteTest.cs (revision 1069)
+++ src/DbLinq/Test/Providers/WriteTest.cs (working copy)
@@ -786,9 +786,22 @@
db.Categories.InsertOnSubmit(category);
db.SubmitChanges();
+ var p2 = new Product
+ {
+ Discontinued = true,
+ ProductName = "Another Test Product",
+ };
+ category.Products.Add(p2);
+ db.SubmitChanges();
+
+ Assert.AreEqual(1, db.Products.Where(p => p.ProductName == "Another Test Product").Count());
+
db.Products.DeleteOnSubmit(product);
+ db.Products.DeleteOnSubmit(p2);
db.Categories.DeleteOnSubmit(category);
db.SubmitChanges();
+
+ Assert.AreEqual(0, db.Categories.Where(c => c.CategoryName == "My New Category").Count());
}
}
}