38

我已经阅读了 基于消息的 Web 服务的优势一文,想知道是否有推荐的风格/实践来对 ServiceStack 中的 Restful 资源进行版本控制?不同的版本可能会在请求 DTO 中呈现不同的响应或具有不同的输入参数。

我倾向于使用 URL 类型版本控制(即 /v1/movies/{Id}),但我看到了在 HTTP 标头中设置版本的其他做法(即 Content-Type: application/vnd.company.myapp-v2 )。

我希望有一种适用于元数据页面的方法,但并不像我注意到的那样在渲染路由时只需使用文件夹结构/命名空间就可以正常工作。

例如(这不会在元数据页面中正确呈现,但如果您知道直接路由/url,则可以正常执行)

  • /v1/movies/{id}
  • /v1.1/movies/{id}

代码

namespace Samples.Movies.Operations.v1_1
{
    [Route("/v1.1/Movies", "GET")]
    public class Movies
    {
       ...
    } 
}
namespace Samples.Movies.Operations.v1
{
    [Route("/v1/Movies", "GET")]
    public class Movies
    {
       ...
    }   
}

以及相应的服务...

public class MovieService: ServiceBase<Samples.Movies.Operations.v1.Movies>
{
    protected override object Run(Samples.Movies.Operations.v1.Movies request)
    {
    ...
    }
}

public class MovieService: ServiceBase<Samples.Movies.Operations.v1_1.Movies>
    {
        protected override object Run(Samples.Movies.Operations.v1_1.Movies request)
        {
        ...
        }
    }
4

3 回答 3

64

尝试发展(而不是重新实现)现有服务

对于版本控制,如果您尝试为不同的版本端点维护不同的静态类型,您将陷入困境。我们最初是沿着这条路线开始的,但是一旦您开始支持您的第一个版本,维护同一服务的多个版本的开发工作就会爆炸式增长,因为您将需要维护不同类型的手动映射,这很容易泄露到必须维护多个并行实现,每个都耦合到不同的版本类型 - 严重违反 DRY。这对于动态语言来说不是什么问题,因为相同的模型可以很容易地被不同的版本重用。

利用序列化程序中的内置版本控制

我的建议不是显式版本,而是利用序列化格式中的版本控制功能。

例如:您通常不需要担心 JSON 客户端的版本控制,因为JSON 和 JSV 序列化器的版本控制功能更具弹性

防御性地增强您现有的服务

使用 XML 和 DataContract,您可以在不进行重大更改的情况下自由添加和删除字段。如果您将IExtensibleDataObjectDTO 添加到您的响应中,您也有可能访问未在 DTO 上定义的数据。我的版本控制方法是防御性编程,因此不会引入重大更改,您可以验证使用旧 DTO 进行集成测试的情况。以下是我遵循的一些提示:

  • 永远不要更改现有属性的类型 - 如果您需要它是不同的类型,请添加另一个属性并使用旧的/现有的属性来确定版本
  • 程序防御性地意识到旧客户不存在哪些属性,因此不要强制它们。
  • 保留一个全局命名空间(仅与 XML/SOAP 端点相关)

我通过使用您的每个 DTO 项目的AssemblyInfo.cs中的 [assembly] 属性来做到这一点:

[assembly: ContractNamespace("http://schemas.servicestack.net/types", 
    ClrNamespace = "MyServiceModel.DtoTypes")]

程序集属性使您免于在每个 DTO 上手动指定显式命名空间,即:

namespace MyServiceModel.DtoTypes {
    [DataContract(Namespace="http://schemas.servicestack.net/types")]
    public class Foo { .. }
}

如果您想使用不同于上述默认命名空间的 XML 命名空间,您需要将其注册到:

SetConfig(new EndpointHostConfig {
    WsdlServiceNamespace = "http://schemas.my.org/types"
});

在 DTO 中嵌入版本控制

