Pull to refresh

Comments 29

Потому что у нас использовался REST-api. Описание REST-api возможно только в WADL или WSDL 2.0, но WCF их не поддерживает.
Так ведь для генерации вроде же WSDL не нужен, достаточно ServiceDescription? WSDL для WCF — лишь формат передачи ServiceDescription, но у нас-то все классы рядом…

Хм, не знал. Но после беглого прочтения документации похоже, что этим можно было бы заменить мою реализацию генерации клиента. Однако, тут у нас возникает проблема получения метаданных. Если посмотреть пример, то нам нужно заполнить класс ContractDescription, на основании которого он сгенерирует там клиент. В принципе, это может сработать, но тогда возникает проблема зависимости от System.ServiceModel.dll на стороне клиента. Либо билд одного проекта будет класть сгенерированные *.cs-файлики в другой проект, который и будет "настоящей" сборкой с клиентами. Но, с этой библиотекой это невозможно, я с автором обсуждал, его позиция заключается в том, что это поведение "by design": сборка должна быть самодостаточной, и не должна модифицироваться при билде каких-либо других проектов.


Таким образом, принципиально это возможно, но могут возникнуть сложности не меньше, чем те, что мы пытались избежать, воспользовавшись функционалом "из коробки".

Хм, а вы на клиенте все-таки не WCF используете? Ну тогда понятно почему пришлось так много генерировать…

Хотя я все еще не понимаю чем ситуация с кодогенерацией отличается для ServiceContractGenerator и для того что получилось у вас. И в том и в другом случае генерируются какие-то .cs-файлы в другом проекте.

Ну, генерировать немного, результат выглядит примерно так:


namespace Clients
{
    using System;
    using System.Collections.Immutable;
    using System.Threading.Tasks;

    public sealed class MyCoolRestServiceClient : DisposableBase, IDisposable
    {
        private readonly IRemoteRequestProcessor processor;
        public MyCoolRestServiceClient(IRemoteRequestProcessor processor)
        {
            this.processor = processor ?? throw new ArgumentNullException("processor");
        }

        protected override void Dispose(bool disposing)
        {
            processor.Dispose();
        }

        public Task<string> GetMessage(string hello, string world)
        {
            if (IsDisposed)
                throw new ObjectDisposedException(this.GetType().FullName);
            var queryStringParamters = ImmutableDictionary.CreateBuilder<string, object>();
            var bodyParamters = ImmutableDictionary.CreateBuilder<string, object>();
            queryStringParamters.Add("hello", hello);
            bodyParamters.Add("world", world);
            var descriptor = new RemoteOperationDescriptor("GET", "/{hello}", OperationWebMessageFormat.Xml, OperationWebMessageFormat.Xml);
            var request = new RemoteRequest(descriptor, queryStringParamters.ToImmutable(), bodyParamters.ToImmutable());
            return processor.GetResultAsync<string>(request);
        }
    }
}

Собственно тут уже всё есть. Нужен только HttpClient или любой другой хэндлер, хоть HttpWebRequest, хоть что. Никакого WCF со стороны клиента.


Что касается кодогенерации: этот пакет можно установить как develop-депенденси и он не будет требоваться для пользователей клиентов этого апи. То есть клиенты вообще не будут затронуты тем, что что-то генерируется, тогда как референс по-моему так выкидывать нельзя.

Так, а какое отношение ServiceContractGenerator имеет к референсам проекта?

Разумеется, после кодогенерации должна получиться отдельная клиентская библиотека. Почему вы думаете что это невозможно без референса на WCF?

Если генерируется отдельная библиотека, то это нехорошо тем, что у нас на выходе Service.dll будет что-то вроде Service.Client.dll, мы либо хардкодим имя результирующей сборки, либо должны как-то в атрибутах говорить "ты компилируйся вон туда, а ты — вон туда". У нас также должны будут быть вот эти пустые сборки, куда будут копироваться файлы, и на которые нужно будет повесить красную табличку "НЕ УДАЛЯТЬ. НУЖНО!".


В противовес этому сборки, в которых интерфейс и клиент под них лежат вместе. Может, это не всегда нужно, и мы распространяем клиенты в обязательном порядке, но мне кажется это меньшим злом. Сломать тут очень трудно что-то. Есть атрибут — генерируем, нет — нет. В случае выше, например, тут и вопрос сборок (удалили/неудалили), неймингов, неправильно прописанного пути (опечатались, и вместо AbcbdSas.dll написали AbcbbSas.dll и всё) ...


В итоге, это возможный способ, но как мне кажется, менее удачный.


Ну и как бонус, этот код очень легко заставить работать, например, с ASP.Net, достаточно научить вместо WebInvoke использовать атрибут Route.

Я вас не понимаю. Только что вы говорили что у клиентской сборки не должно быть зависимости от ServiceModel — а тут вдруг говорите что интерфейс должен быть в той же самой сборке… Или что вы понимаете под интерфейсом?


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


