首页 > 解决方案 > 同一实体的多个 Dto

问题描述

对不同 API 端点中的同一实体使用多个 DTO 是否是一种好习惯。例如:我有一个 api 端点,它接受以下 Dto:

public class AddressDto
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

现在有第二个 Api 接受相同的 dto,但在那个 api 调用中我只使用了Streer1, Street2, Contact所有其他的被忽略。

我应该再做一个DTOapi endpoint

public class AddressDtoForSecondAPI
{
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
}

标签: c#.netasp.net-web-api2api-design

解决方案


简而言之,是的,这是可以接受的。


但是,正如您在评论和其他答案中看到的那样,并非所有人都同意。所以让我解释一下我的答案。

论点 1 - 误导消费者

现在有第二个 Api 接受相同的 dto,但在那个 api 调用中我只使用了Streer1, Street2, Contact所有其他的被忽略。

这里的问题之一是明确你的意图。如果您允许消费者向您发送完整的AddressDTO,但只使用属性的子集,那么您就是在误导您的消费者。你让他们认为其他属性是相关的。

这实际上与以下内容相同:

public int AddNumbersTogether(int a, int b, int c, int d)
{
    return a + c + d; //we ignore b
}

没有存在的理由b。任何使用此方法的人在AddNumbersTogether(1,2,3,4)返回8. 语法与行为相矛盾。

是的,省略未使用的方法参数比开发第二个 DTO 更容易。但是您需要在这里保持一致并坚持相同的原则:不误导消费者

论点 2 - DTO 不是实体

您的消费者与您的 API 的交互需要在消费者不了解您的数据库记录结构的情况下进行。

这就是为什么您开始使用 DTO 而不是实体类的原因!您在采取行动和存储该行动的数据之间提供了逻辑分离。

消费者并不关心数据存储在哪里。无论您是将街道存储在与地址相同的表中,还是完全存储在不同的表(或数据库)中,在调用 API 方法的消费者范围内都无关紧要

论点 3 - 反击 S.Akbari

SOLID 中的继承和/或接口隔离原则如何?– S.Akbari

对于这种特殊情况,这些不是有效的论据。

继承是一种有缺陷的方法。是的,从技术上讲,您可以在发布的示例代码中做一些类似的事情AddressDto : AddressDtoForSecondAPI,但这是一个巨大的代码气味。
当需要第三个 DTO 时会发生什么,例如只使用邮政编码和城市名称的 DTO?您不能AddressDto从多个来源继承,并且AddressDtoForSecondAPI与新创建的AddressDtoForThirdAPI.

接口不是这里的解决方案。是的,您可以在技术上创建一个IAddressDtoForSecondAPIIAddressDtoForThirdAPI适当字段的接口,然后执行类似AddressDto : IAddressDtoForSecondAPI, IAddressDtoForThirdAPI. 但是,这又是相同的大量代码气味。

如果第二个和第三个变体有一些共享属性和一些单独的属性会发生什么?如果你应用接口隔离,那么重叠的属性需要自己抽象到一个接口中。
如果出现第四个变体,它与第二个变体有一些共同的属性,一些与第三个变体有共同的属性,一些与第二个和第三个变体同时存在,以及一些单独的属性,那么你将需要创建更多接口!

给定同一实体的足够多的变化,并重复应用界面隔离原则;你最终会得到一个实体的每个属性的接口;这需要大量的样板化。你最终会得到类似的东西:

public class AddressDto : IAddressCity, IAddressCountry, IAddressContact, IAddressStreet1, IAddressStreet2, IAddressState, IAddressZip
{
    public string City { get; set; }
    public string Country { get; set; }
    public string Contact { get; set; }
    public string Street1 { get; set; }
    public string Street2 { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
}

想象一下必须对所有班级都这样做;因为相同的原则适用于 API 使用的每个 DTO。

参数 4 - DRY 不适用于此处

我有点明白你为什么担心创建两个类。最有可能的是,您的脑海中出现了 DRY/WET 错误标志。

避免 WET 是一种很好的反射;但你不能总是听它。因为如果您要真正避免重复,那么您实际上也不应该创建单独的实体和 DTO 类,因为它们通常是彼此的复制/粘贴。

DRY 不是绝对的。以实体/DTO 为例,这里有一个平衡的考虑因素:

  • 你想不惜一切代价避免重复吗?(= 干燥)
  • 您想将您的 DAL 与您的 API 逻辑分开吗?(=关注点分离)

在这种情况下,后者通常会胜出。

同样的论点也适用于你的情况。在这种情况下,反对遵循 DRY 的论点(这是我刚刚列出的论点)远远超过了遵循 DRY 的好处。


推荐阅读