ABP理論學習之數據傳輸對象(DTO)


返回總目錄


本篇目錄

DTO用於應用層展現層間的數據傳輸。

展現層調用具有DTO參數的應用服務方法,然后應用服務使用領域對象來執行一些特定的業務邏輯,最后返回給展現層一個DTO。因此,展現層完全獨立於領域層。在一個理想的分層應用中,展現層不直接和領域對象打交道(倉儲,實體...)。

為何需要DTO###

為每個應用服務方法創建一個DTO起初可能被看作是一項乏味而又耗時的事情。但如果正確地使用它,那么DTOs可能會拯救你應用。為啥呢?

領域層抽象

DTO為展現層抽象領域對象提供了一種有效方式。這樣,層與層之間就正確分離了。即使你想完全分離展現層,仍然可以使用已存在的應用層和領域層。相反,只要領域服務的契約(方法簽名和DTOs)保持不變,即使重寫領域層,完全改變數據庫模式,實體和ORM框架,也不需要在展現層做任何改變。

數據隱藏

試想你有一個User實體,包含Id,Name,EmailAddress和Password字段。如果UserAppService的GetAllUsers()方法返回一個List ,即使你沒有在屏幕上顯示它,那么任何人也都能看到所有user的密碼。它不是涉及安全的,而是與數據隱藏相關的。應用服務都應該返回給展現層需要的,不要更多,也不很少,要的是恰到好處。

序列化和懶加載問題

當返回給展現層一個對象時,它很可能在某個地方序列化。比如,一個MVC方法返回JSON,一個對象會被序列化成JSON,然后發送到客戶端。在那種情況,將一個實體返回到展現層是有問題的。這是怎么回事呢?

在一個真實應用中,實體之間是相互引用的。User實體可能有一個Role的引用。因此,如果你想序列化User,那么Role也會序列化。而且,如果Role有一個List 且Permission類有一個PermissionGroup類的引用等等。你能想象所有的對象都會被序列化的那種場景嗎?你可能會意外地序列化整個數據庫。那么解決方案是什么呢?把屬性標記為NonSerilized嗎?不,你可能不知道它何時應該序列化,何時不應該。它可能在一個應用方法中需要,可能在另一個就不需要了。因此,在這種情景中,設計一個可安全序列化的,特別設計的DTOs是一種好的選擇。

幾乎所有的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特性是雙向映射方式, 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>(); //從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命名空間下看到其他的接口和類。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM