1. 程式人生 > >ABP官方文件(二十四)【資料傳輸物件】

ABP官方文件(二十四)【資料傳輸物件】

4.2 ABP應用層 - 資料傳輸物件

資料傳輸物件(Data Transfer Objects)用於應用層和展現層的資料傳輸。

展現層傳入資料傳輸物件(DTO)呼叫一個應用服務方法,接著應用服務通過領域物件執行一些特定的業務邏輯並且返回DTO給展現層。這樣展現層和領域層被完全分離開了。在具有良好分層的應用程式中,展現層不會直接使用領域物件(倉庫,實體)。

4.2.1 資料傳輸物件的作用

為每個應用服務方法建立DTO看起來是一項乏味耗時的工作。但如果你正確使用它們,這將會解救你的專案。為啥呢?

(1)抽象領域層 (Abstraction of domain layer)

在展現層中資料傳輸物件對領域物件進行了有效的抽象。這樣你的層(layers)將被恰當的隔離開來。甚至當你想要完全替換展現層時,你還可以繼續使用已經存在的應用層和領域層。反之,你可以重寫領域層,修改資料庫結構,實體和ORM框架,但並不需要對展現層做任何修改,只要你的應用層沒有發生改變。

(2)資料隱藏 (Data hiding)

想象一下,你有一個User實體擁有屬性Id, Name, EmailAddress和Password。如果UserAppService的GetAllUsers()方法的返回值型別為List\

4.2.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
{
    [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; }
}

驗證:作為約定,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實體包含大量屬性時更是如此。

4.2.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.Map<List<PersonDto>>(peopleEntityList) };
}

這就是全部程式碼。你可以在實體和DTO中新增更多的屬性,但是轉換程式碼依然保持不變。在這之前你只需要做一件事:對映

Mapper.CreateMap<Person, PersonDto>();

AutoMapper建立了對映的程式碼。這樣,動態對映就不會成為效能問題。真是快速又方便。AutoMapper根據Person實體建立了PersonDto,並根據命名約定來給PersonDto的屬性賦值。命名約定是可配置的並且很靈活。你也可以自定義對映和使用更多特性,檢視AutoMapper的文件獲取更多資訊。

使用特性(attributes)和擴充套件方法來對映 (Mapping using attributes and extension methods)

ABP提供了幾種attributes和擴充套件方法來定義對映。使用它你需要通過nuget將Abp.AutoMapper新增到你的專案中。使用AutoMap特性(attribute)可以有兩種方式進行對映,一種是使用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>(); //建立了新的MyClass2物件,並將obj1.TestProp的值賦值給新的MyClass2物件的TestProp屬性。
上面的程式碼根據MyClass1建立了新的MyClass2物件。你也可以對映已存在的物件,如下所示:
var obj1 = new MyClass1 { TestProp = "Test value" };
var obj2 = new MyClass2();
obj1.MapTo(obj2); //根據obj1設定obj2的屬性

4.2.4 輔助介面和型別

ABP還提供了一些輔助介面,定義了常用的標準化屬性。

ILimitedResultRequest 定義了 MaxResultCount 屬性。所以你可以在你的Input DTO上實現該介面來限制結果集數量。

IPagedResultRequest 擴充套件了 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; }
}

對於分頁請求,你可以將實現IHasTotalCountOutput DTO作為返回結果。標準化屬性幫助我們建立可複用的程式碼和規範。可在Abp.Application.Services.Dto名稱空間下檢視其他的介面和型別。