哈喽你们周四好,时间是过的真快,这几天一直忙着在公司的项目,而后带带新人,眼看这周要过去了,仍是要抽出时间学习学习,这些天看到群里的小伙伴也都在忙着新学习,仍是很开心的,至少当时的初衷已经达到了,一块儿学习一块儿进步嘛,哪怕是对如今或者是对之后的工做有一丢丢的帮助,也是不枉此时的努力,哈哈夜里写文章老是容易多想,好啦,废话很少说,上次我们说到了《从壹开始微服务 [ DDD ] 之七 ║项目第一次实现 & CQRS初探》,今天原本应该接着写 领域命令 了,在设计的领域命令的时候,发现了值对象的存在,对 领域模型 和 视图模型 有着剪不断理还乱的困扰,因此我就暂时单写一篇了,既是对上一篇的补充,又是对领域命令的铺垫,好啦,立刻开始今天的说明吧~~html
仍是老规矩,每篇文章先给你们一个小问题,先思考下,而后有助于理解本文:前端
问题:咱们在领域模型 Student 中,有一个户籍的值对象(为啥叫户籍,下边会说到),而后咱们也有一个学生的视图模型 StudentViewModel ,那么问题来了,咱们在 StudentViewModel 中,如何去定义这个户籍的视图模型呢,而后又是如何传给领域模型 Student 呢?git
一、不写这户籍一块,直接在业务逻辑里,手动赋值给 Student 领域模型github
public class StudentViewModel { [Required(ErrorMessage = "The Name is Required")] [MinLength(2)] [MaxLength(100)] [DisplayName("Name")] public string Name { get; set; } //... 等等其余,只是学生的我的信息,不涉及户籍地址 }
二、和领域模型同样,也写一个对象,甚至直接就用领域模型中的 Address 值对象数据库
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; } //... 等等其余的信息 //这个就是在领域模型Student中使用的,户籍值对象 public Address Address { get; set; } }
三、把 Address 属性拆开,一个一个的放在视图模型 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; } //... 等等其余学生信息,好比手机号,邮箱等
/// <summary> /// 城市 /// </summary> public string City { get; set; }//注意这里能够进行set 赋值操做,和值对象不是一回事 /// <summary> /// 区县 /// </summary> public string County { get; set; } /// <summary> /// 街道 /// </summary> public string Street { get; set; } }
或许你还有其余啥办法,要是有感受更好的,或者更正确的,千万要评论留言哟,只不过这三种办法是我亲身实验的,这里你们先思考一下,但愿看完本文你会有一些本身的想法。app
话说上次我们是把领域模型(包括实体和值对象)经过EFCore保存到了数据库,而后也查询出来了相应的学习信息,(这里注意下,学习的户籍信息尚未取出来),这里说一下为何是户籍地址信息,函数
上篇文章中,有小伙伴仍是对这个不是很理解,一直想着要必定和数据库对应上,好比说,为啥叫地址,那若是学生有多个地址咋办;再好比,这样修改学生信息,值对象就会发生变化呀,这样就不能知足值对象不可变的特性;等等诸如此类的疑问,这里说一下:微服务
一、值对象其实就是一个值,它和Name、Phone、Email等等如出一辙,只不过它是一个对象,复杂了一些,有了本身的内部结构,因此说,值对象是没有状态的,没有惟一标识(多个学生叫张三 == 两个学生一个地址),是内部不可变性,就好比咱们修改一个学校省份,须要将整个值对象都修改,这和修改Name是同样的。post
二、值对象是一个领域中孕育出来的概念,千万不要事事都要和数据库,数据模型,扯上关系,若是想要一个会员多个地址,那这个时候地址就是一个实体,甚至是一个聚合了,好比物流地址,这也就是我为何要把这个Address称之为 户籍 的缘由了,从领域出发,而不要再和数据模型数据库表相提并论了。
那我们就先添加学生的 Create 模块
// GET: Student/Create // 页面 public ActionResult Create() { return View(); } // POST: Student/Create // 方法 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(StudentViewModel studentViewModel) { try { // 视图模型验证 if (!ModelState.IsValid) return View(studentViewModel); // 执行添加方法 _studentAppService.Register(studentViewModel); ViewBag.success = "Student Registered!"; return View(studentViewModel); } catch(Exception e) { return View(e.Message); } }
这个时候你们确定都已经很熟悉了,并且 Service 层注入什么的,相信你们已经驾轻就熟了,这里都不细说了。
@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 的模型命令还有验证等,相比之前的模型,已经有很大的改善了,这个能够本身试试,很简单,直接往下走,重头戏来了。
这个时候,若是咱们添加信息保存的话,必定会发现一个问题,就是户籍信息到底如何传入呢,上边说的三种办法到底该选择哪种呢,下边我们一一来实验下。
这个时候确定会有小伙伴说,为何必定要把值对象放到视图模型中,就好比文章的第一个方法,我就不放进去,我从页面内获取到Country、Province、City等等后,而后再传到领域模型不就好了,真的么?
假设咱们已经从前台页面内获取到了户籍信息,而后咱们就会这么作(红色部分)
public ActionResult Create(StudentViewModel studentViewModel,string country,string provice,string city,string street) { // 视图模型验证 if (!ModelState.IsValid) return View(studentViewModel); //这个时候还须要对户籍信息进行验证判断 //好比字符串不能数字,字符啥的 // 执行添加方法,把户籍信息传递过去
_studentAppService.Register(studentViewModel,country, provice, city, street); ViewBag.success = "Student Registered!"; return View(studentViewModel); }
Stop!相信我,你确定不会这么作的,固然,偶尔偶尔咱们会这么接受一个参数,也偶尔会这么写,但是这么写确定是不行的,且不说不是DDD领域驱动设计思想,就连OOP思想也没有发挥起来,因此方法一直接pass。
这个时候咱们开始思考,至少须要把户籍信息放到视图模型 StudentViewMode 中吧,嗯看着文章开头的第二个方法就特别好!对象是吧,这个但是真是的OOP思想,所有用对象接收参数,而后把数据传如到仓储的Add()方法中,这样就直接保存了嘛,多好呀!想一想的心动,那就开始吧,一个小坑正在慢慢变大。
听着很拗口,说白了,就是文章开头的第二种方法,领域模型和视图模型,共用一个 值对象。而后咱们修改下 view 页面,用来传递参数。
<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>
这个时候,咱们必定很欢喜,而后点击提交,发现,不管怎么提交都不会在
public ActionResult Create(StudentViewModel studentViewModel)
中获取到咱们须要的户籍信息,天哪!这是啥状况,固然是获取不到的,由于 Address 是一个值对象,具备不可变性,它的 set 都是私有的,不能被赋值,不信请看
这个时候怎么办,聪明的你确定能想到一个方法,既然值对象不行,它内部不可变,不能赋值,那我就本身在视图模型中,再写一个 AddressViewModel 不就行啦,而后能够进行set操做,想到这里仍是很激动,赶忙试试,这就看看能不能获取到值。
很不错,已经把内容获取到了,而后经过视图对象传到Add() 方法,很成功的达到了目的。
看来这个方法也是能够的,只不过有一个小问题就是,这里须要多了一个类来实现,若是我不想用类接受,并且是直接用属性呢?那就是第三种办法了,请继续往下看。
就是文章开头的第三种办法,这样的:
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> /// 区县 /// </summary> public string County { get; set; } /// <summary> /// 街道 /// </summary> public string Street { get; set; } }
而后再修改下页面里的调用状况,直接用调用属性
<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>
这个时候,咱们满怀开心的运行项目的时候,发现,index页面的户籍信息没有了,也就是说 Student -> StudentViewModel 的时候,经过 Automapper 没有成功。
而后咱们提交的时候,发现后端虽然能接受到数据,
但是在转换到 Student 的时候失败了:
这里显示的是,咱们没法对其进行转换,由于在视图模型中,没有匹配到 Student 的 Address 值对象信息,不要慌,下边咱们会说这个问题。
为了解决上一个问题,我研究了下 Automapper 官网,发现,这种复杂拷贝,须要进行手动配置,其实也是很简单,只须要建立匹配属性便可
注意,在第二种方法中是不须要配置的,由于第二种方法,两个模型结构几乎如出一辙,这第三种方法,结构已经变了,一个是对象,一个仅仅是一个属性值。
/// <summary> /// 配置构造函数,用来建立关系映射 /// </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)) ; }
这个时候,咱们看Index页面,户籍信息也出来了
public ViewModelToDomainMappingProfile()
{
//手动进行配置
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 中的户籍信息,一一匹配到视图模型中的属性。
而后咱们测试数据,不只仅能够把数据获取到,还能够成功的转换过去:
最后首页查看验证信息,以及添加上了,完成。
今天呢,是补充了上一把的坑,一共提供了三个办法,固然其实第一种也不算是方法,主要是后二者,不知道你们是否能看的懂,而后更倾向于哪种:
二、不用配置 Automapper 映射信息,只须要新建一个同样的户籍值对象的视图模型 —— 户籍视图模型便可,由于结构相同,因此不须要手动配置映射,就能达到目的。
三、只须要一个视图模型便可控制,在某些状况下,咱们不方便使用嵌套的复杂视图模型,只须要配置下映射文件便可达到目的。
今天,也为下一篇作准备,怎么说呢,你们发现,如今咱们能正确的添加进去了,可是若是咱们要进行验证该怎么办?好比说,咱们要判断学校不能小于14岁,手机号格式,邮箱格式等等,
固然,你能够说,我会用前端js校验,也能够后端获取到,if 判断,都是能够的,
不过我我的感受,后端校验仍是很须要的,我采用 FluentValidation 进行后端校验,而且融入到 领域命令 中,那如何实现呢,下次再见咯~~~