文檔目錄
本節內容:
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命名空間下查看其它接口和類。