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)可以有兩種方式進行對映,一種是使用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的值賦值給新的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; }
}
對於分頁請求,你可以將實現IHasTotalCount
的Output DTO
作為返回結果。標準化屬性幫助我們建立可複用的程式碼和規範。可在Abp.Application.Services.Dto
名稱空間下檢視其他的介面和型別。