我应该在Web API服务和客户端之间共享类型吗?还有其他选项吗?

54
我们正在开发Web API RESTful服务,为我们企业所有应用程序提供通用数据访问。为了帮助,我们还将发布一个客户端API,它封装了所有HttpClient的细节,并提供了对数据的强类型访问。
我们的目标是从小处开始逐步添加功能,同时仍然保持向后兼容已部署版本的客户端API(与相同主要版本的客户端兼容)。
在讨论设计时,我们的团队刚刚进行了一次非常长的讨论,讨论是否应该在服务器和客户端之间共享类型(例如通过版本化的NuGet包),并得出了利弊...但我们没有成功决定走何种方式。
共享类型(共享程序集)
优点: - 客户端模型和服务器模型始终保持最新状态 - 没有序列化/反序列化问题,因为相同的类型被序列化/反序列化 - 没有重复
缺点: - 需要找到一种方法在服务器和客户端之间共享类型 - 即使对于序列化Json没有影响,也可能会破坏现有的客户端应用程序(更改服务器模型中类或其命名空间的名称) - 可能会意外破坏客户端
为客户端和服务器分别使用结构等效的类型
优点: - 客户端“模型”与服务器实现耦合度较低(只是服务器输出的Json的镜像,但没有硬性的“同一类型”关系) - 服务器模型可以自由演变而不会破坏任何客户端 - 可以独立于服务器模型增强客户端模型 - 客户端模型是客户端包的一部分,没有需要在服务器和客户端之间维护的“共享包”
缺点: - 在服务器代码和客户端代码之间存在重复
  • 在服务器端和客户端之间保持结构同步是一个容易出错的任务
  • 在我们团队中,似乎对于每种解决方案都有50/50的偏好。

    个人而言,我更喜欢第二个选项,因为我认为RESt是关于解耦的,而解耦意味着客户端不应该关心服务器端如何实现(哪些类型、或者是否是.NET应用程序),但我希望我们可以通过代码生成或类似方法来消除可能的重复,但找不到任何有关此主题的指导。

    共享客户端和服务器之间的类型还有其他优缺点吗?

    如果我们不共享它们,那么在尝试保持客户端模型和服务器模型同步时,有没有降低维护成本的方法


    人们在这种情况下经常忽视的一个方面是,有时您正在进行点对点应用程序,其中无法轻松分离服务器和客户端。在这种情况下,代码重复是一个相当严重的问题...没有容易回答的“正确做法”。 - nikib3ro
    1
    只需添加一个关于此主题的 Twitter 对话链接:https://twitter.com/UdiDahan/status/933381212764360704 - tsimbalar
    松耦合的系统可能会令人害怕去修改。如果你需要进行突破性的变化,并且无法充分识别正在使用服务的客户端,那么你能做的最好的事情就是进行更改,并等待系统崩溃时尖叫声的出现。我认为添加共享组件可以帮助识别松散系统的客户端,以便在服务器和客户端之间积极管理变化。 - barrypicker
    @tsimbalar,“需要找到一种在服务器和客户端之间共享类型的方法” - 你找到这种方法了吗? - Denis Bredikhin
    1
    当然,我们把所有的类型都放在一个专门的类库中,并将其发布到我们的内部Nuget Feed。然后服务器和客户端可以引用这个Nuget包。 - tsimbalar
    3个回答

    28
    我认为,如果你不小心的话,第二个选项可能会比第一个更少符合RESTful。 REST不太关注去耦合,而更多的是管理和专注于客户端和服务器之间的耦合。
    在一个RESTful系统中,你知道客户端和服务器之间的耦合存在于媒体类型定义和链接关系定义中。
    在两个选项中,你都在有效地共享客户端和服务器之间的类型。在第一个选项中,这种共享是通过一个具体的实现来明确表示的,可以作为一个NuGet包进行管理,并与客户端和服务器独立进行版本控制。
    在第二个选项中,你有两个共享类型的实现。然而,我猜想你没有计划定义一个明确定义了这些类型属性的媒体类型。因此,你没有单一的真相来源,也没有定义客户端和服务器之间数据合同的方法。你如何知道什么时候会有一个改变会破坏客户端?至少使用一个共享库,你可以知道服务器现在正在使用共享类型的1.4.7版本,而客户端正在使用1.3.9版本。你可以使用语义化版本控制来知道你是否正在做一个将强制客户端更新的破坏性变更。
    使用第二个选项时,你有一个独立版本化的客户端和服务器,并且很难跟踪两个版本之间是否有破坏性的变化。
    明确的媒体类型始终是捕获HTTP客户端和服务器之间合同并对合同进行版本控制的最佳方式。然而,如果你不想这样做,那么共享的NuGet库就是下一步最好的选择,因为你将从客户端和服务器实现中隔离出共享部分。这是REST的一个关键目标。事实上,你实际上共享了一个共享合约的实现库,只会影响不能使用该库的其他平台上的使用者。

    我几年前创造了术语Web Pack,用于描述使用共享nuget包来包含共享耦合的想法。我在这个主题上写了一些文章这里这里


    2
    考虑到你是微软公司的人,我知道你支持在客户端和服务器中共享强类型定义。这遵循微软的历史方法,可以追溯到分布式COM以及WCF(SOAP)。非常类似RPC。 WebApi利用默认模型绑定器支持此思想。我已经与非微软中心的开发人员讨论过,并发现有时会向反方向推动 - 更少的共享,更少的RPC样式,其中服务从负载中尽可能获取所需的内容 - 较少的复杂性和框架。这似乎已成为宗教争议的主题。不确定我站在哪一边…… - barrypicker
    1
    我主张使用媒体类型来传达语义。然而,当面临两个选项时,这两个选项都依赖于基于单一语言和平台的兼容类型库,我会倡导选择最简单和最可靠的解决方案。 - Darrel Miller

    14

    我们进行了一次类似的讨论,讨论了类似的利弊,并采取了混合方法。我们在客户端和服务器之间共享一个程序集,但只共享接口。然后,在客户端基于这些接口创建了类。优势在于客户端和服务器上的实际对象可以独立地进行更改。


    如果您的 Dto 不包含其他 Dto,则可能是一个解决方案。 - IRONicMAN
    泛型可以很容易地解决这个问题。 - Beakie

    3

    领域模型类主要被定义为服务器使用。在服务器端,模型类型由控制器内定义的方法用于访问数据,例如使用实体框架。

    但是,出于某些原因,您可能希望向客户端传递另一个版本的模型对象。已知的方法是定义非常相似但不完全相同于模型类型的DTO类。

    在控制器中的每个方法中,当您从数据库获取数据时,需要将检索到的数据从其模型类型格式映射到可比较的DTO类。AutoMapper使这种映射更加容易。

    因此,您需要完成以下步骤:

    1. 在服务器项目中定义您的模型类型。
    2. 将以下包作为依赖项添加到您的服务器项目中:
      • AutoMapper
      • AutoMapper.Extensions.Microsoft.DependencyInjection(版本1.2.0)
    3. 在服务器项目中定义MappingProfile(或MappingConfiguration),并在Startup.cs中的ConfigureServices方法中使用services.AddAutoMapper()
    4. 修改您的控制器方法以对检索到的数据进行适当的映射,并返回等效的DTO作为方法的输出。
    5. 同时,您可以创建一个新项目,其中包含您的DTO类。该项目在服务器和客户端项目之间共享。

    然后,在客户端方面,您不需要知道任何模型类型的详细信息。您的客户端仅使用DTO类。这些类包含客户端所需的所有必要数据。在某些情况下,您可能需要组合多个模型对象的数据,以提供客户端的单个容器。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接