ServiceStack: RESTful Resource Versioning

Я прочитал статью Преимущества веб-сервисов, основанных на сообщениях, и мне интересно, существует ли рекомендованный стиль/практика для управления версиями. Исправленные ресурсы в ServiceStack? Различные версии могут отображать разные ответы или иметь разные входные параметры в запросе DTO.

Я склоняюсь к типу управления версиями URL (т.е./v1/movies/{Id}), но я видел другие методы, которые устанавливают версию в заголовках HTTP (например, Content-Type: application/vnd.company. MyApp-v2).

Я надеюсь, что это будет работать со страницей метаданных, но не столько требованием, сколько я заметил, просто используя структуру папок /namespacing отлично работает при рендеринге маршрутов.

Например (это не отображается правильно на странице метаданных, но выполняется правильно, если вы знаете прямой маршрут /URL )

  • /v1/кино/{идентификатор}
  • /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)
        {
        ...
        }
    }

Ответ 1

Попробуйте разработать (не реинвестировать) существующие службы

Для управления версиями вы будете в ужасе, если попытаетесь поддерживать разные статические типы для разных конечных точек версии. Сначала мы начали этот маршрут, но как только вы начнете поддерживать свою первую версию, усилия разработчиков по поддержанию нескольких версий одной и той же службы взрываются, так как вам потребуется либо ручное сопоставление различных типов, которые легко просачиваются из необходимости поддерживать несколько параллельные реализации, каждый из которых связан с другим типом версий - массовое нарушение DRY. Это не проблема для динамических языков, где одни и те же модели могут быть легко использованы разными версиями.

Воспользуйтесь встроенным управлением версиями в сериализаторах

Моя рекомендация заключается не в явной версии, а в том, чтобы использовать возможности управления версиями в форматах сериализации.

Например: вам вообще не нужно беспокоиться об управлении версиями с клиентами JSON, поскольку возможности управления версиями JSON и JSV Serializers намного более устойчивы.

Повысьте эффективность существующих служб

С помощью XML и DataContract вы можете свободно добавлять и удалять поля без внесения изменений. Если вы добавите IExtensibleDataObject в свой ответ DTO, вы также сможете получить доступ к данным, которые не определены в DTO. Мой подход к управлению версиями заключается в том, чтобы запрограммировать защиту, чтобы не вносить изменения в разрыв, вы можете проверить это в случае тестов интеграции с использованием старых DTO. Ниже приведены некоторые советы:

  • Никогда не изменяйте тип существующего свойства. Если вам нужно, чтобы он был другим типом, добавьте другое свойство и используйте старый/существующий, чтобы определить версию
  • Программа защищает то, какие свойства не существуют со старыми клиентами, поэтому не делайте их обязательными.
  • Хранить одно глобальное пространство имен (только для конечных точек XML/SOAP)

Я делаю это, используя атрибут [assembly] в AssemblyInfo.cs каждого из ваших проектов DTO:

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

Встраивание версий в DTOs

В большинстве случаев, если вы запрограммируете защиту и развиваете свои услуги изящно, вам не нужно точно знать, какую версию использует конкретный клиент, как вы можете сделать вывод из данных, которые заполнены. Но в редких случаях ваши службы должны настраивать поведение на основе конкретной версии клиента, вы можете встраивать информацию о версии в свои DTO.

С первым выпуском ваших DTO, которые вы публикуете, вы можете с радостью создать их, не думая о версии.

class Foo {
  string 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 Release:

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

Вы можете продолжать обрабатывать эти разные версии в одной и той же реализации (которая будет использовать последнюю версию VTO версии 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)
    }
}

Ответ 2

Обрамление проблемы

API - это часть вашей системы, которая предоставляет свое выражение. Он определяет понятия и семантику общения в вашем домене. Проблема возникает, когда вы хотите изменить то, что может быть выражено, или как оно может быть выражено.

В методе выражения и в выражении могут быть различия. Первая проблема имеет тенденцию к различиям в токенах (имя и фамилия вместо имени). Вторая проблема заключается в выражении разных вещей (умение переименовывать себя).

Долгосрочное решение для управления версиями должно решить обе эти проблемы.

Разработка API

