昨天我們實際使用 Postman 測試了先前實作的地址 Web Api 的 CRUD 功能,但是在列表以及查詢時得到如下格式的資料:
{
"id": 3,
"areaId": 4,
"area": {
"id": 4,
"name": "三民區",
"cityId": 6,
"city": {
"id": 6,
"name": "高雄市",
"areas": []
}
},
"line": "漢口街302號"
}
這些資料若是以將這些資料傳遞到使用者介面的觀點來看,似乎有許多不必要的資料被讀出。
ViewModel
如同文章開頭所述,以及回憶一下先前的《領域物件初體驗》所講的,為了解決某商業運作問題而設計出領域物的,有時並不適合原原本地就直接呈現在使用者介面上,以目前這個地址物件為例,雖然在決定是否為外送區域,或是因為估價基準,必需切開成 City、Area、Line 等組合,但是使用者最直接想看到的應該是類似:《高雄市三民區漢口街302號》 這樣直述的地址資訊才對。況且有時在設計領域物件時會為各個物件加入為了往後系統運作時用來追蹤用的物件何時建立、何人建立、何時修改、何人修改等屬性,但是這些追蹤屬對一般使用者來講並不會感興趣,也不應該讓他們看見才對。
因此,有針對給特定某個 View 使用的 Model 的設計需要,這種給 View 使用的 Model 稱為 ViewModel。
除了 ViewModel 之外,還有一種稱為 DTO (Data Transfer Object)這兩種物件很類似,因而造成許多人分不太清楚這兩者之間的差別,往後有機會會再加以解說。
接著,請在 Demae.Core 專案加入 Models 資料夾,並在資料夾中加入命名為 AddressModel.cs 的類別:
接著加入如下的程式碼:
public class AddressModel
{
public int Id { get; set; }
public string City { get; set; }
public string Area { get; set; }
public string Address { get; set; }
}
該 ViewModel 類別的屬性只要單單包含足夠想要傳達給使用者的資訊就可以了。
Entity To ViewModel
設計完 ViewModel 之後,接下來就是將讀取到的 Entity 資料投射(Projection)到 ViewModel 因此可修改因來的列表動作方法如下:
[HttpGet]
public IEnumerable<AddressModel> GetAddresses()
{
return _context.Addresses.Select(e => new AddressModel
{
Id = e.Id,
City = e.Area.City.Name,
Area = e.Area.Name,
Address = e.Area.City.Name + e.Area.Name + e.Line
});
}
上述程式碼使用 Select()
將需要傳遞使用者的資料挑選出來。
改完程式之後,試著以 Postman 執行,成果如下:
同樣地,查詢的程式也可修改如下:
[HttpGet("{id}")]
public async Task<IActionResult> GetAddress([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var address = await _context.Addresses.Select(e => new AddressModel
{
Id = e.Id,
City = e.Area.City.Name,
Area = e.Area.Name,
Address = e.Area.City.Name + e.Area.Name + e.Line
}).SingleOrDefaultAsync(m => m.Id == id);
if (address == null)
{
return NotFound();
}
return Ok(address);
}
改完程式之後,試著以 Postman 執行,成果如下:
好吧!今天就學習到這裡,在離開之前先丟一個思考題給各位讀者。目前這個 ViewModel 只有 4 個屬性,還可以一個一個的投射,但是如果屬性一多怎麼辦?而列表和查詢好像相同的投射卻寫了兩次(雖然可以複製貼上)是不是好像很沒效率?特別是屬性一多又特別會感覺得出來。這問題就留待明天再來解答。