实体框架4.1代码优先和自动映射器问题
考虑这个简单的模型和视图模型场景:
public class SomeModel
{
public virtual Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
public class SomeViewModel
{
public Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
出现的问题是:
我有一个不需要公司的编辑页面,所以我不从数据库中获取它。现在,当提交表单时,我会执行以下操作:
SomeModel destinationModel = someContext.SomeModel.Include("Company").Where( i => i.Id == id) // assume id is available from somewhere.
然后我执行 a
Company oldCompany = destinationModel.company; // save it before mapper assigns it null
Mapper.Map(sourceViewModel,destinationModel);
//After this piece of line my company in destinationModel will be null because sourceViewModel's company is null. Great!!
//so I assign old company to it
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
问题是,即使我将 oldCompany 分配给我的公司,在保存更改后,它在数据库中仍然为空。
注意:
如果我将这些行:更改
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
为:
context.Entry(destinationModel).State = EntityState.Modified;
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
注意我更改状态两次,它工作正常。可能是什么问题?这是 ef 4.1 的错误吗?
这是一个解决该问题的示例控制台应用程序:
using System;
using System.Linq;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using AutoMapper;
namespace Slauma
{
public class SlaumaContext : DbContext
{
public DbSet<Company> Companies { get; set; }
public DbSet<MyModel> MyModels { get; set; }
public SlaumaContext()
{
this.Configuration.AutoDetectChangesEnabled = true;
this.Configuration.LazyLoadingEnabled = true;
}
}
public class MyModel
{
public int Id { get; set; }
public string Foo { get; set; }
[ForeignKey("CompanyId")]
public virtual Company Company { get; set; }
public int? CompanyId { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyViewModel
{
public string Foo { get; set; }
public Company Company { get; set; }
public int? CompanyId { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<SlaumaContext>(new DropCreateDatabaseIfModelChanges<SlaumaContext>());
SlaumaContext slaumaContext = new SlaumaContext();
Company company = new Company { Name = "Microsoft" };
MyModel myModel = new MyModel { Company = company, Foo = "Foo"};
slaumaContext.Companies.Add(company);
slaumaContext.MyModels.Add(myModel);
slaumaContext.SaveChanges();
Mapper.CreateMap<MyModel, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyModel>();
//fetch the company
MyModel dest = slaumaContext.MyModels.Include("Company").Where( c => c.Id == 1).First(); //hardcoded for demo
Company oldCompany = dest.Company;
//creating a viewmodel
MyViewModel source = new MyViewModel();
source.Company = null;
source.CompanyId = null;
source.Foo = "foo hoo";
Mapper.Map(source, dest); // company null in dest
//uncomment this line then only it will work else it won't is this bug?
//slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
dest.Company = oldCompany;
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
slaumaContext.SaveChanges();
Console.ReadKey();
}
}
}
Consider this simple Model and ViewModel scenario:
public class SomeModel
{
public virtual Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
public class SomeViewModel
{
public Company company {get; set;}
public string name {get; set;}
public string address {get; set;}
//some other few tens of properties
}
Problem that occurs is:
I have a edit page where company is not needed so I do not fetch it from database. Now when the form is submitted I do:
SomeModel destinationModel = someContext.SomeModel.Include("Company").Where( i => i.Id == id) // assume id is available from somewhere.
Then I do a
Company oldCompany = destinationModel.company; // save it before mapper assigns it null
Mapper.Map(sourceViewModel,destinationModel);
//After this piece of line my company in destinationModel will be null because sourceViewModel's company is null. Great!!
//so I assign old company to it
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
And problem is even when I assign oldCompany to my company it is still null in database after savechanges.
Note:
If I change these lines:
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
to these:
context.Entry(destinationModel).State = EntityState.Modified;
destinationModel.company = oldCompany;
context.Entry(destinationModel).State = EntityState.Modified;
context.SaveChanges();
Notice I change the state 2 times, it works fine. What can be the issue? Is this a ef 4.1 bug?
This is a sample console application to address the issue:
using System;
using System.Linq;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using AutoMapper;
namespace Slauma
{
public class SlaumaContext : DbContext
{
public DbSet<Company> Companies { get; set; }
public DbSet<MyModel> MyModels { get; set; }
public SlaumaContext()
{
this.Configuration.AutoDetectChangesEnabled = true;
this.Configuration.LazyLoadingEnabled = true;
}
}
public class MyModel
{
public int Id { get; set; }
public string Foo { get; set; }
[ForeignKey("CompanyId")]
public virtual Company Company { get; set; }
public int? CompanyId { get; set; }
}
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyViewModel
{
public string Foo { get; set; }
public Company Company { get; set; }
public int? CompanyId { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer<SlaumaContext>(new DropCreateDatabaseIfModelChanges<SlaumaContext>());
SlaumaContext slaumaContext = new SlaumaContext();
Company company = new Company { Name = "Microsoft" };
MyModel myModel = new MyModel { Company = company, Foo = "Foo"};
slaumaContext.Companies.Add(company);
slaumaContext.MyModels.Add(myModel);
slaumaContext.SaveChanges();
Mapper.CreateMap<MyModel, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyModel>();
//fetch the company
MyModel dest = slaumaContext.MyModels.Include("Company").Where( c => c.Id == 1).First(); //hardcoded for demo
Company oldCompany = dest.Company;
//creating a viewmodel
MyViewModel source = new MyViewModel();
source.Company = null;
source.CompanyId = null;
source.Foo = "foo hoo";
Mapper.Map(source, dest); // company null in dest
//uncomment this line then only it will work else it won't is this bug?
//slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
dest.Company = oldCompany;
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
slaumaContext.SaveChanges();
Console.ReadKey();
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
默认情况下,Automapper 始终更新从源实例到目标实例的每个属性。因此,如果您不希望覆盖您的 Company 属性,那么您必须为您的映射器显式配置此属性:
到目前为止,没有任何 EF 相关的内容。但是,如果您将其与 EF 一起使用,则必须一致地使用导航属性 Company 和 CompanyId:您还需要在映射期间使用 CompanyId 的目标值:
编辑:但问题不在于您的公司为空,但重置后在数据库中仍然为空。这是因为如果您有像“CompanyId”这样的显式 Id 属性,则必须维护它。因此,调用
destinationModel.company = oldCompany;
是不够的,您还需要调用destinationModel.companyId = oldCompany.Id;
并且因为您从上下文中检索了目标实体它已经为您进行更改跟踪,因此无需设置 EntityState.Modified。
编辑:您修改后的示例:
Automapper always updates every property from the source instance to the destination instance by default. So if you don't want your Company property overwritten then you have to explicitly configure this for your mapper:
So far nothing EF related. But if you are using this with EF you have to use your navigation property Company and the CompanyId consistently: you also need to use the destination value for CompanyId during mapping:
EDIT: But the problem is not that your Company is null but after resetting it is still null in the DB. And this caused by the fact that if you are having an explicit Id property like 'CompanyId' you have to mantain it. So it is not enough to call
destinationModel.company = oldCompany;
you also need to calldestinationModel.companyId = oldCompany.Id;
And because you retrieved your dest entity from the context it's already doing the change tracking for you therefore there is no need set EntityState.Modified.
EDIT: Your modified sample:
在我看来,@nemesv 答案中的第二次编辑或 AutoMapper 的微调是可行的方法。你应该接受他的回答。我只添加一个解释为什么你的代码不起作用(但你的代码设置状态两次却可以)。首先,该问题与 AutoMapper 无关,当您手动设置属性时,您会得到相同的行为。
需要了解的重要一点是,设置状态 (
Entry(dest).State = EntityState.Modified
) 不仅会在上下文中设置一些内部标志,还会设置State
的属性设置器code> 实际上调用了一些复杂的方法,特别是它调用DbContext.ChangeTracker.DetectChanges()
(如果您不禁用AutoDetectChangesEnabled
)。那么,第一种情况会发生什么:
第二种情况会发生什么:
所以,这实际上是正常的更改跟踪行为,而不是 EF 中的错误。
我发现令人不安的一件事是您有一个 ViewModel显然它具有您在视图中未使用的属性。如果您的 ViewModel 没有
Company
和CompanyId
所有问题都会消失。 (或者至少配置 AutoMapper 不映射这些属性,如 @nemesv 所示。)The second EDIT in @nemesv's answer or the finetuning of AutoMapper is the way to go in my opinion. You should accept his answer. I only add an explanation why your code doesn't work (but your code with setting the state twice does). First of all, the problem has nothing to do with AutoMapper, you will get the same behaviour when you set the properties manually.
The important thing to know is that setting the state (
Entry(dest).State = EntityState.Modified
) does not only set some internal flag in the context but the property setter forState
calls actually some complex methods, especially it callsDbContext.ChangeTracker.DetectChanges()
(if you don't disableAutoDetectChangesEnabled
).So, what happens in the first case:
What happens in the second case:
So, this is actually normal change tracking behaviour and not a bug in EF.
One thing I find disturbing is that you have a ViewModel which apparently has properties you are not using in the view. If your ViewModel wouldn't have
Company
andCompanyId
all the trouble would disappear. (Or configure at least AutoMapper to not map these properties, as shown by @nemesv.)