在上一篇介紹完ViewModel的好處之後,留下的問題是,ViewModel雖然有帶來好處,但是ViewModel和實際Entity之間的對應其實是很麻煩的一件事情,那麼我們如何能夠簡化對應的邏輯呢?
這時候就是AutoMapper這個套件入場的時候。
同步發表於我的部落格:http://alantsai2007.blogspot.tw/2014/09/ithome-07-automapper-entityviewmodel.html
AutoMapper的目的就是要解決無聊的左邊資料倒到右邊。我們舉一個例子,如果是在早期的Asp .Net Webform,當一個Form進來的時候,我們常常會需要:
// psuedo 程式碼
string name = Request.Form["name"];
string age = Request.Form["age"];
.....
這些其實很無聊但是又不得不做。在Mvc裡面Model Binding解決了這個問題。
但是如果用ViewModel,還是有這個問題,因此就有人開發了AutoMapper。
AutoMapper簡單來說,使用步奏就是:
在介紹AutoMapper之前,我們先設定好我們的測試情景。假設我們有一個DB,裡面一個Table叫做Post,代表著一個部落格網站裡面所擁有的文章。Entity可能如下:
public partial class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string PostContent { get; set; }
public System.DateTime CreateDateTime { get; set; }
public Nullable<System.DateTime> LastModifyDateTime { get; set; }
}
那用這個Entity,透過Mvc的Scaffolding,我們建立出基本的CRUD頁面。那Scaffolding出來的CRUD,Entity會是預設的ViewModel。透過上一篇我們知道使用Entity做ViewModel的壞處是什麼,因此我們會開始針對CRUD建立對應的ViewModel。
假設我說,我們的Index頁面不要顯示CreateDateTime和LastModifyDateTime。我們當然可以只改View的HtmlHelper,不要顯示這兩個Property,不過等一下我們就知道為什麼用ViewModel更好。
因此,我建立了一個如下的ViewModel:
public class IndexViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public string PostContent { get; set; }
}
在沒有時候AutoMapper的情況下,我們會需要做:
List<IndexViewModel> viewModel = new List<IndexViewModel>();
foreach (var item in db.Post.ToList())
{
viewModel.Add(new IndexViewModel()
{
Id = item.Id,
PostContent = item.PostContent,
Title = item.Title
});
}
想想,我們才3個property而已就佔了這麼多行數的程式碼,如果有20個property不就很恐怖。而且,明明兩邊的Property都一樣,他不能夠自己對應嗎?
如果用上AutoMapper,程式碼變成:
// 定義Post是來源的Class而IndexViewModel是最後結果
Mapper.CreateMap<Post, IndexViewModel>();
// 把List<Post>轉成List<IndexViewModel>
var viewModel2 = Mapper.Map<List<IndexViewModel>>(db.Post.ToList());
用了AutoMapper總共有2個好處:
[*]程式碼變少了:本來要11行,現在只要2行
[*]程式碼的可讀性提高:本來還會跑迴圈,如果property參數多了,看起來不是那麼直覺。但是用AutoMapper,非常直覺知道是在轉換Model形態
可能你會在想,為什麼我們什麼都沒有設定,AutoMapper就知道對應欄位是什麼?其實因為AutoMapper會自動把一樣的Property名字作為一對,以我們的例子,Property都一樣,因此我不需要做額外設定。
假設,今天我們Index頁面的需求變了,變成在Index頁面的每一筆Post需要顯示最後一次修改時間距離建立時間過了幾天,如果沒有最後一次修改時間,就用今天日期,這個時候,我們就需要手動設定Property的值要如何產生。
首先我們在IndexViewModel增加一個Property:
1
public double HowManyDayPass { get; set; }
然後AutoMapper的轉換邏輯改一下:
...
Mapper.CreateMap<Post, IndexViewModel>()
.ForMember(member => member.HowManyDayPass,
opt => opt.MapFrom(x => x.LastModifyDateTime == null ?
(DateTime.Now - x.CreateDateTime).TotalDays :
(x.LastModifyDateTime.Value - x.CreateDateTime).TotalDays));
....
應該很好懂,設定member.HowManyDayPass這個property的值要從:如果沒有最後修改時間,就用今天日期減掉建立日期。不然就用 最後修改時間減掉建立時間。
AutoMapper其實有提供一個IQueryable的Extension方法,讓我們EF在對DB下Sql的時候,只下我們需要的部分。我們直接看例子:
//需要先加Using
using AutoMapper.QueryableExtensions;
...
var projectIQueryable = (db.Post.Project().To<IndexViewModel>()).ToList(); // AutoMapper QuerableExtension
var normalIQuerable = db.Post.AsQueryable().ToList();
...
用了AutoMapper產生的Sql(左邊)和沒用的產生Sql(右邊)
有這樣的效果是因為EF用了LazyLoading。
這邊我快速介紹一下修改頁面的ViewModel。
以我們的例子,只允許修改Title和PostContent而已,同時,LastModifyDateTime應該要自動使用系統時間。
這個時候我們的EditViewModel就會是:
public class EditViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public string PostContent { get; set; }
}
AutoMapper的設定:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditViewModel post)
{
if (ModelState.IsValid)
{
// 建立Mapping邏輯,並且LastModifyDateTime使用系統時間
Mapper.CreateMap<EditViewModel, Post>().
ForMember(member => member.LastModifyDateTime,
opt => opt.UseValue(DateTime.Now));
Post postEntity = db.Post.Find(post.Id);
// 只更新ViewModel的部分到Entity
Mapper.Map(post, postEntity);
db.Entry(postEntity).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
到目前為止我們介紹了AutoMapper的基本概念,和基本的使用。相信在整體使用上面來說,比自己手動轉換來的方便。但是,假設以我們目前介紹的方式去做,AutoMapper還是有點不好用。
其實最主要的問題是在:設定對應邏輯的地方。雖然說AutoMapper簡化了很多設定,可是還是要設定啊,這些設定到底要放在那裡?想像一下,假設我們每一個View有個 ViewModel,一個功能至少有CRUD,很快我們就有一堆ViewModel,這些設定就變的不好維護。雖然說AutoMapper簡化了很多設定,可是還是要設定啊,這些設定到底要放在那裡?想像一下,假設我們每一個View有個 ViewModel,一個功能至少有CRUD (4個ViewModel),很快我們就有一堆ViewModel,這些設定就變的不好維護
因此在下一篇,我們會建立一些功能,讓我們的在開發上面使用起來更方便和好維護。