2011-10-18 85 views
3

考慮這個簡單的模型和視圖模型場景:實體框架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. 

然後我做了

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

     } 
    } 
} 
+0

在SomeModel和/或SomeViewModel中有'Company'的外鍵屬性,像'CompanyId' ?當你知道它在ViewModel中爲null時,爲什麼你用'Include'加載'Company'?在我看來,你可以刪除'Include'。 – Slauma

+0

外鍵屬性是否可以使用,也可以在'SomeViewModel'中使用? 'Mapper.Map'後會發生什麼,它是否覆蓋'destinationModel'中的FK值? – Slauma

+0

@Slauma:是的,你非常正確。外鍵是可空的。在Mapper.Map之後是fk值爲null,但是我明確地給它賦值但沒有效果。它「只喜歡」null。 – Jaggu

回答

3

Automapper默認總是從源實例每個屬性更新到目標實例。所以,如果你不想那麼你的公司財產被覆蓋,你必須爲你的映射器明確配置此:

Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.Company, c => c.UseDestinationValue()); 

到目前爲止沒有EF相關。但是,如果你正在使用此與EF你必須使用你的導航物業公司和CompanyId一致:你還需要映射過程中使用CompanyId目標值:

Mapper.CreateMap<MyViewModel, MyModel>().ForMember(m => m.CompanyId, c => c.UseDestinationValue()); 

編輯:但問題是不您的公司爲空,但重置後它仍然在數據庫中爲空。這是由於如果你有一個像'CompanyId'這樣的明確的Id屬性,你必須維護它。因此,它是不夠的,叫你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(); 
+0

您的解決方案很好,但不完全是我想要的,因爲您希望我更改我的CreateMaps。我的問題依然存在:爲什麼即使我指定公司,它也會失效? – Jaggu

+0

@Jaggu我已經更新了我的答案。 – nemesv

+0

很好的答案nemesv。對此,我真的非常感激!現在,在Slauma的解釋和答案之後,事情變得更加清晰。 – Jaggu

2

在@ nemesv的回答第二個編輯或AutoMapper的是細化和微調在我看來要走的路。你應該接受他的回答。我只是添加一個解釋爲什麼你的代碼不工作(但你的代碼設置兩次的狀態)。首先,該問題與AutoMapper無關,您將在手動設置屬性時得到相同的行爲。

瞭解的重要一點是,設置狀態(Entry(dest).State = EntityState.Modified)不僅設置一些內部標誌的背景下,但State電話竟有些複雜的方法,屬性setter,尤其是它調用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所示)

+0

這不是令人不安。其實我有一個共享的ViewModel用於編輯,刪除和添加操作。在添加操作中,我需要companyId和company,但是在編輯和刪除中我不需要。在添加,編輯和刪除之間創建共享視圖模型是不好的做法嗎?我不想爲每個添加,編輯和刪除操作創建類重載。所以我試着重用我的課程。這不好嗎? – Jaggu

+0

也許我應該創建它作爲與這個問題的鏈接不同的問題。 – Jaggu

+1

@Jaggu ViewModel通常爲他們所服務的View量身打造。對我而言,我發現用戶需求,業務規則或其他細微差別(如使用戶友好)決定了每個視圖都有所不同。因此,您可能爲每個視圖都有一個ViewModel。這至少是我自己的MVC經驗。 – AaronLS