修改
这似乎发生在任何实体属性中,该属性在一个方向上引用另一个实体。换句话说,对于以下示例,Bar
覆盖Equality的事实似乎无关紧要。
假设我有以下类:
public class Foo
{
public int? Id { get; set; }
public virtual Bar { get; set; }
}
public class Bar : IEquatable<Bar>
{
public int Id { get; set; }
public override bool Equals(object obj)
{
var other = obj as Bar;
return Equals(other);
}
public bool Equals(Bar other)
{
if (object.Equals(other, null))
return false;
return this.Id == other.Id;
}
public static bool operator ==(Bar left, Bar right)
{
return object.Equals(left, right);
}
public static bool operator !=(Bar left, Bar right)
{
return !object.Equals(left, right);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}
请注意,在这里,“Bar”故意具有“Id”相等性,因为它或多或少代表一个查找表 - 因此任何具有相同Id的两个对象引用应始终被视为相同。
这是奇怪的部分,当我将Foo.Bar
设置为另一个Bar
实例时,一切正常 - 所有内容都按预期更新。
但是,当foo
从Bar
检索到DbContext
时,foo.Bar = null
已存在var throwAway = foo.Bar;
foo.Bar = null;
:
Foo.Bar
然后该属性实际上没有改变!
如果我这样做:
null
然后该属性将实际设置并保存为null。
由于{{1}}属性只是一个虚拟的,自动实现的属性,我只能得出结论,这与延迟加载和实体框架代理有关 - 但为什么这个特殊情况会导致问题,我不知道。
为什么实体框架会以这种方式运行,如何让它实际设置{{1}}?
答案 0 :(得分:2)
作为一种解决方法,我发现缓解此问题的最简单方法是让在将设置支持字段为空之前调用getter ,例如。
public class Foo
{
public int? Id { get; set; }
private Bar _bar;
public virtual Bar
{
get { return _bar; }
set
{
var entityFrameworkHack = this.Bar; //ensure the proxy has loaded
_bar = value;
}
}
}
这样,无论其他代码是否实际加载了该属性,该属性仍然有效,代价是可能不需要的实体加载。
答案 1 :(得分:1)
使其工作的一种方法是使用属性API:
var foo = context.Foos.Find(1);
context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;
context.SaveChanges();
好处是,这可以在不通过延迟加载加载foo.Bar
的情况下工作,它也适用于不支持延迟加载或更改跟踪代理(没有virtual
属性)的纯POCO。缺点是您需要在要将相关context
设置为Bar
的位置提供null
个实例。
答案 2 :(得分:0)
你是对的 - 这是因为你在EF(virtual
属性)中使用了延迟加载。您可以删除virtual
(但这对您来说可能是不可能的)。您在问题中描述的其他方式 - 调用属性,并将其设置为null。
另外,您可以在SO上阅读another topic关于此问题的内容。
答案 3 :(得分:0)
我对官方解决方法不满意:
context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;
因为它涉及POCO对象的用户过多的上下文知识。我的修复是在将值设置为null时触发lazy属性的加载,以便我们不会从EF获得误报:
public virtual User CheckoutUser
{
get { return checkoutUser; }
set
{
if (value != null || !LazyPropertyIsNull(CheckoutUser))
{
checkoutUser = value;
}
}
}
并在我的基础DbEntity类中:
protected bool LazyPropertyIsNull<T>(T currentValue) where T : DbEntity
{
return (currentValue == null);
}
将属性传递给LazyPropertyIsNull函数会触发延迟加载并进行正确的比较。
请在EF issues log上投票支持此问题:
答案 4 :(得分:0)
就我个人而言,我认为Nathan的答案(在属性设置器中延迟加载)是最可靠的。但是,它增加了您的域类(每个属性10行)的可读性。
作为另一种解决方法,我将两个方法编译为扩展方法:
public static void SetToNull<TEntity, TProperty>(this TEntity entity, Expression<Func<TEntity, TProperty>> navigationProperty, DbContext context = null)
where TEntity : class
where TProperty : class
{
var pi = GetPropertyInfo(entity, navigationProperty);
if (context != null)
{
//If DB Context is supplied, use Entry/Reference method to null out current value
context.Entry(entity).Reference(navigationProperty).CurrentValue = null;
}
else
{
//If no DB Context, then lazy load first
var prevValue = (TProperty)pi.GetValue(entity);
}
pi.SetValue(entity, null);
}
static PropertyInfo GetPropertyInfo<TSource, TProperty>( TSource source, Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
MemberExpression member = propertyLambda.Body as MemberExpression;
if (member == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a method, not a property.",
propertyLambda.ToString()));
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a field, not a property.",
propertyLambda.ToString()));
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a property that is not from type {1}.",
propertyLambda.ToString(),
type));
return propInfo;
}
如果有DbContext,则可以提供它,在这种情况下,它将使用最有效的方法并将Entry Reference的CurrentValue设置为null。
entity.SetToNull(e => e.ReferenceProperty, dbContext);
如果未提供DBContext,它将首先延迟加载。
entity.SetToNull(e => e.ReferenceProperty);
答案 5 :(得分:0)
如果您想避免操纵EntityEntry
,则可以通过在POCO中包含FK属性来避免对数据库的延迟加载调用(如果您不希望用户具有访问权限,可以将其私有化) ),并让Nav属性设置器将FK值设置为null(如果设置器value
为空)。示例:
public class InaccessibleFKDependent
{
[Key]
public int Id { get; set; }
private int? PrincipalId { get; set; }
private InaccessibleFKPrincipal _principal;
public virtual InaccessibleFKPrincipal Principal
{
get => _principal;
set
{
if( null == value )
{
PrincipalId = null;
}
_principal = value;
}
}
}
public class InaccessibleFKDependentConfiguration : IEntityTypeConfiguration<InaccessibleFKDependent>
{
public void Configure( EntityTypeBuilder<InaccessibleFKDependent> builder )
{
builder.HasOne( d => d.Principal )
.WithMany()
.HasForeignKey( "PrincipalId" );
}
}
测试:
public static void TestInaccessibleFKSetToNull( DbContextOptions options )
{
using( var dbContext = DeleteAndRecreateDatabase( options ) )
{
var p = new InaccessibleFKPrincipal();
dbContext.Add( p );
dbContext.SaveChanges();
var d = new InaccessibleFKDependent()
{
Principal = p,
};
dbContext.Add( d );
dbContext.SaveChanges();
}
using( var dbContext = new TestContext( options ) )
{
var d = dbContext.InaccessibleFKDependentEntities.Single();
d.Principal = null;
dbContext.SaveChanges();
}
using( var dbContext = new TestContext( options ) )
{
var d = dbContext.InaccessibleFKDependentEntities
.Include( dd => dd.Principal )
.Single();
System.Console.WriteLine( $"{nameof( d )}.{nameof( d.Principal )} is NULL: {null == d.Principal}" );
}
}