Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making AddOrUpdate change only some properties

This might be a simple question, however I'm new to Code First and Migrations so bear with me. I'll keep sample code to a minimum to show the problem:

I have a BaseAuditableEntity which includes this (among other things, but let's simplify):

public abstract class BaseAuditableEntity : BaseEntity, IAuditableEntity
{
  public DateTime CreatedOn { get; set; }
  public DateTime LastModified { get; set; }
}

Now a (for example) User POCO inherits from it:

public class User : BaseAuditableEntity
{
  public string UserName { get; set; }
  public string PasswordHash { get; set; }
  public string FullName { get; set; }
  public string Email { get; set; }
  public bool Active { get; set; }
  public DateTime? LastLogin { get; set; }
}

I have this on my context's SaveChanges method, to fill in the CreatedOn and LastModified dates (simplified):

public override int SaveChanges()
{
  var changeSet = ChangeTracker.Entries<IAuditableEntity>();

  if (changeSet != null)
  {
    foreach (var entry in changeSet.Where(p => p.State != EntityState.Unchanged))
    {
      var now = DateTime.UtcNow;
      if (entry.State == EntityState.Added)
        entry.Entity.CreatedOn = now;
      entry.Entity.LastModified = now;
    }
  }      
  return base.SaveChanges();
}

And now I have a migration in place that seeds some users, like this:

protected override void Seed(MyContext context)
{
  context.Users.AddOrUpdate(
      p => p.UserName,
      new User { Active = true, 
                 FullName = "My user name",
                 UserName = "ThisUser",
                 PasswordHash = "",
                 Email = "my@email",
                 LastLogin = null,
            }
      // etc.
    );
}

Now I have a problem on seeding with AddOrUpdate after the migration. When the entity is new (it's being added), CreatedOn gets filled correctly and everything works as expected. However when the entity is modified (it already exists on the database and UserName matches), it tries to update it with the new entity I'm creating... this fails because CreatedOn has an invalid DateTime (in this case, DateTime.MinValue).

Is there any way to use the AddOrUpdate method so that it actually retrieves the matching entity from the database and just update the non-default fields? Or maybe some way to tell it which fields NOT to update? For this specific case, I'd like the CreatedOn field to be unchanged, but a generic solution would be appreciated.

Maybe I should do my own AddOrUpdate method which includes a predicate with the fields I want to change, instead of passing it a completely new entity?

This is EF 6.1

Update

I know I can easily solve this for the CreatedOn date, this is what I'm currently doing for this specific case:

foreach (var entry in changeSet.Where(c => c.State != EntityState.Unchanged))
{
  var now = DateTime.UtcNow;
  if (entry.State == EntityState.Added)
  {
    entry.Entity.CreatedOn = now;
  }
  else
  {
    if (entry.Property(p => p.CreatedOn).CurrentValue == DateTime.MinValue)
    {
      var original = entry.Property(p => p.CreatedOn).OriginalValue;
      entry.Property(p => p.CreatedOn).CurrentValue = original != SqlDateTime.MinValue ? original : now;
      entry.Property(p => p.CreatedOn).IsModified = true;
     }
   }
   entry.Entity.LastModified = now;
}

I am looking for a more generic solution though

like image 242
Jcl Avatar asked Sep 19 '14 07:09

Jcl


1 Answers

The implementation of AddOrUpdate uses CurrentValues.SetValues so that all scalar properties will be modified.

I have extended the functionality to accept properties to be modified when it's an update, otherwise it's a creation, just use DbSet<T>::Add.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class SeedExtension
{
    public static void Upsert<T>(this DbContext db, Expression<Func<T, object>> identifierExpression, Expression<Func<T, object>> updatingExpression, params T[] entities)
        where T : class
    {
        if (updatingExpression == null)
        {
            db.Set<T>().AddOrUpdate(identifierExpression, entities);
            return;
        }

        var identifyingProperties = GetProperties<T>(identifierExpression).ToList();
        Debug.Assert(identifyingProperties.Count != 0);

        var updatingProperties = GetProperties<T>(updatingExpression).Where(pi => IsModifiedable(pi.PropertyType)).ToList();
        Debug.Assert(updatingProperties.Count != 0);

        var parameter = Expression.Parameter(typeof(T));
        foreach (var entity in entities)
        {
            var matches = identifyingProperties.Select(pi => Expression.Equal(Expression.Property(parameter, pi.Name), Expression.Constant(pi.GetValue(entity, null))));
            var matchExpression = matches.Aggregate<BinaryExpression, Expression>(null, (agg, v) => (agg == null) ? v : Expression.AndAlso(agg, v));

            var predicate = Expression.Lambda<Func<T, bool>>(matchExpression, new[] { parameter });
            var existing = db.Set<T>().SingleOrDefault(predicate);
            if (existing == null)
            {
                // New.
                db.Set<T>().Add(entity);
                continue;
            }

            // Update.
            foreach (var prop in updatingProperties)
            {
                var oldValue = prop.GetValue(existing, null);
                var newValue = prop.GetValue(entity, null);
                if (Equals(oldValue, newValue)) continue;

                db.Entry(existing).Property(prop.Name).IsModified = true;
                prop.SetValue(existing, newValue);                    
            }
        }
    }

    private static bool IsModifiedable(Type type)
    {
        return type.IsPrimitive || type.IsValueType || type == typeof(string);
    }

    private static IEnumerable<PropertyInfo> GetProperties<T>(Expression<Func<T, object>> exp) where T : class
    {
        Debug.Assert(exp != null);
        Debug.Assert(exp.Body != null);
        Debug.Assert(exp.Parameters.Count == 1);

        var type = typeof(T);
        var properties = new List<PropertyInfo>();

        if (exp.Body.NodeType == ExpressionType.MemberAccess)
        {
            var memExp = exp.Body as MemberExpression;
            if (memExp != null && memExp.Member != null)
                properties.Add(type.GetProperty(memExp.Member.Name));
        }
        else if (exp.Body.NodeType == ExpressionType.Convert)
        {
            var unaryExp = exp.Body as UnaryExpression;
            if (unaryExp != null)
            {
                var propExp = unaryExp.Operand as MemberExpression;
                if (propExp != null && propExp.Member != null)
                    properties.Add(type.GetProperty(propExp.Member.Name));
            }
        }
        else if (exp.Body.NodeType == ExpressionType.New)
        {
            var newExp = exp.Body as NewExpression;
            if (newExp != null)
                properties.AddRange(newExp.Members.Select(x => type.GetProperty(x.Name)));
        }

        return properties.OfType<PropertyInfo>();
    }
}

Usage.

context.Upsert(
    p => p.UserName,   
    p => new { p.Active, p.FullName, p.Email },
    new User
    {
        Active = true, 
        FullName = "My user name",
        UserName = "ThisUser",
        Email = "my@email",
    }
);
like image 178
Yuliam Chandra Avatar answered Oct 14 '22 10:10

Yuliam Chandra