从壹开始前后端分离【 .NET Core2.2 +Vue2 】框架之十二 、十三:DTOs(数据传输对象)

2022-04-10 10:55:48 浏览数 (1)

初探DTOs

请看以下实体类

代码语言:javascript复制
//数据库实体类
public class Author
{
    public string Name { get; set; }
}
public class Book
{
    public string Title { get; set; }
    public Author Author { get; set; }
}
//页面实体类
public class BookViewModel
{
    public string Title { get; set; }
    public string Author { get; set; }
}
//api调用
BookViewModel model = new BookViewModel
{
    Title = book.Title,
    Author = book.Author.Name
}

  上面的例子相当的直观了,我们平时也是这么用的基本,但是问题也随之而来了,我们可以看到在上面的代码中,如果一旦在Book对象里添加了一个额外的字段,而后想在前台页面输出这个字段,那么就需要去在项目里找到每一处有这样BookViewModel转换字段的地方,这是非常繁琐的。另外,BookViewModel.Author是一个string类型的字段,但是Book.Author属性却是Author对象类型的,我们用的解决方法是通过Book.Auther对象来取得Author的Name属性值,然后再赋值给BookViewModel的Author属性,这样看起来行的通,但是想一想,如果打算在以后的开发中把Name拆分成两个-FisrtName和LastName,我的天呐!我们得去把原来的ViewModel对象也拆分成对应的两个字段,然后在项目中找到所有的转换,然后替换。 那么有什么办法或者工具来帮助我们能够避免这样的情况发生呢?AutoMapper正是符合要求的一款插件。 只需一键操作,就能一劳永逸,解决所有问题,然后通过依赖注入,快速使用:

代码语言:javascript复制
        //AutoMapper自动映射
            //Mapper.Initialize(cfg => cfg.CreateMap<BlogArticle, BlogViewModels>());
            //BlogViewModels models = Mapper.Map<BlogArticle, BlogViewModels>(blogArticle);

            BlogViewModels models = IMapper.Map<BlogViewModels>(blogArticle);//就这一句话完全搞定所有转换

一、在项目中使用添加一个案例使用AutoMapper

1、普通的模型映射

在接口 IBlogArticleServices.cs和 类BlogArticleServices.cs中,添加GetBlogDetails()方法,返回类型是BlogViewModels

请看这两个类

