緣起
哈嘍大家周四好笙隙,時(shí)間是過(guò)的真快洪灯,這幾天一直忙著在公司的項(xiàng)目,然后帶帶新人竟痰,眼看這周要過(guò)去了签钩,還是要抽出時(shí)間學(xué)習(xí)學(xué)習(xí),這些天看到群里的小伙伴也都在忙著新學(xué)習(xí)坏快,還是很開(kāi)心的铅檩,至少當(dāng)時(shí)的初衷已經(jīng)達(dá)到了,一起學(xué)習(xí)一起進(jìn)步嘛莽鸿,哪怕是對(duì)現(xiàn)在或者是對(duì)以后的工作有一丟丟的幫助柠并,也是不枉此時(shí)的努力,哈哈夜里寫(xiě)文章總是容易多想,好啦臼予,廢話(huà)不多說(shuō)鸣戴,上次咱們說(shuō)到了《從壹開(kāi)始微服務(wù) [ DDD ] 之七 ║項(xiàng)目第一次實(shí)現(xiàn) & CQRS初探》,今天本來(lái)應(yīng)該接著寫(xiě) **領(lǐng)域命令 了粘拾,在設(shè)計(jì)的領(lǐng)域命令的時(shí)候窄锅,發(fā)現(xiàn)了值對(duì)象的存在,對(duì) 領(lǐng)域模型 **和 **視圖模型 **有著剪不斷理還亂的困擾缰雇,所以我就暫時(shí)單寫(xiě)一篇了入偷,既是對(duì)上一篇的補(bǔ)充,又是對(duì)領(lǐng)域命令的鋪墊械哟,好啦疏之,馬上開(kāi)始今天的說(shuō)明吧~~
還是老規(guī)矩,每篇文章先給大家一個(gè)小問(wèn)題暇咆,先思考下锋爪,然后有助于理解本文:
問(wèn)題:我們?cè)陬I(lǐng)域模型 Student 中,有一個(gè)戶(hù)籍的值對(duì)象(為啥叫戶(hù)籍爸业,下邊會(huì)說(shuō)到)其骄,然后我們也有一個(gè)學(xué)生的視圖模型 StudentViewModel ,那么問(wèn)題來(lái)了扯旷,我們?cè)?StudentViewModel 中拯爽,如何去定義這個(gè)戶(hù)籍的視圖模型呢,然后又是如何傳給領(lǐng)域模型 Student 呢钧忽?
1毯炮、不寫(xiě)這戶(hù)籍一塊,直接在業(yè)務(wù)邏輯里耸黑,手動(dòng)賦值給 Student 領(lǐng)域模型
public class StudentViewModel
{ [Required(ErrorMessage = "The Name is Required")]
[MinLength(2)]
[MaxLength(100)]
[DisplayName("Name")] public string Name { get; set; } **//... 等等其他桃煎,只是學(xué)生的個(gè)人信息,不涉及戶(hù)籍地址**
}
2崎坊、和領(lǐng)域模型一樣备禀,也寫(xiě)一個(gè)對(duì)象洲拇,甚至直接就用領(lǐng)域模型中的 Address 值對(duì)象
public class StudentViewModel
{
[Key] public Guid Id { get; set; }
[Required(ErrorMessage = "The Name is Required")]
[MinLength(2)]
[MaxLength(100)]
[DisplayName("Name")] public string Name { get; set; } //... 等等其他的信息 **//這個(gè)就是在領(lǐng)域模型Student中使用的奈揍,戶(hù)籍值對(duì)象**
public Address Address { get; set; }
}
3、把 Address 屬性拆開(kāi)赋续,一個(gè)一個(gè)的放在視圖模型 StudentViewModel 中
public class StudentViewModel
{
[Key] public Guid Id { get; set; }
[Required(ErrorMessage = "The Name is Required")]
[MinLength(2)]
[MaxLength(100)]
[DisplayName("Name")] public string Name { get; set; } //... 等等其他學(xué)生信息男翰,比如手機(jī)號(hào),郵箱等
/// <summary>
/// 城市 /// </summary>
public string City { get; set; }**//注意這里可以進(jìn)行set 賦值操作纽乱,和值對(duì)象不是一回事**
/// <summary>
/// 區(qū)縣 /// </summary>
public string County { get; set; } /// <summary>
/// 街道 /// </summary>
public string Street { get; set; }
}
或許你還有其他啥辦法蛾绎,要是有感覺(jué)更好的,或者更正確的,千萬(wàn)要評(píng)論留言喲租冠,只不過(guò)這三種辦法是我親身實(shí)驗(yàn)的鹏倘,這里大家先思考一下,希望看完本文你會(huì)有一些自己的想法顽爹。
零纤泵、今天實(shí)現(xiàn)藍(lán)色的部分
一、創(chuàng)建 Student 的添加模塊
話(huà)說(shuō)上次咱們是把領(lǐng)域模型(包括實(shí)體和值對(duì)象)通過(guò)EFCore保存到了數(shù)據(jù)庫(kù)镜粤,然后也查詢(xún)出來(lái)了相應(yīng)的學(xué)習(xí)信息捏题,(這里注意下,學(xué)習(xí)的戶(hù)籍信息還沒(méi)有取出來(lái))肉渴,這里說(shuō)一下為什么是戶(hù)籍地址信息公荧,
上篇文章中,有小伙伴還是對(duì)這個(gè)不是很理解同规,一直想著要一定和數(shù)據(jù)庫(kù)對(duì)應(yīng)上循狰,比如說(shuō),為啥叫地址捻浦,那如果學(xué)生有多個(gè)地址咋辦晤揣;再比如,這樣修改學(xué)生信息朱灿,值對(duì)象就會(huì)發(fā)生變化呀昧识,這樣就不能滿(mǎn)足值對(duì)象不可變的特性;等等諸如此類(lèi)的疑問(wèn)盗扒,這里說(shuō)一下:
1跪楞、值對(duì)象其實(shí)就是一個(gè)值,它和Name侣灶、Phone甸祭、Email等等一模一樣,只不過(guò)它是一個(gè)對(duì)象褥影,復(fù)雜了一些池户,有了自己的內(nèi)部結(jié)構(gòu),所以說(shuō)凡怎,值對(duì)象是沒(méi)有狀態(tài)的校焦,沒(méi)有唯一標(biāo)識(shí)(多個(gè)學(xué)生叫張三 == 兩個(gè)學(xué)生一個(gè)地址),是內(nèi)部不可變性统倒,就比如我們修改一個(gè)學(xué)校省份寨典,需要將整個(gè)值對(duì)象都修改,這和修改Name是一樣的房匆。
2耸成、值對(duì)象是一個(gè)領(lǐng)域中孕育出來(lái)的概念报亩,千萬(wàn)不要事事都要和數(shù)據(jù)庫(kù),數(shù)據(jù)模型井氢,扯上關(guān)系弦追,如果想要一個(gè)會(huì)員多個(gè)地址,那這個(gè)時(shí)候地址就是一個(gè)實(shí)體花竞,甚至是一個(gè)聚合了骗卜,比如物流地址,這也就是我為什么要把這個(gè)Address稱(chēng)之為 戶(hù)籍 的原因了左胞,從領(lǐng)域出發(fā)寇仓,而不要再和數(shù)據(jù)模型數(shù)據(jù)庫(kù)表相提并論了。
那咱們就先添加學(xué)生的 Create 模塊
1烤宙、在 StudentController 中添加 Create Action
// GET: Student/Create // 頁(yè)面
public ActionResult Create()
{ return View();
} // POST: Student/Create // 方法
[HttpPost]
[ValidateAntiForgeryToken] public ActionResult Create(StudentViewModel studentViewModel)
{ try { // 視圖模型驗(yàn)證
if (!ModelState.IsValid) return View(studentViewModel); // 執(zhí)行添加方法
_studentAppService.Register(studentViewModel);
ViewBag.Sucesso = "Student Registered!"; return View(studentViewModel);
} catch(Exception e)
{ return View(e.Message);
}
}
這個(gè)時(shí)候大家肯定都已經(jīng)很熟悉了遍烦,而且 Service 層注入什么的,相信大家已經(jīng)得心應(yīng)手了躺枕,這里都不細(xì)說(shuō)了服猪。
2、創(chuàng)建 Create View頁(yè)面
@model Christ3D.Application.ViewModels.StudentViewModel
@{
ViewData["Title"] = "Register new Student";
} <h2>@ViewData["Title"]</h2>
<form asp-action="Create">
<div class="form-horizontal">
<hr /> @* Replacing classic Validation Summary to Custom ViewComponent as TagHelper *@ <vc:summary />
<div class="form-group">
<label asp-for="Name" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Phone" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Phone" class="form-control" />
<span asp-validation-for="Phone" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="BirthDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="BirthDate" class="form-control" />
<span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-success" />
<a asp-action="Index" class="btn btn-info">Back to List</a>
</div>
</div>
</div>
</form>
這些都是 AspNetCore.Mvc.ViewFeature 的模型命令還有驗(yàn)證等拐云,相比以前的模型罢猪,已經(jīng)有很大的改善了,這個(gè)可以自己試試叉瘩,很簡(jiǎn)單膳帕,直接往下走,重頭戲來(lái)了薇缅。
這個(gè)時(shí)候危彩,如果我們添加信息保存的話(huà),一定會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題泳桦,就是戶(hù)籍信息到底如何傳入呢汤徽,上邊說(shuō)的三種辦法到底該選擇哪一種呢,下邊咱們一一來(lái)實(shí)驗(yàn)下灸撰。
二谒府、如何把值對(duì)象添加到視圖模型
這個(gè)時(shí)候肯定會(huì)有小伙伴說(shuō),為什么一定要把值對(duì)象放到視圖模型中浮毯,就比如文章的第一個(gè)方法完疫,我就不放進(jìn)去,我從頁(yè)面內(nèi)獲取到Country亲轨、Province趋惨、City等等后鸟顺,然后再傳到領(lǐng)域模型不就行了惦蚊,真的么器虾?
1、手動(dòng)賦值的方法
假設(shè)我們已經(jīng)從前臺(tái)頁(yè)面內(nèi)獲取到了戶(hù)籍信息蹦锋,然后我們就會(huì)這么做
public ActionResult Create(StudentViewModel studentViewModel,string country,string provice,string city,string street)
{ // 視圖模型驗(yàn)證
if (!ModelState.IsValid) return View(studentViewModel); //這個(gè)時(shí)候還需要對(duì)戶(hù)籍信息進(jìn)行驗(yàn)證判斷 //比如字符串不能數(shù)字兆沙,字符啥的 // 執(zhí)行添加方法,把戶(hù)籍信息傳遞過(guò)去
_studentAppService.Register(studentViewModel,country, provice, city, street);
ViewBag.Sucesso = "Student Registered!";
return View(studentViewModel);
}
Stop莉掂!相信我葛圃,你肯定不會(huì)這么做的,當(dāng)然憎妙,偶爾偶爾我們會(huì)這么接受一個(gè)參數(shù)库正,也偶會(huì)會(huì)這么寫(xiě),可是這么寫(xiě)肯定是不行的厘唾,且不說(shuō)不是DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)思想褥符,就連OOP思想也沒(méi)有發(fā)揮起來(lái),所以方法一直接pass抚垃。
這個(gè)時(shí)候我們開(kāi)始思考喷楣,至少需要把戶(hù)籍信息放到視圖模型 StudentViewMode 中吧,嗯看著文章開(kāi)頭的第二個(gè)方法就特別好鹤树!對(duì)象是吧铣焊,這個(gè)可是真是的OOP思想,全部用對(duì)象接收參數(shù)罕伯,然后把數(shù)據(jù)傳如到倉(cāng)儲(chǔ)的Add()方法中曲伊,這樣就直接保存了嘛,多好呀追他!想想的心動(dòng)熊昌,那就開(kāi)始吧,一個(gè)小坑正在慢慢變大湿酸。
2婿屹、用對(duì)象的方法將值對(duì)象添加到視圖模型中
聽(tīng)著很拗口,說(shuō)白了推溃,就是文章開(kāi)頭的第二種方法昂利,領(lǐng)域模型和視圖模型,共用一個(gè) 值對(duì)象铁坎。然后我們修改下 view 頁(yè)面蜂奸,用來(lái)傳遞參數(shù)。
<div class="form-group">
<label asp-for="BirthDate" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="BirthDate" class="form-control" />
<span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address.County" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address.County" class="form-control" />
<span asp-validation-for="Address.County" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address.Province" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address.Province" class="form-control" />
<span asp-validation-for="Address.Province" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address.City" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address.City" class="form-control" />
<span asp-validation-for="Address.City" class="text-danger"></span>
</div>
</div>
這個(gè)時(shí)候硬萍,我們一定很歡喜扩所,然后點(diǎn)擊提交,發(fā)現(xiàn)朴乖,無(wú)論怎么提交都不會(huì)在
public ActionResult Create(StudentViewModel studentViewModel)
中獲取到我們需要的戶(hù)籍信息祖屏,天哪助赞!這是啥情況,當(dāng)然是獲取不到的袁勺,因?yàn)?Address 是一個(gè)值對(duì)象雹食,具有不可變性,它的 set 都是私有的期丰,不能被賦值群叶,不信請(qǐng)看
這個(gè)時(shí)候怎么辦,聰明的你肯定能想到一個(gè)方法钝荡,既然值對(duì)象不行街立,它內(nèi)部不可變,不能賦值埠通,那我就自己在視圖模型中几晤,再寫(xiě)一個(gè) AddressViweModel 不就行啦,然后可以進(jìn)行set操作植阴,想到這里還是很激動(dòng)蟹瘾,趕緊試試,這就看看能不能獲取到值掠手。
很不錯(cuò)憾朴,已經(jīng)把內(nèi)容獲取到了,然后通過(guò)視圖對(duì)象傳到Add() 方法喷鸽,很成功的達(dá)到了目的众雷。
看來(lái)這個(gè)方法也是可以的,只不過(guò)有一個(gè)小問(wèn)題就是做祝,這里需要多了一個(gè)類(lèi)來(lái)實(shí)現(xiàn)砾省,如果我不想用類(lèi)接受,而且是直接用屬性呢混槐?那就是第三種辦法了编兄,請(qǐng)繼續(xù)往下看。
3声登、用屬性字段來(lái)講戶(hù)籍信息放到視圖模型中
就是文章開(kāi)頭的第三種辦法狠鸳,這樣的:
public class StudentViewModel
{
[Required(ErrorMessage = "The Name is Required")]
[MinLength(2)]
[MaxLength(100)]
[DisplayName("Name")] public string Name { get; set; } //... 其他
/// <summary>
/// 省份 /// </summary>
[Required(ErrorMessage = "The Province is Required")]
[DisplayName("Province")]
public string Province { get; set; } /// <summary>
/// 城市 /// </summary>
public string City { get; set; } /// <summary>
/// 區(qū)縣 /// </summary>
public string County { get; set; } /// <summary>
/// 街道 /// </summary>
public string Street { get; set; }
}
然后再修改下頁(yè)面里的調(diào)用情況,直接用調(diào)用屬性
<div class="form-group">
<label asp-for="Province" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Province" class="form-control" />
<span asp-validation-for="Province" class="text-danger"></span>
</div>
</div>
這個(gè)時(shí)候悯嗓,我們滿(mǎn)懷開(kāi)心的運(yùn)行項(xiàng)目的時(shí)候件舵,發(fā)現(xiàn),index頁(yè)面的戶(hù)籍信息沒(méi)有了脯厨,也就是說(shuō) Student -> StudentViewModel 的時(shí)候铅祸,通過(guò) Automapper 沒(méi)有成功。
然后我們提交的時(shí)候合武,發(fā)現(xiàn)后端雖然能接受到數(shù)據(jù)临梗,
可是在轉(zhuǎn)換到 Student 的時(shí)候失敗了:
這里顯示的是涡扼,我們無(wú)法對(duì)其進(jìn)行轉(zhuǎn)換,因?yàn)樵谝晥D模型中夜焦,沒(méi)有匹配到 Student 的 Address 值對(duì)象信息,不要慌岂贩,下邊我們會(huì)說(shuō)這個(gè)問(wèn)題茫经。
三、Automapper實(shí)現(xiàn)復(fù)雜對(duì)象的轉(zhuǎn)換
為了解決上一個(gè)問(wèn)題萎津,我研究了下 Automapper 官網(wǎng)卸伞,發(fā)現(xiàn),這種復(fù)雜拷貝锉屈,需要進(jìn)行手動(dòng)配置荤傲,其實(shí)也是很簡(jiǎn)單,只需要?jiǎng)?chuàng)建匹配屬性即可
注意颈渊,在第二種方法中是不需要配置的遂黍,因?yàn)榈诙N方法,兩個(gè)模型結(jié)構(gòu)幾乎一模一樣俊嗽,這第三種方法雾家,結(jié)構(gòu)已經(jīng)變了,一個(gè)是對(duì)象绍豁,一個(gè)僅僅是一個(gè)屬性值芯咧。
1、復(fù)雜領(lǐng)域模型轉(zhuǎn)換到視圖模型
/// <summary>
/// 配置構(gòu)造函數(shù)竹揍,用來(lái)創(chuàng)建關(guān)系映射 /// </summary>
public DomainToViewModelMappingProfile()
{
CreateMap<Student, StudentViewModel>()
.ForMember(d => d.County, o => o.MapFrom(s => s.Address.County))
.ForMember(d => d.Province, o => o.MapFrom(s => s.Address.Province))
.ForMember(d => d.City, o => o.MapFrom(s => s.Address.City))
.ForMember(d => d.Street, o => o.MapFrom(s => s.Address.Street))
;
}
這個(gè)時(shí)候敬飒,我們看Index頁(yè)面,戶(hù)籍信息也出來(lái)了
2芬位、視圖模型轉(zhuǎn)換到復(fù)雜領(lǐng)域模型
public ViewModelToDomainMappingProfile()
{
//手動(dòng)進(jìn)行配置
CreateMap<StudentViewModel, Student>()
.ForPath(d => d.Address.Province, o => o.MapFrom(s => s.Province))
.ForPath(d => d.Address.City, o => o.MapFrom(s => s.City))
.ForPath(d => d.Address.County, o => o.MapFrom(s => s.County))
.ForPath(d => d.Address.Street, o => o.MapFrom(s => s.Street))
;
}
這里將 Student 中的戶(hù)籍信息无拗,一一匹配到視圖模型中的屬性。
然后我們測(cè)試數(shù)據(jù)昧碉,不僅僅可以把數(shù)據(jù)獲取到蓝纲,還可以成功的轉(zhuǎn)換過(guò)去:
最后首頁(yè)查看驗(yàn)證信息,以及添加上了晌纫,完成税迷。
四、結(jié)語(yǔ)
今天呢锹漱,是補(bǔ)充了上一把的坑箭养,一共提供了三個(gè)辦法,當(dāng)然其實(shí)第一種也不算是方法哥牍,主要是后兩者毕泌,不知道大家是否能看的懂喝检,然后更傾向于哪一種:
2、不用配置 Automapper 映射信息撼泛,只需要新建一個(gè)一樣的戶(hù)籍值對(duì)象的視圖模型 —— 戶(hù)籍視圖模型即可挠说,因?yàn)榻Y(jié)構(gòu)相同,所以不需要手動(dòng)配置映射愿题,就能達(dá)到目的损俭。
3、只需要一個(gè)視圖模型即可控制潘酗,在某些情況下杆兵,我們不方便使用嵌套的復(fù)雜視圖模型,只需要配置下映射文件即可達(dá)到目的仔夺。
今天琐脏,也為下一篇做準(zhǔn)備,怎么說(shuō)呢缸兔,大家發(fā)現(xiàn)日裙,現(xiàn)在我們能正確的添加進(jìn)去了,但是如果我們要進(jìn)行驗(yàn)證該怎么辦惰蜜?比如說(shuō)阅签,我們要判斷學(xué)校不能小于14歲,手機(jī)號(hào)格式蝎抽,郵箱格式等等政钟,
當(dāng)然,你可以說(shuō)樟结,我會(huì)用前端js校驗(yàn)养交,也可以后端獲取到,if 判斷瓢宦,都是可以的碎连,
不過(guò)我個(gè)人感覺(jué),后端校驗(yàn)還是很需要的驮履,我采用 FluentValidation 進(jìn)行后端校驗(yàn)鱼辙,并且融入到 **領(lǐng)域命令 **中,那如何實(shí)現(xiàn)呢玫镐,下次再見(jiàn)咯~~~