• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    解析ABP框架中的数据传输对象与应用服务

    数据传输对象(DTOs)
    数据传输对象(Data Transfer Objects)用于应用层和展现层的数据传输。

    展现层传入数据传输对象(DTO)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回DTO给展现层。这样展现层和领域层被完全分离开了。在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体)。

    1.数据传输对象的作用:
    为每个应用服务方法创建DTO看起来是一项乏味耗时的工作。但如果你正确使用它们,这将会解救你的项目。为啥呢?

    (1)抽象领域层 (Abstraction of domain layer)

    在展现层中数据传输对象对领域对象进行了有效的抽象。这样你的层(layers)将被恰当的隔离开来。甚至当你想要完全替换展现层时,你还可以继续使用已经存在的应用层和领域层。反之,你可以重写领域层,修改数据库结构,实体和ORM框架,但并不需要对展现层做任何修改,只要你的应用层没有发生改变。

    (2)数据隐藏 (Data hiding)

    想象一下,你有一个User实体拥有属性Id, Name, EmailAddress和Password。如果UserAppService的GetAllUsers()方法的返回值类型为List。这样任何人都可以查看所有人的密码,即使你没有将它打印在屏幕上。这不仅仅是安全问题,这还跟数据隐藏有关。应用服务应只返回展现层所需要的,不多不少刚刚好。

    (3)序列化 惰性加载 (Serialization lazy load problems)

    当你将数据(对象)返回给展现层时,数据有可能会被序列化。举个例子,在一个返回Json的MVC的Action中,你的对象需要被序列化成JSON并发送给客户端。直接返回实体给展现层将有可能会出现麻烦。

    在真实的项目中,实体会引用其他实体。User实体会引用Role实体。所以,当你序列化User时,Role也将被序列化。而且Role还拥有一个List并且Permission还引用了PermissionGroup等等….你能想象这些对象都将被序列化吗?这有很有可能使整个数据库数据意外的被序列化。那么该如何解决呢?将属性标记为不可序列化?不行,因为你不知道属性何时该被序列化何时不该序列化。所以在这种情况下,返回一个可安全序列化,特别定制的数据传输对象是不错的选择哦。

    几乎所有的ORM框架都支持惰性加载。只有当你需要加载实体时它才会被加载。比如User类型引用Role类型。当你从数据库获取User时,Role属性并没有被填充。当你第一次读取Role属性时,才会从数据库中加载Role。所以,当你返回这样一个实体给展现层时,很容易引起副作用(从数据库中加载)。如果序列化工具读取实体,它将会递归地读取所有属性,这样你的整个数据库都将会被读取。

    在展现层中使用实体还会有更多的问题。最佳的方案就是展现层不应该引用任何包含领域层的程序集。

    2.DTO 约定 验证
    ABP对数据传输对象提供了强大的支持。它提供了一些相关的(Conventional)类型 接口并对DTO命名和使用约定提供了建议。当你像这里一样使用DTO,ABP将会自动化一些任务使你更加轻松。

    一个例子 (Example)

    让我们来看一个完整的例子。我们相要编写一个应用服务方法根据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建议命名input/ouput对象类似于MethodNameInput/MethodNameOutput,对于每个应用服务方法都需要将Input和Output进行分开定义。甚至你的方法只接收或者返回一个值,也最好创建相应的DTO类型。这样,你的代码才会更具有扩展性,你可以添加更多的属性而不需要更改方法的签名,这并不会破坏现有的客户端应用。

    当然,方法返回值有可能是void,之后你添加一个返回值并不会破坏现有的应用。如果你的方法不需要任何参数,那么你不需要定义一个Input Dto。但是创建一个Input Dto可能是个更好的方案,因为该方法在将来有可能会需要一个参数。当然是否创建这取决于你。 Input和Output DTO类型定义如下:

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

    验证:作为约定,Input DTO实现IInputDto 接口,Output DTO实现IOutputDto接口。当你声明IInputDto参数时, 在方法执行前ABP将会自动对其进行有效性验证。这类似于ASP.NET MVC验证机制,但是请注意应用服务并不是一个控制器(Controller)。ABP对其进行拦截并检查输入。查看DTO 验证(DTO Validation)文档获取更多信息。 EntityDto是一个简单具有与实体相同的Id属性的简单类型。如果你的实体Id不为int型你可以使用它泛型版本。EntityDto也实现了IDto接口。你可以看到PersonDto并不包含Password属性,因为展现层并不需要它。

    跟进一步之前我们先实现IPersonAppService:

    public class PersonAppService : IPersonAppService
    {
      private readonly IPersonRepository _personRepository;
    
      public PersonAppService(IPersonRepository personRepository)
      {
        _personRepository = personRepository;
      }
      public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
      {
        //获取实体
        var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName));
    
        //转换成DTO
        var peopleDtoList = peopleEntityList
          .Select(person => new PersonDto
                    {
                      Id = person.Id,
                      Name = person.Name,
                      EmailAddress = person.EmailAddress
                    }).ToList();
    
        return new SearchPeopleOutput { People = peopleDtoList };
      }
    }
    
    

     
    我们从数据库获取实体,将实体转换成DTO并返回output。注意我们没有手动检测Input的数据有效性。ABP会自动验证它。ABP甚至会检查Input是否为null,如果为null则会抛出异常。这避免了我们在每个方法中都手动检查数据有效性。

    但是你很可能不喜欢手动将Person实体转换成PersonDto。这真的是个乏味的工作。Peson实体包含大量属性时更是如此。

    3.DTO和实体间的自动映射
    还好这里有些工具可以让映射(转换)变得十分简单。AutoMapper就是其中之一。你可以通过nuget把它添加到你的项目中。让我们使用AutoMapper来重写SearchPeople方法:

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

    这就是全部代码。你可以在实体和DTO中添加更多的属性,但是转换代码依然保持不变。在这之前你只需要做一件事:映射

    Mapper.CreateMapPerson, PersonDto>();
    

    AutoMapper创建了映射的代码。这样,动态映射就不会成为性能问题。真是快速又方便。AutoMapper根据Person实体创建了PersonDto,并根据命名约定来给PersonDto的属性赋值。命名约定是可配置的并且很灵活。你也可以自定义映射和使用更多特性,查看AutoMapper的文档获取更多信息。

    4.使用特性(attributes)和扩展方法来映射 (Mapping using attributes and extension methods)

    ABP提供了几种attributes和扩展方法来定义映射。使用它你需要通过nuget将Abp.AutoMapper添加到你的项目中。使用AutoMap特性(attribute)可以有两种方式进行映射,一种是使用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.MapToMyClass2>(); //创建了新的MyClass2对象,并将obj1.TestProp的值赋值给新的MyClass2对象的TestProp属性。
    上面的代码根据MyClass1创建了新的MyClass2对象。你也可以映射已存在的对象,如下所示:
    var obj1 = new MyClass1 { TestProp = "Test value" };
    var obj2 = new MyClass2();
    obj1.MapTo(obj2); //根据obj1设置obj2的属性
    

    5.辅助接口和类型
    ABP还提供了一些辅助接口,定义了常用的标准化属性。

    ILimitedResultRequest定义了MaxResultCount属性。所以你可以在你的Input DTO上实现该接口来限制结果集数量。

    IPagedResultRequest扩展了ILimitedResultRequest,它添加了SkipCount属性。所以我们在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的Output DTO作为返回结果。标准化属性帮助我们创建可复用的代码和规范。可在Abp.Application.Services.Dto命名空间下查看其他的接口和类型。

    应用服务
    应用服务用于将领域(业务)逻辑暴露给展现层。展现层通过传入DTO(数据传输对象)参数来调用应用服务,而应用服务通过领域对象来执行相应的业务逻辑并且将DTO返回给展现层。因此,展现层和领域层将被完全隔离开来。在一个理想的层级项目中,展现层应该从不直接访问领域对象。

    1.IApplicationService接口
    在ABP中,一个应用服务需要实现IApplicationService接口。最好的实践是针对每个应用服务都创建相应的接口。所以,我们首先定义一个应用服务接口,如下所示:

    public interface IPersonAppService : IApplicationService
    {
      void CreatePerson(CreatePersonInput input);
    }
    

    IPersonAppService只有一个方法,它将被展现层调用来创建一个新的Person。CreatePersonInput是一个DTO对象,如下所示:

     

    public class CreatePersonInput : IInputDto
    {
      [Required]
      public string Name { get; set; }
    
      public string EmailAddress { get; set; }
    }
    
    
    接着,我们实现IPersonAppService接口: 
    public class PersonAppService : IPersonAppService
    {
      private readonly IRepositoryPerson> _personRepository;
      public PersonAppService(IRepositoryPerson> personRepository)
      {
        _personRepository = personRepository;
      }
    
      public void CreatePerson(CreatePersonInput input)
      {
        var person = _personRepository.FirstOrDefault(p => p.EmailAddress == input.EmailAddress);
        if (person != null)
        {
          throw new UserFriendlyException("There is already a person with given email address");
        }
    
        person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
        _personRepository.Insert(person);
      }
    }
    
    

     
    以下是几个重要提示:

    2.应用服务类型

    应用服务(Application Services)需要实现IApplicationService接口。当然,你可以选择将你的应用服务(Application Services)继承自ApplicationService基类,这样你的应用服务也就自然而然的实现IApplicationService接口了。ApplicationService基类提供了方便的日志记录和本地化功能。在此建议你针对你的应用程序创建一个应用服务基类继承自ApplicationService类型。这样你就可以添加一些公共的功能来提供给你的所有应用服务使用。一个应用服务示例如下所示:

    public class TaskAppService : ApplicationService, ITaskAppService
    {
      public TaskAppService()
      {
        LocalizationSourceName = "SimpleTaskSystem";
      }
    
      public void CreateTask(CreateTaskInput input)
      {
        //记录日志,Logger定义在ApplicationService中
        Logger.Info("Creating a new task with description: " + input.Description);
    
        //获取本地化文本(L是LocalizationHelper.GetString(...)的简便版本, 定义在 ApplicationService类型)
        var text = L("SampleLocalizableTextKey");
    
        //TODO: Add new task to database...
      }
    }
    
    

     本例中我们在构造函数中定义了LocalizationSourceName,但你可以在基类中定义它,这样你就不需要在每个具体的应用服务中定义它。查看日志记录(logging)和本地化(localization)文档可以获取更多的相关信息。


    您可能感兴趣的文章:
    • ABP框架中的日志功能完全解析
    • 详解ABP框架的参数有效性验证和权限验证
    • 详解ABP框架中领域层的领域事件Domain events
    • 解析ABP框架中的事务处理和工作单元
    • 详解ABP框架中的数据过滤器与数据传输对象的使用
    • 解析ABP框架领域层中的实体类与仓储类
    • 详解ABP框架中Session功能的使用方法
    • 详解ABP框架中的日志管理和设置管理的基本配置
    • ABP框架的基础配置及依赖注入讲解
    • ABP框架的体系结构及模块系统讲解
    • ASP.NET样板项目ABP框架的特性总结
    • 基于ASP.NET MVC的ABP框架入门学习教程
    • ABP框架中导航菜单的使用及JavaScript API获取菜单的方法
    上一篇:解读ASP.NET 5 & MVC6系列教程(17):MVC中的其他新特性
    下一篇:利用ASP.NET MVC和Bootstrap快速搭建响应式个人博客站(一)
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    解析ABP框架中的数据传输对象与应用服务 解析,ABP,框架,中的,数据传输,