本篇目錄
DTO用於應用層和 展現層間的數據傳輸。
展現層調用具有DTO參數的應用服務方法,然后應用服務使用領域對象來執行一些特定的業務邏輯,最后返回給展現層一個DTO。因此,展現層完全獨立於領域層。在一個理想的分層應用中,展現層不直接和領域對象打交道(倉儲,實體...)。
為何需要DTO###
為每個應用服務方法創建一個DTO起初可能被看作是一項乏味而又耗時的事情。但如果正確地使用它,那么DTOs可能會拯救你應用。為啥呢?
領域層抽象
DTO為展現層抽象領域對象提供了一種有效方式。這樣,層與層之間就正確分離了。即使你想完全分離展現層,仍然可以使用已存在的應用層和領域層。相反,只要領域服務的契約(方法簽名和DTOs)保持不變,即使重寫領域層,完全改變數據庫模式,實體和ORM框架,也不需要在展現層做任何改變。
數據隱藏
試想你有一個User實體,包含Id,Name,EmailAddress和Password字段。如果UserAppService的GetAllUsers()方法返回一個List
序列化和懶加載問題
當返回給展現層一個對象時,它很可能在某個地方序列化。比如,一個MVC方法返回JSON,一個對象會被序列化成JSON,然后發送到客戶端。在那種情況,將一個實體返回到展現層是有問題的。這是怎么回事呢?
在一個真實應用中,實體之間是相互引用的。User實體可能有一個Role的引用。因此,如果你想序列化User,那么Role也會序列化。而且,如果Role有一個List
幾乎所有的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特性是雙向映射方式, 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>(); //從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命名空間下看到其他的接口和類。