这可能是一个简单的问题,但我是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
方法中有这个,以填写CreatedOn
和LastModified
日期(简化):
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;
}
我正在寻找更通用的解决方案
答案 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