• ABP理论学习之数据传输对象(DTO)


    返回总目录


    本篇目录

    DTO用于应用层展现层间的数据传输。

    展现层调用具有DTO参数的应用服务方法,然后应用服务使用领域对象来执行一些特定的业务逻辑,最后返回给展现层一个DTO。因此,展现层完全独立于领域层。在一个理想的分层应用中,展现层不直接和领域对象打交道(仓储,实体...)。

    为何需要DTO###

    为每个应用服务方法创建一个DTO起初可能被看作是一项乏味而又耗时的事情。但如果正确地使用它,那么DTOs可能会拯救你应用。为啥呢?

    领域层抽象

    DTO为展现层抽象领域对象提供了一种有效方式。这样,层与层之间就正确分离了。即使你想完全分离展现层,仍然可以使用已存在的应用层和领域层。相反,只要领域服务的契约(方法签名和DTOs)保持不变,即使重写领域层,完全改变数据库模式,实体和ORM框架,也不需要在展现层做任何改变。

    数据隐藏

    试想你有一个User实体,包含Id,Name,EmailAddress和Password字段。如果UserAppService的GetAllUsers()方法返回一个List,即使你没有在屏幕上显示它,那么任何人也都能看到所有user的密码。它不是涉及安全的,而是与数据隐藏相关的。应用服务都应该返回给展现层需要的,不要更多,也不很少,要的是恰到好处。

    序列化和懒加载问题

    当返回给展现层一个对象时,它很可能在某个地方序列化。比如,一个MVC方法返回JSON,一个对象会被序列化成JSON,然后发送到客户端。在那种情况,将一个实体返回到展现层是有问题的。这是怎么回事呢?

    在一个真实应用中,实体之间是相互引用的。User实体可能有一个Role的引用。因此,如果你想序列化User,那么Role也会序列化。而且,如果Role有一个List且Permission类有一个PermissionGroup类的引用等等。你能想象所有的对象都会被序列化的那种场景吗?你可能会意外地序列化整个数据库。那么解决方案是什么呢?把属性标记为NonSerilized吗?不,你可能不知道它何时应该序列化,何时不应该。它可能在一个应用方法中需要,可能在另一个就不需要了。因此,在这种情景中,设计一个可安全序列化的,特别设计的DTOs是一种好的选择。

    几乎所有的ORM框架都支持懒加载。它的特征是当需要时才从数据库中加载实体。假如说User类有一个Role类的引用。当从数据库中获得一个User时,此时Role属性还没有填充,当第一次读该Role属性时,它才从数据库中加载。因此,不要将这样的一个实体直接返回给展现层,它可能会轻易造成从数据库检索额外的实体。如果序列化工具读到了该实体,它会递归地读取所有属性,最终整个数据库可能会被检索(如果实体间有合适的关系)。

    在展现层使用实体还会有更多的问题。最好压根不要在将包含领域(业务)层的程序集引用到展现层上。

    DTO惯例和验证###

    ABP高度支持DTOs,它提供了一些符合惯例的类和接口,并且对于DTO的命名和用法提出了一些建议。当按照下面描述的那样编写代码时,ABP会轻易地自动处理一些事情。

    举个例子

    让我们看一个完整的例子。假如我们想要开发一个应用服务方法,作用是使用一个名字来搜索人,并返回一个人的集合。这种情况下,我们可能会有一个如下的Person实体:

    public class Person : Entity
    {
        public virtual string Name { get; set; }
        public virtual string EmailAddress { get; set; }
        public virtual string Password { get; set; }
    }
    
    
    

    首先,我们定义一个应用服务的接口:

    public interface IPersonAppService : IApplicationService
    {
        SearchPeopleOutput SearchPeople(SearchPeopleInput input);
    }
    
    
    

    ABP建议将input/output参数命名为MethodNameInput和 MethodNameOutput,并为每个应用服务方法定义一个单独的input和output DTO。即使你的方法只需要或返回一个参数,最好也创建一个DTO类。这样,你的代码回更具有扩展性。以后你可以添加更多的属性而不用改变方法的签名,而且也不用使已存在的客户端应用发生重大变化。

    当然,如果你的方法没有返回值,那么方法可以返回void。如果以后添加了一个返回值,也不会打破已存在的应用。如果你的方法不需要任何参数,那么你也不必定义一个输入DTO。但是如果未来很可能添加参数,那么也许最好还是编写一个输入DTO。这取决于你。

    让我们看一下为这个例子定义的输入和输出的DTO:

    public class SearchPeopleInput : IInputDto
    {
        [StringLength(40, MinimumLength = 1)]
        public string SearchedName { get; set; }
    }
    
    public class SearchPeopleOutput : IOutputDto
    {
        public List<PersonDto> People { get; set; }
    }
    
    public class PersonDto : EntityDto
    {
        public string Name { get; set; }
        public string EmailAddress { get; set; }
    }
    
    

    验证:按照惯例,输入DTO实现了 IInputDto接口,输出DTO实现了 IOutputDto接口。当实现了IInputDto时,ABP会在方法执行前自动验证输入。这和ASP.NET MVC的验证很相似,但是注意应用服务不是控制器,它是纯粹的C#类。ABP使用拦截来自动检查输入。关于更多的验证,请看下篇DTO验证。

    EntityDto是一个声明了Id属性的简单类。因为这对于所有的实体都是公用的。如果你的实体的主键不是int的,那么还有一个泛型版本。PersonDto不包含Password属性,因为表现层不需要。甚至将所有人的密码都发送到展现层可能是很危险的。想象一下,如果Javascript客户端发送请求,任何人就会轻易地抓取到所有的密码。

    接下来进一步实现之前的IPersonAppService

    public class PersonAppService : IPersonAppService
    {
        private readonly IPersonRepository _personRepository;
    
        public PersonAppService(IPersonRepository personRepository)
        {
            _personRepository = personRepository;
        }
    
        public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
        {
            //Get entities
            var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName));
    
            //Convert to DTOs
            var peopleDtoList = peopleEntityList
                .Select(person => new PersonDto
                                    {
                                        Id = person.Id,
                                        Name = person.Name,
                                        EmailAddress = person.EmailAddress
                                    }).ToList();
    
            return new SearchPeopleOutput { People = peopleDtoList };
        }
    }
    
    

    我们从数据库中获得实体,再将它们转成DTOs,然后返回到输出。注意我们没有验证输入,因为ABP会自动验证。ABP甚至会检查输入参数是否为null,如果为null,就会抛出异常。

    但是很可能你不喜欢从一个Person实体到一个PersonDto对象的转换代码。这是相当无聊的事情,而且,Person实体可能会有更多的属性。

    DTO和实体的自动映射###

    幸好,我们有工具可以让这个变得很简单。AutoMapper就是之一(要学习AutoMapper,请看我的AutoMapper系列教程。它已经发布到Nuget上了,你可以轻松地将它添加到项目中。让我们再次写一下SearchPeople方法,但是这次是用AutoMapper:

    public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
    {
        var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName));
        return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(peopleEntityList) };
    }
    
    

    这样就ok了。你可以给实体和DTO添加更多的属性而不需要转换代码做任何改变。唯一要做的事情就是在使用前定义一个映射:

    Mapper.CreateMap<Person, PersonDto>();
    
    

    AutoMapper创建了映射代码。这样,动态映射就不会成为性能问题了。它既快速又容易。AutoMapper为Person实体创建了PersonDto,并使用命名规范赋予DTO属性。命名规范可能是复杂的且可配置的。此外,你还可以定义自定义映射以及更多。

    使用特性和扩展方法进行映射

    ABP提供了若干特性和扩展方法来定义映射。首先,要将Abp.AutoMapper nuget包添加到项目中。然后,AutoMap特性是双向映射方式, AutoMapFromAutoMapTo是单向映射方式。最后,使用MapTo扩展方法将一个对象映射到另一个对象。映射定义的例子如下:

    [AutoMap(typeof(MyClass2))] //定义双向映射
    public class MyClass1
    {
        public string TestProp { get; set; }
    }
    
    public class MyClass2
    {
        public string TestProp { get; set; }
    }
    
    

    定义了上面的代码之后,就可以使用MapTo扩展方法映射它们了:

    var obj1 = new MyClass1 { TestProp = "Test value" };
    var obj2 = obj1.MapTo<MyClass2>(); //从obj1的副本创建一个新的MyClass2对象
    
    

    上面的代码从MyClass1的对象创建了MyClass2一个新的对象。此外,你可以像下面那样,映射到一个已存在的对象:

    var obj1 = new MyClass1 { TestProp = "Test value" };
    var obj2 = new MyClass2();
    obj1.MapTo(obj2);
    
    

    帮助接口###

    ASP.NET 提供了一些实现标准化公共DTO属性名称的帮助接口。

    ILimitedResultRequest定义了 MaxResultCount属性。这样你就可以在你的输入DTO中实现它来标准化有限的结果集。

    IPagedResultRequest通过添加了 SkipCount扩展了 ILimitedResultRequest。这样,我们可以在SearchPeopleInput中为分页显示实现这个接口:

    public class SearchPeopleInput : IInputDto, IPagedResultRequest
    {
        [StringLength(40, MinimumLength = 1)]
        public string SearchedName { get; set; }
    
        public int MaxResultCount { get; set; }
        public int SkipCount { get; set; }
    }
    
    

    对于一个分页请求的结果,你可以返回一个实现了IHasTotalCount的输出DTO。命名标准化帮助我们创建可重复使用的代码和惯例。你也可以在 Abp.Application.Services.Dto命名空间下看到其他的接口和类。

  • 相关阅读:
    Codeforces Round #622 C2.Skyscrapers (hard version)
    蓝桥杯 54合根植物(并查集+统计集合个数)
    蓝桥杯 6翻硬币
    Codeforces Round #622 (Div. 2) C1. Skyscrapers (easy version)(简单版本暴力)
    Codeforces Round #622 (Div. 2) A. Fast Food Restaurant
    洛谷P1734 最大约数和(01背包)
    HDU 1069 Monkey and Banana(线性DP)
    2019CSP-S T1格雷码
    eclipse使用git提交项目
    GitHub 注册失败的原因 以及解决 。
  • 原文地址:https://www.cnblogs.com/farb/p/ABPDTO.html
Copyright © 2020-2023  润新知