代码语言:javascript复制
   /// <summary>
    /// 博客文章实体类
    /// </summary>
    public class BlogArticle
    {
        /// <summary>
        /// 
        /// </summary>
        public int bID { get; set; }
        /// <summary>
        /// 创建人
        /// </summary>
        public string bsubmitter { get; set; }

        /// <summary>
        /// 博客标题
        /// </summary>
        public string btitle { get; set; }

        /// <summary>
        /// 类别
        /// </summary>
        public string bcategory { get; set; }

        /// <summary>
        /// 内容
        /// </summary>
        public string bcontent { get; set; }

        /// <summary>
        /// 访问量
        /// </summary>
        public int btraffic { get; set; }

        /// <summary>
        /// 评论数量
        /// </summary>
        public int bcommentNum { get; set; }

        /// <summary> 
        /// 修改时间
        /// </summary>
        public DateTime bUpdateTime { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        public System.DateTime bCreateTime { get; set; }
        /// <summary>
        /// 备注
        /// </summary>
        public string bRemark { get; set; }
    }
-------------------------------------------------
   /// <summary>
    /// 博客信息展示类
    /// </summary>
    public class BlogViewModels
    {
        /// <summary>
        /// 
        /// </summary>
        public int bID { get; set; }
        /// <summary>/// 创建人
        /// </summary>
        public string bsubmitter { get; set; }

        /// <summary>/// 博客标题
        /// </summary>
        public string btitle { get; set; }

        /// <summary>/// 摘要
        /// </summary>
        public string digest { get; set; }

        /// <summary>
        /// 上一篇
        /// </summary>
        public string previous { get; set; }

        /// <summary>
        /// 上一篇id
        /// </summary>
        public int previousID { get; set; }

        /// <summary>
        /// 下一篇
        /// </summary>
        public string next { get; set; }

        /// <summary>
        /// 下一篇id
        /// </summary>
        public int nextID { get; set; }

        /// <summary>/// 类别
        /// </summary>
        public string bcategory { get; set; }

        /// <summary>/// 内容
        /// </summary>
        public string bcontent { get; set; }

        /// <summary>
        /// 访问量
        /// </summary>
        public int btraffic { get; set; }

        /// <summary>
        /// 评论数量
        /// </summary>
        public int bcommentNum { get; set; }

        /// <summary>/// 修改时间
        /// </summary>
        public DateTime bUpdateTime { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        public System.DateTime bCreateTime { get; set; }
        /// <summary>/// 备注
        /// </summary>
        public string bRemark { get; set; }
    }

两个实体类字段还基本可以,不是很多,但是我曾经开发一个旅游网站的系统,有一个表字段都高达30多个,当然还有更多的,额,如果我们一个个赋值是这样的

代码语言:javascript复制
            BlogViewModels models = new BlogViewModels()
            {
                bsubmitter=blogArticle.bsubmitter,
                btitle = blogArticle.btitle,
                bcategory = blogArticle.bcategory,
                bcontent = blogArticle.bcontent,
                btraffic = blogArticle.btraffic,
                bcommentNum = blogArticle.bcommentNum,
                bUpdateTime = blogArticle.bUpdateTime,
                bCreateTime = blogArticle.bCreateTime,
                bRemark = blogArticle.bRemark,
            };

所以这个方法的全部代码是:

接口层也要添加:

代码语言:javascript复制
   public interface IBlogArticleServices :IBaseServices<BlogArticle>
    {
        Task<List<BlogArticle>> getBlogs();
        Task<BlogViewModels> getBlogDetails(int id);
    }
代码语言:javascript复制
/// <summary>
/// 获取视图博客详情信息(一般版本)
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<BlogViewModels> getBlogDetails(int id)
{
    var bloglist = await dal.Query(a => a.bID > 0, a => a.bID);
    var blogArticle = (await dal.Query(a => a.bID == id)).FirstOrDefault();
    BlogViewModels models = null;

    if (blogArticle != null)
    {
        BlogArticle prevblog;
        BlogArticle nextblog;
        int blogIndex = bloglist.FindIndex(item => item.bID == id);
        if (blogIndex >= 0)
        {
            try
            {
                // 上一篇
                prevblog = blogIndex > 0 ? (((BlogArticle)(bloglist[blogIndex - 1]))) : null;
                // 下一篇
                nextblog = blogIndex   1 < bloglist.Count() ? (BlogArticle)(bloglist[blogIndex   1]) : null;


                BlogViewModels models = new BlogViewModels()
                {
                    bsubmitter = blogArticle.bsubmitter,
                    btitle = blogArticle.btitle,
                    bcategory = blogArticle.bcategory,
                    bcontent = blogArticle.bcontent,
                    btraffic = blogArticle.btraffic,
                    bcommentNum = blogArticle.bcommentNum,
                    bUpdateTime = blogArticle.bUpdateTime,
                    bCreateTime = blogArticle.bCreateTime,
                    bRemark = blogArticle.bRemark,
                };

                if (nextblog != null)
                {
                    models.next = nextblog.btitle;
                    models.nextID = nextblog.bID;
                }
                if (prevblog != null)
                {
                    models.previous = prevblog.btitle;
                    models.previousID = prevblog.bID;
                }
            }
            catch (Exception) { }
        }
        blogArticle.btraffic  = 1;
        await dal.Update(blogArticle, new List<string> { "btraffic" });
    }

    return models;

}

想了想这才是一个方法,一般的系统都会有少则几十,多则上百个这样的方法,这还不算,大家肯定遇到过一个情况,如果有一天要在页面多显示一个字段,噗!不是吧,首先要存在数据库,然后在该实体类就应该多一个,然后再在每一个赋值的地方增加一个,而且也没有更好的办法不是,一不小心就少了一个,然后被产品测试说咱们不细心,心塞哟,别慌!神器来了,一招搞定。

2、先来引入DTO讲解,以及它的原理

  在学习EF的时候我们知道了ORM(Object Relational Mapping)映射,是一种对象关系的映射,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。

而Automapper是一种实体转换关系的模型,AutoMapper是一个.NET的对象映射工具。主要作用是进行领域对象与模型(DTO)之间的转换、数据库查询结果映射至实体对象。

下边的是基本原理,大家喵一眼就行:

Ø 什么是DTO?   数据传输对象(DTO)(DataTransfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从而从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。 Ø 为什么用?   它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为何不能直接将领域对象用于数据传递?因为领域对象更注重领域,而DTO更注重数据。不仅如此,由于“富领域模型”的特点,这样做会直接将领域对象的行为暴露给表现层。   需要了解的是,数据传输对象DTO本身并不是业务对象。数据传输对象是根据UI的需求进行设计的,而不是根据领域对象进行设计的。比如,Customer领域对象可能会包含一些诸如FirstName, LastName, Email, Address等信息。但如果UI上不打算显示Address的信息,那么CustomerDTO中也无需包含这个 Address的数据”。 Ø 什么是领域对象? 领域模型就是面向对象的,面向对象的一个很重要的点就是:“把事情交给最适合的类去做”,即:“你得在一个个领域类之间跳转,才能找出他们如何交互”。在我们的系统中Model(EF中的实体)就是领域模型对象。领域对象主要是面对业务的,我们是通过业务来定义Model的。

以上的这些大家简单看看原理即可,意思大家肯定都懂,下边开始讲解如何使用

3、引入 AutoMapper 的相关包

在Blog.Core.Services项目中引用Nuget包,AutoMapper 和 AutoMapper.Extensions.Microsoft.DependencyInjection

AutoMapper.Extensions.Microsoft.DependencyInjection,这个是用来配合依赖注入的,看名字也能看的出来吧,大家回忆下,整个项目中,都是使用的依赖注入,所以尽量不要用new 来实例化,导致层耦合。

4、添加映射文件 CustomProfile.cs

基于上边原理,在接口层Blog.Core 中,添加文件夹AutoMapper,然后添加映射配置文件 CustomProfile.cs,用来匹配所有的映射对象关系

代码语言:javascript复制
     public class CustomProfile : Profile
    {
        /// <summary>
        /// 配置构造函数,用来创建关系映射
        /// </summary>
        public CustomProfile()
        {
            CreateMap<BlogArticle, BlogViewModels>();
        }
    }

下边是来自热心网友@菜工的解惑:

Profile不知有什么用,通过百度了解才了解是services.AddAutoMapper是会自动找到所有继承了Profile的类然后进行配置, 而且我的这个配置文件是在api层的,如果Profile配置类放在别的层(比如Service层), 如果没解耦的话,可以services.AddAutoMapper(),参数留空,AutoMapper会从所有引用的程序集里找继承Profile的类,如果解耦了,就得services.AddAutoMapper(Assembly.Load("Blog.Core.Service"))。

大家看下F12这个CreateMap方法:

public IMappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>();

第一个参数是原对象,第二个是目的对象,所以,要想好,是哪个方向转哪个,当然可以都写上,比如

CreateMap<BlogArticle, BlogViewModels>(); CreateMap<BlogViewModels, BlogArticle>();

代码语言:javascript复制
//如果不想一个一个的配置,可以用接口的形式,批量导入
//这是一个思路,我没有具体去写,留个坑吧

//public interface IMapperTo<TDestination>{}
//然后同样来个Profile集中处理这些interface
代码语言:javascript复制
/// <summary>
/// 根据IMapperTo<>接口 自动初始化AutoMapper
/// </summary>
public class AutoMapperProfile : Profile
{
    public override string ProfileName
    {
        get
        {
            return "AutoForIMapperTo";
        }
    }

    protected override void Configure()
    {
        base.Configure();

        typeof(SaveBuyerDemandRequest).Assembly.GetTypes()//SaveBuyerDemandRequest是TSource同属的Assembly底下的任意类,要包含多个Aeembly的话自己扩展咯
            .Where(i => i.GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IMapperTo<>)))
            .ToList().ForEach(item =>
            {
                item.GetInterfaces()
                    .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IMapperTo<>))
                    .ToList()//这里可以支持多个IMapperTo
                    .ForEach(i => {
                        var t2 = i.GetGenericArguments()[0];
                        Mapper.CreateMap(item, t2);
                        Mapper.CreateMap(t2, item);
                    });
            });
    }
}

