使AddOrUpdate仅更改某些属性

时间:2014-09-19 07:14:33

标签: c# entity-framework ef-migrations

这可能是一个简单的问题,但我是Code First和Migrations的新手,所以请耐心等待。我会将示例代码保持在最低限度以显示问题:

我有一个BaseAuditableEntity,其中包含此内容(除其他外,但让我们简化):

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

现在(例如)User POCO继承自它:

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; }
}

我在我的上下文SaveChanges方法中有这个,以填写CreatedOnLastModified日期(简化):

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();
}

现在我有一个移植到一些用户,如下:

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.
    );
}

现在我在迁移后使用AddOrUpdate播种时遇到问题。当实体是新的(它被添加)时,CreatedOn被正确填充,一切都按预期工作。但是,当实体被修改(它已经存在于数据库中且UserName匹配)时,它会尝试使用我创建的新实体更新它...这会失败,因为CreatedOn有一个无效DateTime(在这种情况下,DateTime.MinValue)。

有没有办法使用AddOrUpdate方法,以便它实际从数据库中检索匹配的实体,只更新非默认字段?或者也许某种方式告诉它哪些字段不更新?对于这种特定情况,我希望CreatedOn字段保持不变,但我们会感谢通用解决方案。

也许我应该使用自己的AddOrUpdate方法,其中包含一个带有我想要更改的字段的谓词,而不是将其传递给一个全新的实体?

这是EF 6.1

更新

我知道我可以在CreatedOn日期轻松解决此问题,这就是我目前针对此特定情况所做的事情:

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;
}

我正在寻找更通用的解决方案

2 个答案:

答案 0 :(得分:11)

AddOrUpdate的实施使用CurrentValues.SetValues,以便修改所有标量属性。

我已将功能扩展为在更新时接受要修改的属性,否则只需使用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>();
    }
}

使用。

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",
    }
);

答案 1 :(得分:0)

我遇到了Expression的问题。@ Yuliam的答案等于类型可以为空,并且必须添加以下内容。

var matches = identifyingProperties.Select(pi => 
     Expression.Equal(Expression.Property(parameter, pi.Name), 
     Expression.Convert(Expression.Constant(pi.GetValue(entity, null)), 
     Expression.Property(parameter, pi.Name).Type)));

我还更新了它以首先获取所有记录,因此“ SingleOrDefault”不会在每个for循环迭代中执行sql查询。

我还设置了AddRange,它的性能要好一些。

这是我解决方案的要点。感谢您发布此Yuliam!我一直在寻找类似的东西。

https://gist.github.com/twilly86/eb6b61a22b66b4b33717aff84a31a060