【问题标题】:entity framework 4.1 code first and auto mapper issue实体框架 4.1 代码优先和自动映射器问题
【发布时间】:2011-12-10 00:33:26
【问题描述】:

考虑这个简单的模型和视图模型场景:

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 的错误吗?

这是解决该问题的示例控制台应用程序:

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();

        }
    }
}

【问题讨论】:

  • SomeModel 和/或SomeViewModel 中的Company 是否有外键属性,例如CompanyId?当您知道 ViewModel 中的 nullnull 时,为什么还要使用 Include 加载 Company?在我看来,您可以删除 Include
  • 外键属性是否可以为空并且也在SomeViewModel中?在Mapper.Map 之后会发生什么,它会覆盖destinationModel 中的FK 值吗?
  • @Slauma:是的,你说得非常正确。外键可以为空。是的,在 Mapper.Map 之后 fk 值为空,但我明确地为其分配了一个值,但它没有效果。它只“喜欢”null。
  • 您能在上面的代码中展示您对 FK 属性所做的确切操作吗?不知怎的,我觉得有问题。
  • @Slauma:我创建了一个简单的演示应用程序。请阅读我编辑的问题。您可以将源代码粘贴到控制台应用程序中并告诉我为什么会这样!

标签: c# entity-framework ef-code-first automapper ef4-code-only


【解决方案1】:

在我看来,@nemesv 的答案中的第二个编辑或 AutoMapper 的微调是要走的路。你应该接受他的回答。我只添加解释为什么您的代码不起作用(但是您的代码设置了两次状态)。首先,问题与 AutoMapper 无关,手动设置属性时会出现同样的行为。

要知道的重要一点是,设置状态(Entry(dest).State = EntityState.Modified)不仅在上下文中设置一些内部标志,而且State 的属性设置器实际上调用了一些复杂的方法,尤其是它调用了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

第二种情况会发生什么:

// ...
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

所以,这实际上是正常的更改跟踪行为,而不是 EF 中的错误。

我觉得令人不安的一件事是,您有一个 ViewModel,它显然具有您未在视图中使用的属性。如果您的 ViewModel 没有CompanyCompanyId,那么所有的麻烦都会消失。 (或者至少配置 AutoMapper 以不映射这些属性,如 @nemesv 所示。)

【讨论】:

  • 这并不令人不安。实际上,我有一个共享的 ViewModel 用于我的编辑、删除和添加操作。在添加操作中,我确实需要 companyId 和 company,但在编辑和删除中我不需要。在添加、编辑和删除之间创建共享视图模型是一种不好的做法吗?我不想为每个添加、编辑和删除操作创建类重载。所以我试图重用我的课程。这很糟糕吗?
  • 也许我应该将它创建为一个不同的问题,并带有指向该问题的链接。
  • @Jaggu ViewModel 通常是为他们服务的 View 量身定做的。对我来说,我发现用户需求、业务规则或其他细微差别(例如使用户友好的东西)决定了每个视图都会有所不同。因此,您可能对每个视图都有一个 ViewModel。这至少是我自己对 MVC 的体验。
【解决方案2】:

默认情况下,Automapper 总是将每个属性从源实例更新到目标实例。因此,如果您不希望您的 Company 属性被覆盖,那么您必须为您的映射器显式配置它:

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;

因为您从上下文中检索了目标实体,所以它已经在为您进行更改跟踪,因此无需设置 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();

【讨论】:

  • 您的解决方案很好,但不是我想要的,因为您希望我更改我的 CreateMaps。我的问题仍然存在:为什么即使我分配了公司,它也需要 null?
  • 很好的答案 nemesv。对此,我真的非常感激!在 Slauma 的解释和您的回答之后,现在事情对我来说更清楚了。
  • @nemesv 如果变成null 的属性是一个集合怎么办?在这里查看我的问题stackoverflow.com/q/41430679/613605。非常感谢任何帮助和建议。
猜你喜欢
  • 2012-03-12
  • 1970-01-01
  • 1970-01-01
  • 2011-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多