大多数时候,如果您进行防御性编程并优雅地发展您的服务,您将不需要确切知道特定客户端正在使用哪个版本,因为您可以从填充的数据中推断出它。但在极少数情况下,您的服务需要根据客户端的特定版本调整行为,您可以在 DTO 中嵌入版本信息。

随着您发布的 DTO 的第一个版本,您可以愉快地创建它们而无需考虑版本控制。

class Foo {
  string Name;
}

但可能由于某种原因,Form/UI 发生了更改,您不再希望客户端使用模棱两可的Name变量,并且您还想跟踪客户端使用的特定版本:

class Foo {
  Foo() {
     Version = 1;
  }
  int Version;
  string Name;
  string DisplayName;
  int Age;
}

后来在团队会议上讨论过,DisplayName 不够好,您应该将它们分成不同的字段:

class Foo {
  Foo() {
     Version = 2;
  }
  int Version;
  string Name;
  string DisplayName;
  string FirstName;
  string LastName;  
  DateTime? DateOfBirth;
}

因此,当前状态是您有 3 个不同的客户端版本,现有调用如下所示:

v1 版本:

client.Post(new Foo { Name = "Foo Bar" });

v2 版本:

client.Post(new Foo { Name="Bar", DisplayName="Foo Bar", Age=18 });

v3 版本:

client.Post(new Foo { FirstName = "Foo", LastName = "Bar", 
   DateOfBirth = new DateTime(1994, 01, 01) });

您可以继续在同一实现中处理这些不同的版本(将使用最新的 v3 版本的 DTO),例如:

class FooService : Service {

    public object Post(Foo request) {
        //v1: 
        request.Version == 0 
        request.Name == "Foo"
        request.DisplayName == null
        request.Age = 0
        request.DateOfBirth = null

        //v2:
        request.Version == 2
        request.Name == null
        request.DisplayName == "Foo Bar"
        request.Age = 18
        request.DateOfBirth = null

        //v3:
        request.Version == 3
        request.Name == null
        request.DisplayName == null
        request.FirstName == "Foo"
        request.LastName == "Bar"
        request.Age = 0
        request.DateOfBirth = new DateTime(1994, 01, 01)
    }
}
于 2012-09-13T19:05:30.500 回答
2

构建问题

API 是系统中公开其表达式的部分。它定义了在您的域中进行通信的概念和语义。当您想改变可以表达的内容或表达方式时,问题就来了。

表达的方法和表达的内容都可能存在差异。第一个问题往往是令牌的差异(名字和姓氏而不是名字)。第二个问题是表达不同的东西(重命名自己的能力)。

一个长期的版本控制解决方案将需要解决这两个挑战。

发展 API

通过更改资源类型来发展服务是一种隐式版本控制。它使用对象的构造来确定行为。当表达方法(如名称)只有很小的变化时,它的效果最好。它不适用于更复杂的表达方式变化或表现力变化的变化。代码往往分散在各处。

特定版本控制

当更改变得更加复杂时,保持每个版本的逻辑分开是很重要的。即使在 mythz 示例中,他也为每个版本隔离了代码。但是,代码仍然以相同的方法混合在一起。不同版本的代码很容易开始相互折叠,并且很可能会散开。摆脱对以前版本的支持可能很困难。

此外,您需要使旧代码与其依赖项的任何更改保持同步。如果数据库发生更改,支持旧模型的代码也需要更改。

更好的方法

我发现最好的方法是直接解决表达问题。每次发布新版本的 API 时,都会在新层之上实现。这通常很容易,因为变化很小。

它确实在两个方面大放异彩:首先,处理映射的所有代码都在一个位置,因此以后很容易理解或删除;其次,随着新 API 的开发(俄罗斯娃娃模型),它不需要维护。

问题在于新 API 的表现力不如旧 API。无论保留旧版本的解决方案是什么,这都是一个需要解决的问题。很明显存在问题以及解决该问题的方法是什么。

这个风格的mythz示例中的示例是:

