Entity Framework 4.1 Code First 和 AutoMapper 的问题

3
考虑这个简单的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分配给了我的company,在保存更改后数据库中仍然为空。
注意:
如果我更改这些行:
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();

        }
    }
}

你的 SomeModel 和/或 SomeViewModel 中是否有 Company 的外键属性,例如 CompanyId?为什么在你的 ViewModel 中已知 Companynull 的情况下还要使用 Include 加载它呢?我认为你可以删除 Include - Slauma
外键属性可为空并且在 SomeViewModel 中吗?然后在 Mapper.Map 之后会发生什么,它会覆盖 destinationModel 中的 FK 值吗? - Slauma
@Slauma:是的,你基本上是正确的。外键是可空的。是的,在 Mapper.Map 之后,fk 值为 null,但是我明确地给它赋了一个值,但是没有效果。它仍然是 null。 - Jaggu
你能展示一下你在上面的代码中如何使用FK属性吗?我有点感觉问题就出在这里。 - Slauma
@Slauma:我创建了一个简单的演示应用程序。请阅读我的编辑问题。您可以将源代码粘贴到控制台应用程序中,并告诉我为什么会发生这种情况! - Jaggu
2个回答

3

默认情况下,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());
编辑:但问题不在于您的公司为null,而是重置后仍然在数据库中为null。这是因为如果您有一个显式的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? - Jaggu
很棒的回答,nemesv。我非常感激!在Slauma的解释和你的回答之后,现在事情变得更加清晰了。 - Jaggu
@nemesv 如果变为“null”的属性是一个集合,怎么办?请参见我的问题:https://dev59.com/L1gR5IYBdhLWcg3wV8Pe。非常感谢任何帮助和建议。 - J86

2
在我看来,@nemesv答案中的第二个EDIT或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,但在编辑和删除操作中则不需要。在添加、编辑和删除操作之间创建共享的ViewModel是一种不好的做法吗?我不想为每个添加、编辑和删除操作创建类重载,所以我尝试重用我的类。这样做有问题吗? - Jaggu
也许我应该创建一个不同的问题,并附上这个问题的链接。 - Jaggu
1
@Jaggu ViewModel通常是为它们服务的视图量身定制的。对我来说,我发现用户需求、业务规则或其他细微差别,如使某些东西更加用户友好,决定了每个视图略有不同。因此,您可能会为每个视图创建一个ViewModel。这至少是我在MVC中的经验。 - AaronLS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接