After upgrading from .NET Framework with EF to .NET 8 with EF Core, I am unable to modify my SalesQuotes
.
I am using table per type strategy.
This code fails:
var connect = HandyDefaults.MakeConnectFromConnectionString(connectionString);
var quote = connect.SalesQuotes.SingleOrDefault(x => x.DocumentNumber == "SQ000017");
var guidString = Guid.NewGuid().ToString();
quote.CustomerReference = guidString;
connect.SaveChanges();
connect = HandyDefaults.MakeConnectFromConnectionString(connectionString);
var quote2 = connect.SalesQuotes.SingleOrDefault(x => x.DocumentNumber == "SQ000017");
Assert.AreEqual(guidString, quote2.CustomerReference); // fails
Where the DbContext
is constructed as:
public static MyEFCoreDbContext MakeConnectFromConnectionString(string connectionString)
{
var optionsBuilder = new DbContextOptionsBuilder<MyEFCoreDbContext>()
.UseSqlServer(connectionString)
.UseChangeTrackingProxies()
.UseObjectSpaceLinkProxies();
return new MyEFCoreDbContext(optionsBuilder.Options);
}
And in OnModelCreating
, I have:
modelBuilder.SetOneToManyAssociationDeleteBehavior(DeleteBehavior.SetNull, DeleteBehavior.Cascade);
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues);
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction);
as well as
modelBuilder.Entity<SalesQuoteLine>()
.HasOne(j => j.SalesQuote)
.WithMany(x => x.Lines)
.HasForeignKey(x => x.SalesQuote_Id)
.OnDelete(DeleteBehavior.Cascade);
The business model is something like
public class SalesQuote : BaseSalesHeader
{
public SalesQuote()
{
Lines = new ObservableCollection<SalesQuoteLine>();
}
public virtual ObservableCollection<SalesQuoteLine> Lines { get; set; }
// other properties
}
With BaseSalesHeader
similar to:
public abstract class BaseSalesHeader
{
// contains properties common to different sales documents.
public virtual string CustomerReference { get; set; }
}
I am using Entity Framework Core v8.01, .NET 8, C#, SQL Server.
I am able to add SalesQuotes
but not modify them.
Update: I changed the unit test code as follows:
var connect = HandyDefaults.MakeConnectFromConnectionString(connectionString);
var quote = connect.SalesQuotes.SingleOrDefault(x => x.DocumentNumber == "SQ000017");
var guidString = Guid.NewGuid().ToString();
Debug.WriteLine($"Original CustomerReference is {quote.CustomerReference}");
quote.CustomerReference = guidString;
Debug.WriteLine($"Modified CustomerReference is {quote.CustomerReference}");
connect.SaveChanges();
connect = HandyDefaults.MakeConnectFromConnectionString(connectionString);
var quote2 = connect.SalesQuotes.SingleOrDefault(x => x.DocumentNumber == "SQ000017");
Debug.WriteLine($"Retrieved CustomerReference is {quote2.CustomerReference}");
Assert.AreEqual(guidString, quote2.CustomerReference); // fails
The debug output is
Original CustomerReference is DC Replacement
Modified CustomerReference is 8944f1f4-0df4-4a3b-bdd8-85b0d80c792f
Retrieved CustomerReference is DC Replacement
Update #2: hovering over .UseChangeTrackingProxies()
, I see this:
linking here for notification entries
I have added the xaf tag because the code is in a DevExpress XAF solution.
Update #3: I tried calling
connect.ChangeTracker.DetectChanges();
before
connect.SaveChanges();
but it made no difference.
Update #4
After reading Steve Py's very helpful answer I realised that I had added not mentioned my full declaration of BaseSalesHeader
public abstract class BaseSalesHeader : BaseDocumentBo, IObjectSpaceLink, IHasSequencedLines , INotifyPropertyChanged
My Test passes when I remove INotifyPropertyChanged
I created a new Business Object from the Dev Express Wizard as follows, and notice the code mentions
You do not need to implement the INotifyPropertyChanged interface - EF Core implements it automatically. And links to Change Detection and Notification.
// Register this entity in your DbContext (usually in the BusinessObjects folder of your project) with the "public DbSet<EntityObject1> EntityObject1s { get; set; }" syntax.
[DefaultClassOptions]
//[ImageName("BO_Contact")]
//[DefaultProperty("Name")]
//[DefaultListViewOptions(MasterDetailMode.ListViewOnly, false, NewItemRowPosition.None)]
// Specify more UI options using a declarative approach (https://documentation.devexpress.com/#eXpressAppFramework/CustomDocument112701).
// **You do not need to implement the INotifyPropertyChanged interface - EF Core implements it automatically.**
// (see [https://learn.microsoft.com/en-us/ef/core/change-tracking/change-detection#change-tracking-proxies][3] for details).
public class EntityObject1 : BaseObject
{
public EntityObject1()
{
// In the constructor, initialize collection properties, e.g.:
// this.AssociatedEntities = new ObservableCollection<AssociatedEntityObject>();
}
// You can use the regular Code First syntax.
// Property change notifications will be created automatically.
// (see https://learn.microsoft.com/en-us/ef/core/change-tracking/change-detection#change-tracking-proxies for details)
//public virtual string Name { get; set; }
// Alternatively, specify more UI options:
[XafDisplayName("My display name"), ToolTip("My hint message")]
[ModelDefault("EditMask", "(000)-00"), VisibleInListView(false)]
[RuleRequiredField(DefaultContexts.Save)]
public virtual string Name { get; set; }
// Collection property:
//public virtual IList<AssociatedEntityObject> AssociatedEntities { get; set; }
//[Action(Caption = "My UI Action", ConfirmationMessage = "Are you sure?", ImageName = "Attention", AutoCommit = true)]
//public void ActionMethod() {
// // Trigger custom business logic for the current record in the UI (https://documentation.devexpress.com/eXpressAppFramework/CustomDocument112619.aspx).
// this.PersistentProperty = "Paid";
//}
}
From the documentation from EF, I suspect the issue is your use of:
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues);
ChangingAndChangedNotificationsWithOriginalValues
To use this strategy, the entity class must implement INotifyPropertyChanged and INotifyPropertyChanging. Original values are recorded when the entity raises the PropertyChanging. Properties are marked as modified when the entity raises the PropertyChanged event.
This setting expects entities to use INotifyPropertyChanged & INotifyProperyChanging. Removing this should use Snapshot which relies on the DetectChanges
call automatically.
Edit: Additionally if you want to use that change tracking strategy, it expects both INotifyPropertyChanged
and INotifyPropertyChanging
which AFAIK is not going to work with auto-properties:
public virtual string CustomerReference { get; set; }
... Those events work with field-based property accessors so that the NotifiyPropertyChanged/ing event listeners can be wored it. Which makes combined with PropertyAccessMode.PreferFieldDuringConstruction
If using a change tracker that ties into NotifyPropertyChanged/ing you wouldn't want it constantly tripping event handlers when populating every property on every entity:
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private string _customerReference;
public virtual string CustomerReference
{
get => _customerReference;
set
{
if(value == _customerReference) return;
var result = RaisePropertyChanging(nameof(CustomerReference), value);
if (result)
{
_customerReference = value;
RaisePropertyChanged(nameof(CustomerReference), value);
}
}
}
where the "RaisePropertyChanging" and "RaisePropertyChanged" methods are wrappers to check for whether the event handlers are set and pass-through to the event handlers. Changing would check for a cancellation and return back a True or False based on if any listener rejected the change or not.