//Class For Example
public class SaveBuyerDemandRequest : IMapperTo<BuyerDemandEntity>
{

}

5、使用AutoMapper实现模型映射,并注入

老规矩,还是在Startup中,注入服务

services.AddAutoMapper(typeof(Startup));//这是AutoMapper的2.0新特性

修改上边服务层BlogArticleServices.cs 中getBlogDetails 方法中的赋值,改用AutoMapper,并用构造函数注入

最终的代码是:

代码语言:javascript复制
 // 依赖注入
 IBlogArticleRepository dal;
 IMapper IMapper;
 public BlogArticleServices(IBlogArticleRepository dal, IMapper IMapper)
 {
     this.dal = dal;
     base.baseDal = dal;
     this.IMapper = IMapper;
 }
 /// <summary>
 /// 获取视图博客详情信息
 /// </summary>
 /// <param name="id"></param>
 /// <returns></returns>
 public async Task<BlogViewModels> getBlogDetails(int id)
        {
            var bloglist = await dal.Query(a => a.bID > 0, a => a.bID);
            var blogArticle = (await dal.Query(a => a.bID == id)).FirstOrDefault();
            BlogViewModels models = null;

            if (blogArticle != null)
            {
                BlogArticle prevblog;
                BlogArticle nextblog;
                int blogIndex = bloglist.FindIndex(item => item.bID == id);
                if (blogIndex >= 0)
                {
                    try
                    {
                        // 上一篇
                        prevblog = blogIndex > 0 ? (((BlogArticle)(bloglist[blogIndex - 1]))) : null;
                        // 下一篇
                        nextblog = blogIndex   1 < bloglist.Count() ? (BlogArticle)(bloglist[blogIndex   1]) : null;

                        // 注意就是这里,mapper
                        models = IMapper.Map<BlogViewModels>(blogArticle);

                        if (nextblog != null)
                        {
                            models.next = nextblog.btitle;
                            models.nextID = nextblog.bID;
                        }
                        if (prevblog != null)
                        {
                            models.previous = prevblog.btitle;
                            models.previousID = prevblog.bID;
                        }
                    }
                    catch (Exception) { }
                }
                blogArticle.btraffic  = 1;
                await dal.Update(blogArticle, new List<string> { "btraffic" });
            }

            return models;

        }

