使AddOrUpdate仅更改某些属性

Jcl*_*Jcl 9 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; }
}
Run Code Online (Sandbox Code Playgroud)

现在(例如)UserPOCO继承自它:

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; }
}
Run Code Online (Sandbox Code Playgroud)

我在上下文的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();
}
Run Code Online (Sandbox Code Playgroud)

现在我有一个移植到位,为一些用户提供种子,如下所示:

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.
    );
}
Run Code Online (Sandbox Code Playgroud)

现在我有一个关于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;
}
Run Code Online (Sandbox Code Playgroud)

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

Yul*_*dra 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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

用法.

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",
    }
);
Run Code Online (Sandbox Code Playgroud)

  • @Jcl,你是对的,这可以防止不必要的更新.我相应地更新了答案 (2认同)