Развитие службы путем изменения типов ресурсов - это тип неявного управления версиями. Он использует конструкцию объекта для определения поведения. Его работы лучше всего, когда есть только незначительные изменения в методе выражения (например, имена). Это не работает для более сложных изменений метода выражения или изменений в изменении выразительности. Код имеет тенденцию рассеиваться повсюду.

Конкретное исполнение версий

Когда изменения становятся более сложными, важно сохранить логику для каждой версии отдельно. Даже в примере мифа, он разделил код для каждой версии. Тем не менее, код все еще смешивается в одних и тех же методах. Кодирование кода для разных версий очень просто начинать рушиться друг с другом, и, скорее всего, оно будет распространено. Избавиться от поддержки предыдущей версии может быть сложно.

Кроме того, вам нужно будет синхронизировать свой старый код с любыми изменениями в его зависимостях. Если база данных изменится, код, поддерживающий старую модель, также изменится.

Лучший способ

Лучший способ найти решение проблемы непосредственно. Каждый раз, когда выпущена новая версия API, она будет реализована поверх нового слоя. Это, как правило, легко, потому что небольшие изменения.

Он действительно сияет двумя способами: сначала весь код для обработки отображения находится в одном месте, поэтому его легко понять или удалить позже, а во-вторых, он не требует обслуживания по мере разработки новых API (модель русской куклы).

Проблема заключается в том, что новый API менее выразителен, чем старый API. Это проблема, которая должна быть решена независимо от того, что решение для сохранения старой версии. Просто становится ясно, что есть проблема и каково решение этой проблемы.

Пример из примера мифов в этом стиле:

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.

Одной из проблем в комментариях было добавление типов в базу кода. Это не проблема, потому что эти типы отображаются снаружи. Предоставление типов явно в базе кода упрощает их обнаружение и изоляцию при тестировании. Для удобства обслуживания намного лучше. Еще одно преимущество заключается в том, что этот метод не создает дополнительной логики, но только добавляет дополнительные типы.

Ответ 3

Я также пытаюсь прийти к решению для этого и думал о том, чтобы сделать что-то вроде ниже. (Исходя из большого количества запросов Googlling и StackOverflow, поэтому это построено на плечах многих других.)

Прежде всего, я не хочу обсуждать, должна ли версия быть в URI или заголовке запроса. Есть плюсы и минусы для обоих подходов, поэтому я думаю, что каждый из нас должен использовать то, что наилучшим образом соответствует нашим требованиям.

Это о том, как создавать/создавать объекты Java Message Object и классы реализации ресурсов.

Итак, давайте доберемся до него.

Я бы подошел к этому в два шага. Незначительные изменения (например, от 1.0 до 1.1) и основные изменения (например, от 1.1 до 2.0)

Подход к незначительным изменениям

Итак, скажем, мы идем теми же примерами классов, которые используются @mythz

Первоначально мы имеем

class Foo {   string Name; }

Мы предоставляем доступ к этому ресурсу как /V 1.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; 
}

Затем у меня есть фильтр ответов, который будет основываться на запрошенной версии, вернуть пользователю только свойства, соответствующие этой версии. Обратите внимание, что для удобства, если есть свойства, которые должны быть возвращены для всех версий, вы просто не комментируете его, и фильтр вернет его независимо от запрошенной версии

Это похоже на уровень посредничества. То, что я объяснил, является упрощенной версией, и это может стать очень сложным, но надеюсь, что вы получите эту идею.

Подход к основной версии

Теперь это может усложниться, когда из одной версии в другую было сделано много изменений. То есть, когда нам нужно перейти на второй вариант.

Вариант 2 состоит в том, чтобы отделить кодовую базу, а затем внести изменения в эту базу кода и разместить обе версии в разных контекстах. На этом этапе нам, возможно, придется немного обработать базу кода, чтобы удалить сложность посредничества версии, введенную в Approach one (т.е. Сделать очиститель кода). Это может быть главным образом в фильтрах.

Обратите внимание, что это просто хочу, чтобы я думал и не реализовал его до сих пор, и задаюсь вопросом, является ли это хорошей идеей.

Также мне было интересно, есть ли хорошие механизмы посредничества /ESB, которые могли бы сделать этот тип преобразования без необходимости использовать фильтры, но не видели каких-либо, которые так же просты, как использование фильтра. Может, я так и не нашел.

Заинтересованы в том, чтобы знать мысли других, и если это решение будет решать исходный вопрос.