Ничуть не сложнее сделать нормальную отдельную клиентскую сборку: делается отдельный проект, и в нем переопределяется цель BeforeCompile, где и вызывается генератор. И тут же удаляется референс на серверную сборку если он был (есть вариант и без него обойтись, но тогда поломаются "смешанные" конфигурации в решении). Ну да, есть возможность что клиентский проект окажется "пустым" (т.е. не будет содержать никаких файлов кроме автогенерированных) — но кто его будет удалять если он прописан в референсах у кучи других проектов?


Что же до хардкода и опечаток — тут я совсем не понимаю в чем же, собственно, проблема? Почему <AssemblyName>Service</AssemblyName> в файле проекта вас устраивает, а <AssemblyName>Service.Client</AssemblyName> — уже хардкод и нехорошо?

Я вас не понимаю. Только что вы говорили что у клиентской сборки не должно быть зависимости от ServiceModel — а тут вдруг говорите что интерфейс должен быть в той же самой сборке… Или что вы понимаете под интерфейсом?

Мне желательно, чтобы вся генерация проекте Х ограничивалась проектом Х, и обеспечить при этом отсутствие зависимостей на внешние сборки-генераторы. У нугета для этого есть удобная опция developmentDependency.


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

Ничуть не сложнее сделать нормальную отдельную клиентскую сборку: делается отдельный проект, и в нем переопределяется цель BeforeCompile, где и вызывается генератор. И тут же удаляется референс на серверную сборку если он был (есть вариант и без него обойтись, но тогда поломаются "смешанные" конфигурации в решении). Ну да, есть возможность что клиентский проект окажется "пустым" (т.е. не будет содержать никаких файлов кроме автогенерированных) — но кто его будет удалять если он прописан в референсах у кучи других проектов?

Можно. Но мне кажется, это сложнее.


Что же до хардкода и опечаток — тут я совсем не понимаю в чем же, собственно, проблема? Почему Service в файле проекта вас устраивает, а Service.Client — уже хардкод и нехорошо?

Ну мне в основном в таком случае не нравится именно то, что тут нужно лазить в чужие проекты. Когда проект себя компилирует, можешь хоть before, хоть after делать что угодно. Когда он во время своей компиляции начинает подкладывать артефакты в чужие проекты, это не очень.


Под хардкодом я предположил, что для того, чтобы положить классы в разные сборки мы будем писать имя сборки в атрибуте, типа [RemoteClient("InternalApi.dll")] и [RemoteClient("PublicApi.dll")]. В моем случае мы пишем 2 сборки — InternalApi и PublicApi и там пишем необходимые интерфейсы. В вашем случае генерируются три, в случае, если мы пишем имя итоговой сборки в атрибуте, либо четыре: InternalApi, PublicApi, InternalApi.Clients, PublicApi.Clients.


То есть если с точки зрения реализации смотреть, то может это удобнее. С точки зрения использования это приводит к удвоению количества сборок. Да, мы можем ILMerge'ом помержить в одну сборку если захотим, но меня больше беспокоят пустые проекты в солюшене, которые ни в коем случае нельзя удалять.

Мне желательно, чтобы вся генерация проекте Х ограничивалась проектом Х, и обеспечить при этом отсутствие зависимостей на внешние сборки-генераторы.

Я понял что вам нужно, но я не понимаю с чем вы спорите.


Ну мне в основном в таком случае не нравится именно то, что тут нужно лазить в чужие проекты. Когда проект себя компилирует, можешь хоть before, хоть after делать что угодно. Когда он во время своей компиляции начинает подкладывать артефакты в чужие проекты, это не очень.

А я что, предлагаю подкладывать файлы в чужие проекты?


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

Ну блин, что вам все-таки нужно? Вот три варианта:


  1. все помержили в одну сборку, проект тоже один — этот вариант вам не нравится потому что зависимость от WCF;
  2. две сборки, которые генерирует один проект — этот вариант вам не нравится потому что хардкод и, наверное, потому что студия такое плохо понимает;
  3. две сборки и два проекта — этот вариант вам не нравится потому что второй проект — "пустой".

Но больше-то вариантов нет в принципе! Я не понимаю какой из вариантов у вас сделан (в статье не написано никакой конкретики) — но явно один из приведенных выше. Почему возражение, которое вы мне приводите, не применимо к вашему коду?

У меня вариант №1, за исключением того, что в моем случае её можно пометить как `developmentDependency`. Таким образом, зависимость после билда удаляется, и потребители про неё никогда не узнают. В данном случае, конечно, это мы сами, но этот механизм применим и для генерации общего назначения для внешних клиентов.
Все еще не понимаю что именно вы сделали.
Извиняюсь, что сразу не ответил, почему-то уведомление не пришло.

По здравому размышлению, с помощью core-проекта и опции privateAssets можно добиться подобного итога с решением, которое вы предложили. И тогда ответ такой: да, так можно было сделать. Я сделал иначе, потому что это было два равноценных подхода, и этот для меня лично выглядел проще. Его плюс в том, что у нас есть больше контроля над тем, как выполняется сериализация, и он работает с любыми сервисами, а не только с WCF. То есть, вы просто учите его работать с атрибутами HttpGet/HttpPost и Route, и пожалуйста, можете генерировать клиента для ASP.Net приложения без каких-либо изменений. Ну и просто, было интересно покопаться в Roslyn. Например, для меня было удивительным открытием, что однострочный комментарий написать сложнее, чем объявить класс-наследник интерфейса и перечислить в нем методы.

