考虑这个简单的Model和ViewModel场景:
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.
然后我做了
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分配给我的公司,它在savechanges之后仍然在数据库中为空。
注意:
如果我更改这些行:
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();
注意我改变状态2次,它工作正常。可能是什么问题?这是一个ef 4.1 bug吗?
这是解决此问题的示例控制台应用程序:
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();
}
}
}
答案 0 :(得分:3)
默认情况下,Automapper始终会将源实例中的每个属性更新到目标实例。因此,如果您不希望覆盖公司属性,则必须为映射器明确配置:
Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.Company, c => c.UseDestinationValue());
到目前为止,没有EF相关。但是如果您在EF中使用它,则必须始终如一地使用导航属性Company和CompanyId:您还需要在映射期间使用CompanyId的目标值:
Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.CompanyId, c => c.UseDestinationValue());
编辑:但问题不在于您的公司为空,但在重置后,它在数据库中仍然为空。这是因为如果你有一个明确的Id属性,比如'CompanyId',你必须保留它。因此仅拨打destinationModel.company = oldCompany;
您还需要致电destinationModel.companyId = oldCompany.Id;
因为您从上下文中检索了您的dest实体,所以它已经为您进行了更改跟踪,因此不需要设置EntityState.Modified。
编辑:您修改过的示例:
Mapper.CreateMap<MyModel, MyViewModel>();
Mapper.CreateMap<MyViewModel, MyModel>();
//fetch the company
MyModel dest = slaumaContext.MyModels.Include("Company").Where(c => c.Id == 18).First(); //hardcoded for demo
var oldCompany = dest.Company;
//creating a viewmodel
MyViewModel source = new MyViewModel();
source.Company = null;
source.CompanyId = null;
source.Foo = "fdsfdf";
Mapper.Map(source, dest); // company null in dest
dest.Company = oldCompany;
dest.CompanyId = oldCompany.Id;
slaumaContext.SaveChanges();
答案 1 :(得分:2)
在@ nemesv的回答中第二次编辑或者AutoMapper的微调是我认为的方式。你应该接受他的回答。我只添加一个解释为什么你的代码不起作用(但你的代码设置状态两次)。首先,问题与AutoMapper无关,手动设置属性时会出现相同的行为。
重要的是要知道设置状态(Entry(dest).State = EntityState.Modified
)不仅在上下文中设置了一些内部标志,而且State
的属性设置器实际上调用了一些复杂的方法,特别是它调用{ {1}}(如果您不禁用DbContext.ChangeTracker.DetectChanges()
)。
那么,第一种情况会发生什么:
AutoDetectChangesEnabled
第二种情况会发生什么:
// ...
Mapper.Map(source, dest);
dest.Company = oldCompany;
// at this point the state of dest EF knows about is still the state
// when you loaded the entity from the context because you are not working
// with change tracking proxies, so the values are at this point:
// dest.CompanyId = null <- this changed compared to original value
// dest.Company = company <- this did NOT change compared to original value
// The next line will call DetectChanges() internally: EF will compare the
// current property values of dest with the snapshot of the values it had
// when you loaded the entity
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
// So what did EF detect:
// dest.Company didn't change, but dest.CompanyId did!
// So, it assumes that you have set the FK property to null and want
// to null out the relationship. As a consequence, EF also sets dest.Company
// to null at this point and later saves null to the DB
所以,这实际上是正常的更改跟踪行为,而不是EF中的错误。
我觉得令人不安的一件事是你有一个ViewModel,它显然具有你在视图中没有使用的属性。如果您的ViewModel没有// ...
Mapper.Map(source, dest);
// Again in the next line DetectChanges() is called, but now
// dest.Company is null. So EF will detect a change of the navigation property
// compared to the original state
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
dest.Company = oldCompany;
// Now DetectChanges() will find that dest.Company has changed again
// compared to the last call of DetectChanges. As a consequence it will
// set dest.CompanyId to the correct value of dest.Company
slaumaContext.Entry(dest).State = System.Data.EntityState.Modified;
// dest.Company and dest.CompanyId will have the old values now
// and SaveChanges() doesn't null out the relationship
和Company
,那么所有麻烦都会消失。 (或者至少配置AutoMapper不映射这些属性,如@nemesv所示。)