C# 实体框架将仅将相关实体属性设置为";空";如果我第一次得到财产
编辑 对于在一个方向上引用另一个实体的任何实体属性,似乎都会发生这种情况。换句话说,对于下面的示例,C# 实体框架将仅将相关实体属性设置为";空";如果我第一次得到财产,c#,entity-framework,entity-framework-6,C#,Entity Framework,Entity Framework 6,编辑 对于在一个方向上引用另一个实体的任何实体属性,似乎都会发生这种情况。换句话说,对于下面的示例,Bar覆盖相等的事实似乎无关紧要 假设我有以下几个类: public class Foo { public int? Id { get; set; } public virtual Bar { get; set; } } public class Bar : IEquatable<Bar> { public int Id { get; set; }
Bar
覆盖相等的事实似乎无关紧要
假设我有以下几个类:
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();
}
}
那么属性实际上不会改变
如果我这样做:
var throwAway = foo.Bar;
foo.Bar = null;
然后该属性将实际设置并另存为null
由于Foo.Bar
属性只是一个虚拟的、自动实现的属性,因此我只能得出结论,这与延迟加载和实体框架代理有关-但是为什么这个特定场景会导致问题,我不知道
为什么实体框架的行为是这样的,我如何才能让它真正可靠地设置
null
?你是对的-这是因为你在EF中使用了延迟加载(virtual
属性)。您可以删除virtual
(但这对您来说可能是不可能的)。您在question-call属性中描述的其他方法,并将其设置为null
您也可以在SO上阅读有关此问题的信息。作为一种解决方法,我发现缓解此问题的最简单方法是让setter在将backing字段设置为null之前调用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;
}
}
}
这样,无论其他代码是否实际加载了该属性,该属性都可以工作,但可能会导致不必要的实体加载。使其工作的一种方法是使用属性API:
var foo = context.Foos.Find(1);
context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;
context.SaveChanges();
这样做的好处是,无需通过延迟加载加载
foo.Bar
,也可用于不支持延迟加载或更改跟踪代理(无virtual
properties)的纯POCO。缺点是,您需要一个上下文
实例,该实例位于要将相关条
设置为空
的位置,我对官方解决方案不满意:
context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;
因为POCO对象的用户需要太多的上下文知识。我的解决方案是在将值设置为null时触发惰性属性的加载,这样我们就不会从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);
}
受保护的bool LazyPropertyIsNull(T currentValue),其中T:DbEntity
{
返回值(currentValue==null);
}
将属性传递给LazyPropertyIsNull函数将触发延迟加载,并进行正确的比较
请在:上投票支持这个问题,我个人认为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,它将首先延迟加载
entity.SetToNull(e => e.ReferenceProperty);
如果要避免操纵
EntityEntry
,可以通过在POCO中包含FK属性(如果不希望用户具有访问权限,可以将其设置为私有)并在setter值
为null时让Nav属性setter将FK值设置为null来避免对数据库的延迟加载调用。例如:
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" );
}
}
我的猜测是,延迟加载实现为
bartypebar{get{if(base.Bar==null){base.Bar=(lazyly load);}返回base.Bar;}设置{base.Bar=value;}}
,其中base.Bar
没有看到从null
(尚未加载)到null
(无)的更改。但如果这是正确的,我认为这是一个bug,值得在他们的CodePlex页面上向EF人员报告。@hvd我打开了,所以我们会看看他们怎么说。在我的例子中,上下文不容易获得,但这仍然是一个好消息。这可能会很昂贵,取决于模型。只有在新值为null时才执行延迟加载更有效,因为只有在这些情况下才会忽略新值。我在下面的回答中给出了一个例子。有关避免不必要的DB查询的方法,请参见我的回答。这是一个不错的解决方案,但代价是为每个受影响的属性引入额外的FK字段,并为每个接受的属性手动配置。鉴于此问题仅在将属性设置为null时发生,是否值得?也许如果我们可以在FK字段中使用一个属性来自动配置,这将减轻痛苦。@RobKent-当然,DataAnnotation
属性可以正常工作;在任何情况下,如果它避免了一个浪费的DB查询,对我来说都是值得的,但是还有其他的选择,比如如果知道他们可能会更改属性,那么就立即加载属性,或者,如另一个答案中所述,直接操作EntityEntry
(但是用户需要访问一些人不允许的DbContext
,但即使这样,他们也可以在存储库中创建helper方法,例如,将属性设置为null。鉴于EF Core不允许延迟加载,并且在某个时候我们必须尝试将EF6应用程序移植过来,我最初的问题是l。)我想建议应该是:选择EF Core,或者,如果你被EF 6困住了(像我一样!),不要使用延迟加载。EF Core现在支持延迟加载这很有趣,谢谢。这应该会让我们的应用程序移植更容易。
entity.SetToNull(e => e.ReferenceProperty);
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}" );
}
}