• ABP框架


    文档目录

    本节内容:

    Data Transfer Objects(DTO)用来在应用层和展现层之间传输数据。

    展现层使用一个DTO调用一个应用服务方法,然后应用服务使用服务对象执行一些特定业务逻辑,并返回一个DTO给展现层。因此,展现层是完全独立于领域层的。在一个理想的分层应用里,展现层不直接使用领域对象(仓储实体...)。

    DTO的必要性

    首先为每个应用服务方法创建一个DTO看起来是件乏味且费时的工作,但如果你正确使用它,它能解救你的应用。为什么呢?

    领域层的抽象

    dto提供一个有效的方法从展现层抽象领域对象,因此,你的层正确分离开,即使你想完全地改变展现层,也可以继续使用已存在的应用层和领域层。相反,你可以重写你的领域层、完全改变数据库结构、实体和ORM框架,只要你的应用服务契约(方法签名和DTO)保持不变,展现层也不用做任何修改。

    数据隐藏

    考虑一下:你有一个User实体,它有Id、Name、EmailAddress和Password属性,如果UserAppService的GetAllUsers()方法返回一个List<User>,任何人都可以看到所有用户的密码,即使你没有在屏幕上显示它,也是不安全的。不只是数据安全,还有就是数据的隐藏,应用服务应该只向展现层返回必要的数据,不多也不少。

    序列化和延迟加载问题

    当你返回一个数据(一个对象)给展现层时,它可能会在某处被序列化,例如:在一个返回Json的MVC方法里,你的对象会被序列化成JSON,然后发送给客户端,在这种情况下,如果返回一个实体给展示层可能会有问题,为什么呢?

    在一个真实的应用里,你的实体间可能存在相互引用,User实体可能关联到Roles(多个角色),所以如果你想序列化User,那么它的Roles也要被序列化,而Role类可能包含一个List<Permission>,Permission类可能又关联到PermissionGroup类等等。你应该能明白序列化这些对象,可能就意外的序列化了整个数据库,而如果你的对象存在循环引用,它就无法完成序列化了。

    怎么解决呢?把属性标记为NonSerialized(不序列化)?不,你不知道它何时应当被序列化又何时不应当被序列化,可能在这个应用服务里要序列化,而在另一个服务里不要序列化,所以返回一个安全地可序列化的,经过特别设计的DTO是一个好的选择。

    另一方面,几乎所有ORM框架都支持延迟加载,它是一个只在需要时从数据库加载实体的特性。假设User类有一个指向Role类的引用,当你从数据库获取一个User时,Role属性没有被填充,当你第一次读取Role属性时,它再从数据库中加载。所以你返回这么一个实体给展现层,它将去数据库获取额外的实体。如果一个序列化工具读取这个实体,它递归读取所有属性,可能又会序列化你整个数据库(如果实体间恰好存在一定的关系)。

    当然我们可以说出在展现层使用实体的更多问题,所以最好的做法是在应用层里不引用包含领域(业务)层的程序集。

    DTO 约定和验证

    ABP强支持DTO,它提供了一些约定类和接口,并建议了一些命名和使用约定,当你如本节描述的这样去写代码,ABP会自动完成一些任务。

    示例

    让我们看一个完整的示例,假设我们想开发一个通过name搜索people并返回一个people列表的应用服务,这样,我们应该有一个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建议命名输入/输出参数为:MethodNameInput和MethodNameOutput,并为每个应用服务方法定义单独的输入和输入DTO。即使你的方法只接受/返回一个参数,也最好是创建一个DTO类,因为你的代码将来可能需要扩展,你可以稍后添加更多属性,而不必修改你方法的签名也不用打断你已存在的客户端应用。

    当然,如果你的方法没有返回值,也就是void,如果你在以后添加一个返回值,它也不会打断已存在的应用。如果你的方法没有参数,你不需要定义一个输入DTO,但如果将来可能会添加参数,最好先添加一个输入DTO类,这取决于你。

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

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

    在方法开始运行前,ABP会自动验证输入,这类似于Asp.net Mvc的验证,但请注意:应用服务不是一个控制器,它就是一个单纯的C#类,ABP拦截它并自动检查输入。有很多的验证,请查阅DTO 验证文档。

    EntityDto是一个实体通用只定义Id属性的简单类,如果你有实体主键不是int,有一个泛型版本可以用。你可以不用EntityDto,但最好定义一个Id属性。

    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 };
        }
    }

    我们从数据库获取实体,把它们转换成DTO再返回给输出,注意:我们没有验证输入,ABP验证了它,它甚至验证了输入参数是否为空,为空时抛出异常,这就省得我们在每个方法里写验证代码。

    但是你可能不喜欢写把一个Person实体转换成PersonDto对象的代码,它是确实是一个乏味的工作,Person实体可能包含很多属性。

    DTO和实体间自动映射

    幸运地是:有工具使这件事变得容易,AutoMapper是其中之一,它发布在nuget上,你可以很容易地把它加入到你的项目里。让我们使用AutoMap再写一下SearchPeople方法:

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

    这样就完事了。你可以添加更多的属性到实体和DTO里,但转换代码不用修改,唯一需要做的就是在使用前定义一个映射:

    Mapper.CreateMap<Person, PersonDto>();

    AutoMapper创建映射代码,因此,动态映射不会造成性能问题,它是快速并容易的。AutoMapper为Person实体创建一个PersonDto,并按命名约定给DTO的属性赋值。命名约定可以很复杂和配置,同样,你也可以定义自己的配置及更多内容。更多信息查询AutoMapper的文档。

    你可在你的模块里的PostInitialzie里定义映射。

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

    ABP提供了多个特性和扩展方法用来定义映射,为使用它,先在你的项目里添加Abp.AutoMapper的nuget包,然后使用AutoMap特性进行双向映射,AutoMapFrom和AutoMapTo进行单向映射。使用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>(); //创建一个新的MyClass2对象,从obj1拷贝TestProp

    上面的代码从一个MyClass1对象创建一个新的MyClass2对象,同样,你也可以映射一个已存在的对象,如下所示:

    var obj1 = new MyClass1 { TestProp = "Test value" };
    var obj2 = new MyClass2();
    obj1.MapTo(obj2); //从obj1设置obj2的属性

    辅助接口和方法

    ABP提供了一些辅助的接口,通过实现这些接口可以标准化一些通用的DTO属性名。

    ILimitedResultRequest定义了MaxResultCount属性,所以可以在你的输入DTO类里实现它,从而限制结果集。

    IPagedResultRequst扩展了ILimitedResultRequest,添加了SkipCount,所以可让SearchPeopleInput实现它,从而提供分页参数:

    public class SearchPeopleInput : 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命名空间下查看。

    kid1412附:英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Data-Transfer-Objects

  • 相关阅读:
    CF做题记录
    MobaXterm左侧没有文件列表,没有SCP,不显示文件夹问题处理
    使用FastJson转换Object时,空字符串丢失的解决办法【转】
    fastjson处理复杂对象,参数为null问题定位
    python 数据库连接池
    Git找回add 后,未commit的文件(使用reset -hard 命令导致文件清除)
    nginx过滤来自特定IP和user-agent的请求
    Redis实现排行榜(带二位小数点)
    系统不做任何优化,性能提升10%的方法
    二(二)、基于注解形式配置bean
  • 原文地址:https://www.cnblogs.com/kid1412/p/6003520.html
Copyright © 2020-2023  润新知