修改BlogController.cs中的 Get(int id)方法,运行项目,断点调试,发现已经成功了,是不是很方便,你也可以反过来试一试

代码语言:javascript复制
     [HttpGet("{id}", Name = "Get")]
        public async Task<object> Get(int id)
        {
            var model = await blogArticleServices.getBlogDetails(id);//调用该方法
            var data = new { success = true, data = model };
            return data;
        }

好啦,到目前为止,咱们已经注入了这些服务了:

6、复杂深拷贝映射

有的小伙伴问,你这个这个简单,都是相同字段的,那当然很方便啦,要是一个复杂的,比如属性名字不一样的,或者说有子类等嵌入型的咋办?放心,一样是可以的,请看

1、属性名称不一样
代码语言:javascript复制
   CreateMap<Student, StudentViewModel>()
       .ForMember(d => d.CountyName, o => o.MapFrom(s => s.County))
       .ForMember(d => d.ProvinceName, o => o.MapFrom(s => s.Province))
       ;
2、如果是还有子类的复杂类型
代码语言:javascript复制
      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))
      ;


   public class Student : Entity
    {
        public string Name { get; private set; }
        public string Email { get; private set; }
        public string Phone { get; private set; }
        public DateTime BirthDate { get; private set; }
        public Address Address { get; private set; }
    }

    public class StudentViewModel
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public DateTime BirthDate { get; set; }
        public string Phone { get; set; }
        public string Province { get; set; }
        public string City { get; set; }
        public string County { get; set; }
        public string Street { get; set; }
    }

0 人点赞