c# - 使 AddOrUpdate 仅更改某些属性

标签 c# entity-framework entity-framework-migrations

这可能是一个简单的问题,但我是 Code First 和迁移的新手,所以请多多包涵。我会将示例代码保持在最低限度以显示问题:

我有一个 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;
}

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

最佳答案

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

关于c# - 使 AddOrUpdate 仅更改某些属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25928353/

相关文章:

c# - ASP.NET MVC。如何禁用基于参数的必需验证?

sql-server - SQL Azure - 登录后阶段超时时间已过

azure - 使用远程连接字符串在 Azure 上执行代码优先迁移

vb.net - 如何覆盖默认的 SQL 迁移生成器?

c# - 记录谁订阅了事件

c#时间计算

C#字典以列表为值,可以直接对列表进行操作吗?

c# - LINQ to Entities 和多态性

c# - 如何使用 linq-to-entities 构建类似 selectbyexample 的查询?

entity-framework - Entity Framework 迁移停止检测POCO更新