web-dev-qa-db-ja.com

オートマッパーがマッププロパティではなく新しいインスタンスを作成する

これは長いです。

したがって、AJAXリクエストから更新しているモデルとビューモデルがあります。WebAPIコントローラーがビューモデルを受信し、次に、以下のようにAutoMapperを使用して既存のモデルを更新します。

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;
}

User-> UserViewModel(およびその逆)マッピングに対して、オートマッパーをそのように構成しました。

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

(反対のマップを明示的に設定し、ReverseMapを省略すると、同じ動作を示すことに注意してください)

別のオブジェクトのICollectionであるModel/ViewModelのメンバーに問題があります。

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

対応するモデルはそのようなものです:

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

問題:

上記のUserPreferences/UserPreferenceViewModelsのICollectionsを除き、UserクラスとUserViewModelクラスのすべてのプロパティは正しくマップされます。これらのコレクションがプロパティをマップするのではなく、ViewModelからモデルにマップすると、既存のオブジェクトをViewModelプロパティで更新するのではなく、UserPreferenceオブジェクトの新しいインスタンスがViewModelから作成されます。

モデル:

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; }
}

そして対応するViewModel

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

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

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

そしてオートマッパーの設定:

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

UserViewModelエンティティをユーザーにマッピングすると、UserPreferenceViewModelsのICollectionもユーザーのUserPreferencesのICollectionにマッピングされます。

ただし、これが発生すると、「DateCreated」、「CreatedBy_Id」、「User_Id」などの個々のUserPreferenceオブジェクトのプロパティは、コピーされる個々のプロパティではなく新しいオブジェクトが作成されたかのようにnullになります。

これは、コレクションに1つのUserPreferenceオブジェクトしかないUserViewModelをマッピングするとき、DbContextを検査するとき、mapステートメントの後に2つのローカルUserPreferenceオブジェクトがあるように、証拠としてさらに示されます。 1つはViewModelから作成された新しいオブジェクトのように見え、もう1つは既存のモデルの元のオブジェクトです。

AutoModelで、ViewModelのコレクションから新しいメンバーをインスタンス化するのではなく、既存のModelのコレクションのメンバーを更新するにはどうすればよいですか?ここで何が間違っているのですか?

Mapper.Map()の前後を示すスクリーンショット

Before

After

14
Ron Brogan

私が知る限り、これはAutoMapperの制限です。ライブラリはビューモデルやエンティティへのマッピングに一般的に使用されていますが、クラスを他のクラスにマッピングするための一般的なライブラリであり、そのため、 Entity FrameworkのようなORM。

だから、これが起こっていることの説明です。 AutoMapperを使用してコレクションを別のコレクションにマップすると、文字通りcollectionがマッピングされます。そのコレクション内のアイテムの値は、同様のコレクション内のアイテムにマッピングされません。振り返ってみると、これは理にかなっています。AutoMapperには、コレクション内の個々の項目を別の項目に整列する方法を確認する信頼できる独立した方法がないためです。 IDはどのプロパティですか?名前が一致する必要がありますか?

つまり、エンティティの元のコレクションが、まったく新しいアイテムインスタンスで構成されるまったく新しいコレクションに完全に置き換えられているのです。多くの場合、これは問題になりませんが、Entity Frameworkの変更追跡と組み合わせると、元のコレクション全体を削除して、新しい一連のエンティティに置き換える必要があることが通知されます。明らかに、それはあなたが望むものではありません。

これを解決するにはどうすればよいですか?まあ、残念ながら、それは少し苦痛です。最初のステップは、マッピング時にコレクションを完全に無視するようにAutoMapperに指示することです。

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

これを2つのマップに分割したことに注意してください。 ビューモデルにをマッピングするときに、コレクションを無視する必要はありません。 EFはそれを追跡しないので、それは問題を引き起こしません。エンティティクラスにマッピングし直すときにのみ問題になります。

しかし、今はそのコレクションをまったくマッピングしていないので、どうやって値をアイテムに戻すのですか?残念ながら、これは手動のプロセスです。

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);
    }
}
19
Chris Pratt

その間、その特定の問題に対するAutoMapper拡張機能が存在します。

cfg.AddCollectionMappers();
cfg.CreateMap<S, D>().EqualityComparison((s, d) => s.ID == d.ID);

AutoMapper.EF6/EFCoreを使用すると、すべての等価比較を自動生成することもできます。参照してください AutoMapper.CollectionAutoMapper.EF6 または AutoMapper.Collection.EFCore

7
r2d2

すべてのICollectionを処理する AutoMapperソースファイル および ICollection Mapper によれば、

コレクションはClear()の呼び出しによってクリアされてから、再度追加されます。私が見る限り、今回はAutoMapperが自動的にマッピングを行う方法はありません。

コレクションをループし、同じものをAutoMapper.Mapにループするロジックを実装します

2
HaroldHues