В который раз у меня в голове один вопрос: REST придумали как замену SOAP. И к чему всё свелось в итоге? Недо-SOAP построен. Одна проблема — работает так себе.

REST работает отлично, проблема в том, что инструмент (в данном случае WCF) не поддерживает стандарт, который позволяет обмениваться метаданными не только для SOAP. Думаю, если бы WSDL 1.0 поддерживал REST и не поддерживал SOAP это утверждение спокойно можно было развернуть обратно.

Но, как сказано в статье, не все так плохо — берете сваггер и он вам генерирует что угодно, быстро и без проблем. Ну или берете такой вот генератор и у вас все из коробки. Ведь все, что есть у SOAP — это количество инструментов, которые его поддерживают. Логично, что у (сравнительно) новой технологии инструментов будет поменьше. Но это не беда технологии, инструменты появятся, как только она докажет свою жизнеспособность. Пример выше — это как раз такая тулза. Просто ставите нугет-пакет и все заводится из коробки. Что там под капотом — да какая разница? Главное, что работает, и работает хорошо.
По-моему проще просто прогонять тесты от предыдущей версии. Если упали — значит сломана обратная совместимость
Тесты не пишутся на все. Статический анализатор всегда лучше, потому что он проверяет сразу все возможные комбинации, а не только те, на которые тесты написаны.
не понимаю почему вы свой исходный код так небрежно форматируете. Это некрасиво.
А что конкретно вас смущает?
Сорри, но выглядит как какой-то адский оверкил.
Разве того же самого нельзя добиться простым проксированием в рантайме?
При проксировании в рантайме нельзя добиться ошибки компиляции, если сервис обновился.
Почему нельзя? Есть интерфейс сервиса, все что нужно это создать прокси инстанс этого интерфейса который будет делать то что сейчас делает сгенерированный код — смотреть на реализацию и по атрибутам прочухивать какие параметры как сериализовывать. Меняется интерфейс сервиса — ломаются места использования прокси.
Единственная проблема это необходимость иметь доступ к реализации интерфейса при создании прокси, но, с другой стороны, реализация сама по себе является интерфейсом, поскольку именно там описано по какому урлу идти с каким хттп методом.
Создать инстанс во время компиляции нельзя. Значит, ошибка будет в лучшем случае при первой попытке использовать сервис. Это неплохо, но недостаточно хорошо.
Сории за жаву, последний раз на сишарпе очень давно писал, вот пример:

tpcg.io/Aw5FAU

Service это чистый клиентский интерфейс.
ServiceImpl это реализация сервиса с хттп маппингом.
Фабрика генерит проксю удовлетворяющую интерфейсу Service и занимающуюся конвертацией параметров в пригодный для реализации вид, будь это хттп или еще какой-то протокол, главное чтобы оно конфигурилось атрибутами.

Клиент зависит от Service интерфейса, когда интерфейс меняется, код перестает компилиться.

В общем-то это почти то же самое что и в статье, только чисто в рантайме, без коодгенерации.

Ну, вы написали эквивалент примера №2. Недостатки у него, соответственно, те же:


  1. клиент обязан реализовывать тот же интерфейс, что и сервис
  2. нельзя понять, что что-то не так, пока вы не запустите программу. Можно конечно в начале проверять все методы всех клиентов всех сервисов, но это не очень удобно, и увеличивает время билда/запуска с каждым новым сервисом.
Ну не совсем, во втором примере сложная кодегенерация и страдания при дебаге(ну, вдруг), а у меня обычный код работающй с хттп.

1. Это да, ок. С другой стороны, мы же не знаем, какой апи хочет конкретный клиент. По идее, если клиенту надо, он докрутит там где надо, и тут уже нет принципиально разницы, докрутит ли он правила кодогенерации чтобы получить особенную версию клиента, либо же завраппит существующий клиент нужным ему образом непосредственно в коде.
2. Я не совсем понимаю, что именно может сломаться? Эта реализация исключительно рантаймовая, просто общим образом реализуется уже существующий интерфейс.
Добавляется новый метод в интерфейс — оно автоматом подхватывает новый метод и продолжает работать. Меняется апи — ошибка компиляции во всех местах где использовалось старое апи.

Единственное что, рефлексия, почему-то некоторые ее не любят. =)
  1. Ну у меня ломалось, например, когда у метода класса не было атрибута, в вашем случае если у интерфейса нет @Method("GET"). В таком случае генерация не знает, что с этим делать, и падает. В вашем случае — в рантайме. В общем, когда меняется что-нибудь, что не является частью сигнатуры метода, но тоже необходимо. Атрибут это отличный пример этого "нечта".
Sign up to leave a comment.

Articles