zzzprojects / EntityFramework-Plus

Entity Framework Plus extends your DbContext with must-haves features: Include Filter, Auditing, Caching, Query Future, Batch Delete, Batch Update, and more
https://entityframework-plus.net/
MIT License
2.21k stars 314 forks source link

EF Core AddOrUpdate #24

Open ljfolino opened 7 years ago

ljfolino commented 7 years ago

My issue is that the I am not getting the change tracking even when I attach the object to the context. This is no way I can find to only set properties to modified and set the entire object to modified. The object is coming in from a website similar to the MVC problem. I want to only audit the properties that have changed, but all properties are set to modified. I wasn't sure if you knew of a work around. I will need to write something that can loop through the properties and check manually until EF Core has this built it. I know this is not a library specific issue, but was looking for guidance.

ljfolino commented 7 years ago

I implemented my own functionality to achieve this. I created a few extension methods. I found at https://weblogs.asp.net/ricardoperes/implementing-missing-features-in-entity-framework-core. I reused most of the reload function but modified it to check each property value. I don't like the detaching re-attaching of the entities to reuse the same context, but it works, is simple, and doesn't seem to cause any issues. This does depend on some of the functionality from the above link mainly Find and GetEntityKey.

public static TEntity GetOriginalValue<TEntity>(this EntityEntry<TEntity> entry) where TEntity : class
    {
        if (entry.State == EntityState.Detached)
        {
            return entry.Entity;
        }

        var context = entry.Context;
        var entity = entry.Entity;
        var keyValues = context.GetEntityKey(entity);

        entry.State = EntityState.Detached;

        var oEntity = context.Set<TEntity>().Find(keyValues);
        var oEntry = context.Entry(oEntity);

        oEntry.State = EntityState.Detached;
        entry.State = EntityState.Unchanged;
        foreach (var prop in oEntry.Metadata.GetProperties())
        {
            var type = prop.ClrType;
            type = Nullable.GetUnderlyingType(type) ?? type;

            dynamic proposedValue = entry.Property(prop.Name).CurrentValue;
            if (proposedValue != null)
            {
                proposedValue = Convert.ChangeType(proposedValue, type);
            }
            dynamic originalValue = oEntry.Property(prop.Name).CurrentValue;
            if (originalValue != null)
            {
                originalValue = Convert.ChangeType(originalValue, type);
            }

            if (proposedValue != originalValue)
            {
                //changedProps.Add(prop.Name);
                entry.Property(prop.Name).OriginalValue = oEntry.Property(prop.Name).CurrentValue;
                entry.Property(prop.Name).IsModified = true;
            }

        }
        return entry.Entity;
    }
zzzprojects commented 7 years ago

Thank you for sharing, I'm currently looking at this feature.

AddOrUpdate has not been implemented yet by EF Team because they don't know how to manage null value (they don't want to make the same mistake from the previous version, EF6 and below).

By example, if the entity to add or update have an email property with null value, should it be updated to the database with a null value or should we keep the database value?

Entity Property Entity Value Old Database Value New Database Value
FirstName Johnny John Johnny
Email NULL z@z.com z@z.com or NULL?

In EF6, NULL would be updated, but some people report this as an issue and would like to keep instead database values when NULL is specified.

One thing that could be done is to specify with an expression to specify which property to map or ignore for the AddOrUpdate. But this solution leads very fast to some error when additional property are added to the entity, so I'm not sure to like this solution.

The biggest problem is to know whether or not the NULL is caused because no property value has been specified or the NULL is the new property value to be updated!

zzzprojects commented 7 years ago

It looks we should have at least two methods:

I'm not sure to completely like it yet but this for sure add some flexibility over existing AddOrUpdate from EF6.

AddOrUpdate

Add entity if not exists in the database or retrieves the entity and update all properties.

Some additional options could be used like which property to map/ignore when it times to update properties

AddOrUpdate(this DbSet<TEntity> set, params T[] entities)
AddOrUpdate(this DbSet<TEntity> set, Expression<Func<TEntity, object>> identityExpression, params T[] entities)
AddOrUpdate(this DbSet<TEntity> set, Expression<Func<TEntity, AddOrUpdateOptions>> options, params T[] entities)

AddOrUpdateFluent(this DbSet<TEntity> set, entities)
.Key(Expression<Func<TEntity, object>> expression)
.Map(Expression<Func<TEntity, object>> expression)
.Ignore(Expression<Func<TEntity, object>> expression)
.Execute();

GetOrAdd

Allow to retrieve an entity from the database, add it otherwise.

It works like GetOrAdd from ConcurrentDictionary, once you get it, you do whatever you want with it. It doesn't update existing value with the entity to be added.

AddOrGet(this DbSet<TEntity> set, Expression<Func<TEntity, object>> identityExpression, T entity)
ljfolino commented 7 years ago

Just want to throw the idea out there. With HTTP Restful web services you can use PATCH or POST. To me PATCH is a partial update and POST is update the whole thing. This is how I differentiate what is being updated. To make it work with EF it would be something similar you suggest with a list of properties to update, or maybe use an anonymous class that has only the properties that should be updated. You could always use a switch or separate functions to get the job done with overloads.