ServiceStack:RESTful资源版本控制

我已经阅读了基于消息的Web服务文章的优点,并且想知道是否有推荐的样式/实践来版本化ServiceStack中的Restful资源? 请求DTO中的不同版本可以呈现不同的响应或具有不同的input参数。

我倾向于URL版本化(ie / v1 / movies / {Id}),但是我已经看到了在HTTP标头中设置版本的其他实践(即Content-Type:application / vnd.company.myapp-v2 )。

我希望能够与元数据页面一起工作的方式,但不像我注意到的那样简单地使用文件夹结构/命名空间在渲染path时工作正常。

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

  • / V1 /电影/ {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) { ... } } 

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

对于版本控制,如果您尝试为不同版本的端点维护不同的静态types,那么您将会受到伤害。 我们最初开始了这条路线,但是一旦你开始支持你的第一个版本,开发工作就会维持同一个服务的多个版本的爆炸,因为你需要维护不同types的手动映射,这种映射很容易泄漏到必须维护多个并行的实现,每个耦合到一个不同的版本types – 大量违反DRY。 对于dynamic语言来说这不是一个问题,其中相同的模型可以很容易地被不同版本重复使用。

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

我的build议不是明确版本,而是利用序列化格式中的版本控制function。

例如:您通常不需要担心使用JSON客户端进行版本控制,因为JSON和JSV串行器的版本控制function更具弹性 。

增强您的现有服务防御

使用XML和DataContract的,你可以自由地添加和删除字段,而不会造成重大改变。 如果将IExtensibleDataObject添加到您的响应DTO中,您还有可能访问DTO上未定义的数据。 我的版本pipe理方法是防御性地编程,所以不会引入重大更改,您可以validation使用旧DTO进行集成testing的情况。 以下是我遵循的一些提示:

  • 永远不要改变现有属性的types – 如果你需要它是一个不同的types添加另一个属性,并使用旧的/现有的属性来确定版本
  • 程序防御地意识到什么属性不存在与旧客户端,所以不要使他们强制性。
  • 保持一个单一的全局名称空间(仅与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中embedded版本

大多数情况下,如果您进行防御性编程并优雅地发展您的服务,则无需知道特定客户端正在使用的版本,因为您可以从填充的数据中推断出该版本。 但在less数情况下,您的服务需要根据客户端的特定版本调整行为,您可以将版本信息embedded您的DTO中。

随着您发布DTO的第一个版本,您可以快速创build它们,而不需要任何版本控制。

 class Foo { string Name; } 

但也许由于某种原因Form / UI被改变了,你不再希望Client使用模糊的Namevariables,而且你也想跟踪客户端使用的特定版本:

 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) }); 

您可以继续在相同的实现中使用这些不同的版本(这将使用最新的DTO版本的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) } } 

构build问题

API是系统公开其expression的一部分。 它定义了在您的域中进行通信的概念和语义。 当你想改变可以expression的东西或者如何expression时,问题就来了。

expression方式和expression方式都可能有所不同。 第一个问题往往是代币的差异(名字而不是名字)。 第二个问题是expression不同的东西(重新命名自己的能力)。

一个长期的版本解决scheme将需要解决这两个挑战。

演变一个API

通过改变资源types来发展服务是一种隐式的版本控制。 它使用对象的构造来确定行为。 当expression方法只有很小的变化(如名称)时,它的效果最好。 对于expression方法的更复杂的改变或对expression的改变的改变,这不起作用。 代码往往散布在整个。

特定版本

当变化变得更加复杂时,保持每个版本的逻辑是分开的。 即使在神话的例子中,他分隔了每个版本的代码。 但是,代码仍然以相同的方法混合在一起。 对于不同版本的代码来说很容易开始崩溃,并且可能会分散开来。 摆脱对以前版本的支持可能很困难。

另外,您需要保持旧代码与其依赖关系中的任何更改保持同步。 如果数据库发生变化,支持旧模型的代码也将需要更改。

一个更好的方法

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

它真的有两个亮点:首先处理映射的所有代码都在一个位置,所以稍后可以很容易地理解或删除;其次,在开发新的API(俄罗斯娃娃模型)时不需要维护。

问题是当新的API不如旧API的performance力。 无论解决scheme是保留旧版本,这都是需要解决的问题。 很明显,有一个问题,这个问题的解决scheme是什么。

神话中这种风格的例子是:

 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 } } } 

每个暴露的对象是清晰的。 相同的映射代码仍然需要以两种样式编写,但是在分离的样式中,只需要写入与types相关的映射。 没有必要明确地映射不适用的代码(这只是另一个潜在的错误来源)。 添加未来的API或更改API层的依赖关系时,先前API的依赖关系是静态的。 例如,如果数据源发生变化,那么只有最新的API(版本3)需要以此风格进行更改。 在组合样式中,您需要为每个支持的API编写更改。

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

我也试图解决这个问题,并正在考虑做下面的事情。 (基于大量的谷歌和StackOverflow查询,所以这是build立在许多其他人的肩膀上。)

首先,我不想争论版本是否应该在URI或Request Header中。 这两种方法都有优点/缺点,所以我认为我们每个人都需要使用最符合我们要求的方法。

这是关于如何devise/架构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; } } 

现在让我们说,我们添加2个附加属性到Foo。

 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; } 

然后我有一个基于请求版本的响应filter,只返回与该版本匹配的属性。 请注意,为了方便起见,如果所有版本都应该返回属性,那么您不需要对它进行注释,并且filter将返回它,而不pipe所请求的版本

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

主要版本的方法

如果从一个版本到另一个版本进行了很多更改,现在可能会变得非常复杂。 那是当我们需要移动到第二select。

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

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

此外,我想知道是否有很好的中介引擎/ ESB可以做这种types的转换,而不必使用filter,但没有看到任何像使用filter一样简单。 也许我没有足够的search。

有兴趣知道他人的想法,如果这个解决scheme将解决原来的问题。