Breeze / breeze.server.net

Breeze support for .NET servers
MIT License
76 stars 62 forks source link

Updates cause all properties of an entity to be marked as modified #188

Open christianacca opened 1 year ago

christianacca commented 1 year ago

When an EntityInfo whose state is modified is processed by EFPersistenceManager.HandleModified all properties in the corresponding EF Core EntityEntry is marked as being modified.

The exact line that causes this is in MarkEntryAndOwnedChildren:

entry.State = state;

I'm not entirely sure this is a bug or by design? It seems unintentional given the comment here:

image

There was an old related issue but @jtraband closed that one but wasn't sure why?

I wonder if this behaviour can be changed such that not all properties are modified? Or maybe a configuration option to allow this behaviour?

steveschmitt commented 11 months ago

I think the reason is that the entityInfo.OriginalValuesMap cannot be trusted to provide all the fields that might be changed. For example, entity changes could happen in a BeforeSaveEntities handler that might not update the OriginalValuesMap.

I wouldn't want to change this default behavior, because it might break some existing Breeze applications, but it might make sense to make it configurable via something in BreezeConfig.

christianacca commented 11 months ago

That makes sense. Would you be willing to accept a PR for this change?

steveschmitt commented 11 months ago

Yes please! A PR would be great.

steveschmitt commented 10 months ago

@christianacca Have you made any progress on this?

christianacca commented 10 months ago

Nope! started this weekend building out some test cases around owned entities. Realistically, I would be surprised if I'm able to get a working PR in the next 4 weeks.

iulianb commented 3 months ago

entity changes could happen in a BeforeSaveEntities handler

We have this scenario and so far I haven't been able to come up with a workaround.

Some of our entities are mapped to database views and they can be edited on the client. When the changes reach the server we have some bits of infrastructure that "convert" the view entities to their table mapped counterparts. Setting the state to unchanged in HandleModified discards the updates done through the converters and there's no step between that and SaveChangesCore that would allow us to reapply the current values.

I'm thinking that "accepting" changes (by manipulating OriginalValues) on the table entity properties and leaving only the entity as modified in the BeforeSaveEntities step would be an acceptable workaround but haven't got to it yet.

LE: seems to be working

var entry = _context.ChangeTracker.Entries().FirstOrDefault(entry => entry.Entity == convertedEntity);

foreach (var property in _context.Model.FindEntityType(convertedEntity.GetType()).GetProperties())
{
    var current = entry.CurrentValues[property];
    var original = entry.OriginalValues[property];

    if (current != original)
    {
        entry.OriginalValues.SetValues(new Dictionary<string, object>()
        {
            { property.Name, current },
        });
    }
}
steveschmitt commented 3 months ago

@iulianb I've also faced a case where I had to update "view" entities on the client. Ultimately I handled it on the client: when it was time to save, I queried the appropriate table entities corresponding to the changed view entities, updated them, and saved them. This is easier than removing and adding entities in BeforeSaveEntities, but it does have a performance cost because the table entities are being brought to the client.

To handle it in BeforeSaveEntities would require similar logic. You would need to query the table entities, add them to the SaveMap, update their properties from the corresponding view entities, and remove the view entities from the SaveMap.

It reminds me a bit of this StackOverflow answer, but of course this is a different problem.

iulianb commented 3 months ago

@steveschmitt

To handle it in BeforeSaveEntities would require similar logic. You would need to query the table entities, add them to the SaveMap, update their properties from the corresponding view entities, and remove the view entities from the SaveMap.

That is exactly what we're doing in the converters, which are basically command handlers executed before the handlers for create/update/delete. We've opted for this approach because in some scenarios the conversion is not 1 to 1 (eg. one view entity needs to be saved to multiple tables) and we wanted to provide developers with some degree of control over how the conversion happens. After conversion the entities go through the create/update/delete flow as there might be other implementation details, depending on the entity.

As stated in the previous reply, the issue is on updated view entities. Their table counterparts are automatically tracked by EF when queried and any property update on them ends up in the state tracker. When the entity state is set to Unchanged (happens in HandleModified) the change tracker discards these updates and returns the entity to its original state. UpdateOriginalValues does set the original values and updates the state but their current value is lost. The bit of code I posted earlier attempts to set the updated values as original in the state tracker. I'm not 100% sure that it's the correct approach but saving an entity whose tracking information has been altered in this manner seems to work correctly (in the sense that the updated value ends up in the database).