namespace APIv3 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var data = repository.getData()
            request.FirstName == data.firstName
            request.LastName == data.lastName
            request.DateOfBirth = data.dateOfBirth
        }
    }
}
namespace APIv2 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v3Request = APIv3.FooService.OnPost(request)
            request.DisplayName == v3Request.FirstName + " " + v3Request.LastName
            request.Age = (new DateTime() - v3Request.DateOfBirth).years
        }
    }
}
namespace APIv1 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v2Request = APIv2.FooService.OnPost(request)
            request.Name == v2Request.DisplayName
        }
    }
}

每个暴露的物体都是清晰的。相同的映射代码仍然需要以两种样式编写,但在分离样式中,只需要编写与类型相关的映射。无需显式映射不适用的代码(这只是另一个潜在的错误来源)。当您添加未来的 API 或更改 API 层的依赖关系时,以前的 API 的依赖关系是静态的。例如,如果数据源发生更改,那么只有最新的 API(版本 3)需要以这种方式更改。在组合样式中,您需要为支持的每个 API 编写更改代码。

评论中的一个问题是向代码库添加类型。这不是问题,因为这些类型是暴露在外部的。在代码库中显式提供类型使它们易于在测试中发现和隔离。明确的可维护性要好得多。另一个好处是这种方法不会产生额外的逻辑,而只会增加额外的类型。

于 2012-09-14T22:43:57.777 回答
2

我也在尝试为此提供解决方案,并正在考虑执行以下操作。(基于大量谷歌搜索和 StackOverflow 查询,因此这是建立在许多其他人的肩膀上的。)

首先,我不想争论版本是否应该在 URI 或请求标头中。这两种方法各有利弊,所以我认为我们每个人都需要使用最符合我们要求的方法。

这是关于如何设计/架构 Java 消息对象和资源实现类。

所以让我们开始吧。

我将分两步解决这个问题。次要更改(例如 1.0 到 1.1)和主要更改(例如 1.1 到 2.0)

小改动的方法

因此,假设我们使用 @mythz 使用的相同示例类

最初我们有

class Foo {   string Name; }

我们以 /V1.0/fooresource/{id} 的形式提供对该资源的访问

在我的用例中,我使用 JAX-RS,

@Path("/{versionid}/fooresource")
public class FooResource {

    @GET
    @Path( "/{id}" )
    public Foo getFoo (@PathParam("versionid") String versionid, (@PathParam("id") String fooId) 
    {
      Foo foo = new Foo();
     //setters, load data from persistence, handle business logic etc                   
     Return foo;
    }
}

现在假设我们向 Foo 添加了 2 个附加属性。

class Foo { 
    string Name;   
    string DisplayName;   
    int Age; 
}

我此时所做的是使用 @Version 注释来注释属性

class Foo { 
    @Version(“V1.0")string Name;   
    @Version(“V1.1")string DisplayName;   
    @Version(“V1.1")int Age; 
}

然后我有一个响应过滤器,它将基于请求的版本,仅将与该版本匹配的属性返回给用户。请注意,为方便起见,如果所有版本都应返回属性,那么您只需不要对其进行注释,过滤器将返回它而与请求的版本无关

这有点像中介层。我所解释的是一个简单的版本,它可能会变得非常复杂,但希望你能明白。

主要版本的方法

现在,当从一个版本到另一个版本进行了大量更改时,这可能会变得相当复杂。那是我们需要转移到第二个选项的时候。

选项 2 本质上是分支代码库,然后在该代码库上进行更改,并将两个版本托管在不同的上下文中。在这一点上,我们可能需要稍微重构代码库以消除方法一中引入的版本中介复杂性(即使代码更干净)这可能主要在过滤器中。

请注意,这只是我想的,还没有实现它,想知道这是否是个好主意。

另外我想知道是否有好的中介引擎/ESB 可以在不必使用过滤器的情况下进行这种类型的转换,但还没有看到任何像使用过滤器一样简单的转换。可能我搜索的还不够。

有兴趣了解他人的想法以及此解决方案是否可以解决原始问题。

于 2012-10-17T01:45:11.670 回答