2014年9月29日 星期一

[iThome 第七屆鐵人賽 07] AutoMapper 介紹 - 簡單化Entity和ViewModel之間的轉換

在上一篇介紹完ViewModel的好處之後,留下的問題是,ViewModel雖然有帶來好處,但是ViewModel和實際Entity之間的對應其實是很麻煩的一件事情,那麼我們如何能夠簡化對應的邏輯呢?

這時候就是AutoMapper這個套件入場的時候。

AutoMapper介紹

AutoMapperimage

AutoMapper的目的就是要解決無聊的左邊資料倒到右邊。我們舉一個例子,如果是在早期的Asp .Net Webform,當一個Form進來的時候,我們常常會需要:

// psuedo 程式碼

string name = Request.Form["name"];
string age = Request.Form["age"];
.....
這些其實很無聊但是又不得不做。在Mvc裡面Model Binding解決了這個問題。

但是如果用ViewModel,還是有這個問題,因此就有人開發了AutoMapper。


AutoMapper簡單來說,使用步奏就是:



  1. 定義好兩個Class之間轉換的邏輯
  2. 把object透過AutoMapper轉換成為另外一個形態的object

測試情景


在介紹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頁面的ViewModel


沒有AutoMapper的做法


假設我說,我們的Index頁面不要顯示CreateDateTimeLastModifyDateTime。我們當然可以只改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


如果用上AutoMapper,程式碼變成:

// 定義Post是來源的Class而IndexViewModel是最後結果
Mapper.CreateMap<Post, IndexViewModel>();

// 把List<Post>轉成List<IndexViewModel>
var viewModel2 = Mapper.Map<List<IndexViewModel>>(db.Post.ToList());

用了AutoMapper總共有2個好處:



  1. 程式碼變少了:本來要11行,現在只要2行
  2. 程式碼的可讀性提高:本來還會跑迴圈,如果property參數多了,看起來不是那麼直覺。但是用AutoMapper,非常直覺知道是在轉換Model形態

可能你會在想,為什麼我們什麼都沒有設定,AutoMapper就知道對應欄位是什麼?其實因為AutoMapper會自動把一樣的Property名字作為一對,以我們的例子,Property都一樣,因此我不需要做額外設定。


需求變更


假設,今天我們Index頁面的需求變了,變成在Index頁面的每一筆Post需要顯示最後一次修改時間距離建立時間過了幾天,如果沒有最後一次修改時間,就用今天日期,這個時候,我們就需要手動設定Property的值要如何產生。


首先我們在IndexViewModel增加一個Property:

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的值要從:如果沒有最後修改時間,就用今天日期減掉建立日期。不然就用 最後修改時間減掉建立時間


Queryable Extension - 減少Sql Select的欄位


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

...
image
用了AutoMapper產生的Sql(左邊)和沒用的產生Sql(右邊)

有這樣的效果是因為EF用了LazyLoading。


Edit頁面的ViewModel


這邊我快速介紹一下修改頁面的ViewModel。


以我們的例子,只允許修改TitlePostContent而已,同時,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,這些設定就變的不好維護


因此在下一篇,我們會建立一些功能,讓我們的在開發上面使用起來更方便和好維護。


沒有留言 :

張貼留言