【C#】Data transfer object(DTO)簡介

顧名思義他就是一個拿來傳遞資料的物件。

  • 用來傳遞資料
  • 沒有任何操作行為、業務邏輯在裡面
  • 通常不設定建構子(不使用建構子設定初始值)

我會在這幾個時候使用 DTO

  • 方法簽章參數多(long parameter)
  • 分層架構中層與層的分離

情境一:refactor 遇到 long parameter 的時候會改用 DTO。
改成 DTO 前應該思考方法簽章中的參數是否可以聚合出有意義集合,因為有意義的集合更能讓人理解這是為了傳遞什麼資料,產生出的 DTO 更有機會在專案中重複利用。

//超多參數閱讀困難
public void SetUserInfo(string name, string address, string country, DateTime birth, string phoneNumber)
{

}
//Transform parameters
public class UserInfo
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
    public DateTime Birth { get; set; }
    public string PhoneNumber { get; set; }
}

//參數變成一個而已
public void SetUserInfo(UserInfo userInfo)
{

}

//使用端直接給定屬性值
UserInfo userInfo = new UserInfo
{
    Name = "AMA",
    Address = "",
    Country = "TW",
    Birth = new DateTime(1990, 1, 1),
    PhoneNumber = "0910-000-000"
};

通常不使用建構子設定初始值是因為仍然難以閱讀,這樣就失去了使用 DTO 的意義。

public class UserInfo
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string Country { get; set; }
    public DateTime Birth { get; set; }
    public string PhoneNumber { get; set; }

    //如果使用建構子初始值,建構子的參數跟一開始的方法簽章一樣長
    public UserInfo(string name, string address, string country, DateTime birth, string phoneNumber)
    {
        Name = name;
        Address = address;
        Country = country;
        Birth = birth;
        PhoneNumber = phoneNumber;
    }
}

情境二:分層架構中層與層的分離

//Controller
//GET api/UserInfo/AMA
public UserInfoResponseDto GetUserInfo(string name)
{
    //DB 存取層回來的物件是 UserInfo 
    UserInfo userInfo = db.GetUserInfo(name);

    //將 UserInfo 轉換成 UserInfoResponseDto
    return userInfo == null ? null: new UserInfoResponseDto()
    {
        Name = userInfo.Name,
        Address = userInfo.Address,
        Country = userInfo.Country,
        Birth = userInfo.Birth,
        PhoneNumber = userInfo.PhoneNumber
    });
}

如果回傳型態是用 UserInfo,而不是 UserInfoResponseDto,當 UserInfo 加一個 Property 是用戶端不需要的,那該怎麼辦呢?
如果沒有用 DTO 隔離的話,改 UserInfo 就會直接影響到 API 接口,而這並不是我們預期的情況,這時有使用 DTO 的話 API 接口就可以避免受到影響。


雖然說通常不使用建構子設定初始值,但遇到這種情況我還是會使用建構子設定初始值

//ClassA 與 ClassB 分別對應資料庫中的兩張資料表
//ClassA 原本擺所有產品的資料,但資料量大且異動頻率高導致一些問題,將部分產品擺到 ClassB
//Property ABC 資料型態一致,名稱也一致
//Property D 名稱一致,但資料型態一個是 double, 一個是 decimal。因為 double 運算會有溢位的問題,我們在 ClassA 時運算前都會先轉成 decimal 才計算,因此,新開的 ClassB 當時就直接將資料型態開成 decimal
//產品分在 ClassA 及 ClassB 之後因為產品有各自的特性,因此又出現了不一致的屬性 ClassA.E 跟 ClassB.F
public class ClassA
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    //資料型態 double
    public double D { get; set; }
    //只有 ClassA 才有的屬性
    public string E { get; set; }
}
public class ClassB
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    //資料型態 decimal
    public decimal D { get; set; }
    //只有 ClassB 才有的屬性
    public string F { get; set; }
}

因為有很多使用情況我需要同時處理這些產品,這樣就會需要建簽章不一樣,內容一樣的兩個方法

public void Method(ClassA a)
{
    //to do
}
public void Method(ClassB b)
{
    //to do
}

這時候方法簽章改用 DTO 就比較好維護

public class ProductDto
{
    public string A { get; set; }
    public string B { get; set; }
    public DateTime C { get; set; }
    //資料型態使用 decimal, 使用於計算時就不用再轉換型態
    public decimal D { get; set; }
    
    public ProductDto(ClassA a)
    {
        A = a.A;
        B = a.B;
        C = a.C;
        D = Convert.ToDecimal(a.D);
    }

    public ProductDto(ClassB b)
    {
        A = b.A;
        B = b.B;
        C = b.C;
        D = b.D;
    }
}


public void Method(ProductDto p)
{
    //to do
}

DTO 還有很多種使用情境,只要不破壞 DTO 不處理邏輯的原則,其餘都可以彈性調整,至於命名規則要不要後墜 DTO,部門內有共識即可。

參考資料:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5
https://stackoverflow.com/questions/72535903/asp-net-c-sharp-dto-initialization-by-constructor-with-parameters-or-paramless-c

分類: C#

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *