2015-08-20 6 views
7

Questo è lungo.Automapper che crea una nuova istanza anziché le proprietà della mappa

Quindi, ho un modello e un viewmodel che sto aggiornando da una richiesta AJAX. controller Web API riceve il ViewModel, che ho poi aggiornare il modello esistente utilizzando automapper come di seguito:

private User updateUser(UserViewModel entityVm) 
{ 
    User existingEntity = db.Users.Find(entityVm.Id); 
    db.Entry(existingEntity).Collection(x => x.UserPreferences).Load(); 

    Mapper.Map<UserViewModel, User>(entityVm, existingEntity); 
    db.Entry(existingEntity).State = EntityState.Modified; 

    try 
    { 
     db.SaveChanges(); 
    } 
    catch 
    { 
     throw new DbUpdateException(); 
    } 

    return existingEntity; 
} 

ho automapper configurato in questo modo per l'utente -> UserViewModel (e ritorno) mappatura.

Mapper.CreateMap<User, UserViewModel>().ReverseMap(); 

(Si noti che l'impostazione in modo esplicito la mappa opposto e omettendo il ReverseMap presenta lo stesso comportamento)

Sto avendo un problema con un membro del Modello/ViewModel che è un ICollection di un oggetto diverso :

[DataContract] 
public class UserViewModel 
{ 
    ... 
    [DataMember] 
    public virtual ICollection<UserPreferenceViewModel> UserPreferences { get; set; } 
} 

Il modello corrispondente è come tale:

public class User 
{ 
    ... 
    public virtual ICollection<UserPreference> UserPreferences { get; set; } 
} 

Il problema:

Ogni proprietà dell'Utente e classi UserViewModel mappe correttamente, fatta eccezione per la ICollections di PreferenzeUtente/UserPreferenceViewModels mostrato sopra. Quando queste raccolte vengono mappate da ViewModel al modello, anziché dalle proprietà della mappa, viene creata una nuova istanza di un oggetto UserPreference dal ViewModel, anziché aggiornare l'oggetto esistente con le proprietà ViewModel.

Modello:

public class UserPreference 
{ 
    [Key] 
    public int Id { get; set; } 

    public DateTime DateCreated { get; set; } 

    [ForeignKey("CreatedBy")] 
    public int? CreatedBy_Id { get; set; } 

    public User CreatedBy { get; set; } 

    [ForeignKey("User")] 
    public int User_Id { get; set; } 

    public User User { get; set; } 

    [MaxLength(50)] 
    public string Key { get; set; } 

    public string Value { get; set; } 
} 

e il corrispondente ViewModel

public class UserPreferenceViewModel 
{ 
    [DataMember] 
    public int Id { get; set; } 

    [DataMember] 
    [MaxLength(50)] 
    public string Key { get; set; } 

    [DataMember] 
    public string Value { get; set; } 
} 

e la configurazione automapper:

Mapper.CreateMap<UserPreference, UserPreferenceViewModel>().ReverseMap(); 

//also tried explicitly stating map with ignore attributes like so(to no avail): 

Mapper.CreateMap<UserPreferenceViewModel, UserPreference>().ForMember(dest => dest.DateCreated, opts => opts.Ignore()); 

Quando la mappatura un'entità UserViewModel a un utente, ICollection di UserPreferenceViewModels è anche mappato l'ICollection dell'utente di UserPreferences, come dovrebbe.

Tuttavia, quando ciò si verifica, le singole proprietà dell'oggetto UserPreference come "DateCreated", "CreatedBy_Id" e "User_Id" vengono annullate come se venisse creato un nuovo oggetto anziché le singole proprietà copiate.

Questo è ulteriormente dimostrato come quando si associa un UserViewModel che ha solo 1 oggetto UserPreference nella raccolta, quando si ispeziona DbContext, ci sono due oggetti UserPreference locali dopo l'istruzione della mappa. Uno che sembra essere un nuovo oggetto creato da ViewModel e uno che è l'originale dal modello esistente.

Come faccio a rendere automapper l'aggiornamento di una collezione di un modello esistente; i membri di s, invece di istanziare nuovi membri dalla raccolta di ViewModel? Cosa sto facendo di sbagliato qui?

Screenshots per dimostrare prima/dopo Mapper.Mappa()

Before

After

+0

Potrebbe essere necessario eseguire l'override del 'GetHashCode' in' 'UserPreference' e UserPreferenceViewModel' in modo che la raccolta li può identificare come la stessa. Forse l'automapper ha un po 'di magia, ma io sono dubbioso – HaroldHues

+0

Yup, deve aver copiato/incollato la linea sbagliata per la linea CreateMap. Sto esaminando l'override del GetHashCode attualmente, ti riporto se viene trovato qualcosa. Grazie per l'aiuto. –

+0

È necessario fare un passo indietro e interrompere l'uso di AutoMapper per mappare da ViewModel ai modelli di entità o di dominio. AutoMapper non è stato creato per questo e non funzionerà come previsto. Usa AutoMapper ** SOLO ** per mappare da Entity/Domain Model a ViewModel/DTO/BindingModel, ** NEVER ** other way around. Ciò è fortemente scoraggiato dall'autore di AutoMapper – Tseng

risposta

9

Questa è una limitazione di automapper per quanto ne sono a conoscenza. È utile tenere a mente che mentre la libreria viene comunemente utilizzata per mappare da/per visualizzare i modelli e le entità, è una libreria generica per mappare qualsiasi classe a qualsiasi altra classe e, come tale, non tiene conto di tutte le eccentricità di un ORM come Entity Framework.

Quindi, ecco la spiegazione di cosa sta succedendo. Quando si associa una raccolta a un'altra raccolta con AutoMapper, si sta letteralmente mappando la raccolta , non i valori dagli elementi di quella raccolta agli elementi di una raccolta simile. In retrospettiva, questo ha senso perché AutoMapper non ha un modo affidabile e indipendente per accertare come dovrebbe allineare un singolo elemento di una raccolta a un altro: per id? quale proprietà è l'id? forse i nomi dovrebbero coincidere?

Quindi, quello che sta succedendo è che la collezione originale della tua entità è completamente sostituita da una nuova collezione composta da nuove istanze di oggetti nuovi. In molte situazioni, questo non sarebbe un problema, ma quando lo combini con il rilevamento delle modifiche in Entity Framework, hai ora segnalato che l'intera collezione originale deve essere rimossa e sostituita con un nuovo set di entità. Ovviamente, non è quello che vuoi.

Quindi, come risolvere questo? Beh, sfortunatamente, è un po 'un dolore. Il primo passo è quello di dire automapper di ignorare completamente la collezione quando mappatura:

Mapper.CreateMap<User, UserViewModel>(); 
Mapper.CreateMap<UserViewModel, User>() 
    .ForMember(dest => dest.UserPreferences, opts => opts.Ignore()); 

Si noti che ho rotto questo in due mappe. Non è necessario ignorare la raccolta quando si esegue il mapping di nel modello di vista. Ciò non causerà alcun problema perché EF non lo sta monitorando. Importa solo quando stai mappando di nuovo alla tua classe di entità.

Ma, ora non stai mappando affatto quella raccolta, quindi come si riportano i valori sugli articoli? Purtroppo, si tratta di un processo manuale:

foreach (var pref in model.UserPreferences) 
{ 
    var existingPref = user.UserPreferences.SingleOrDefault(m => m.Id == pref.Id); 
    if (existingPref == null) // new item 
    { 
     user.UserPreferences.Add(Mapper.Map<UserPreference>(pref)); 
    } 
    else // existing item 
    { 
     Mapper.Map(pref, existingPref); 
    } 
} 
1

Secondo il AutoMapper source file che gestisce tutti i ICollection (tra le altre cose) e il ICollection Mapper:

La collezione viene cancellato da una chiamata al Clear() quindi aggiunto di nuovo , quindi per quanto posso vedere non c'è modo che AutoMapper sarà in grado di eseguire automaticamente la mappatura questa volta.

vorrei implementare una logica di ciclo su collezioni e AutoMapper.Map quelli che sono lo stesso

Problemi correlati