Comments 29
А что мешало использовать обычный ServiceContractGenerator
раз уж у вас как клиент так и сервис WCF используют?
Хм, не знал. Но после беглого прочтения документации похоже, что этим можно было бы заменить мою реализацию генерации клиента. Однако, тут у нас возникает проблема получения метаданных. Если посмотреть пример, то нам нужно заполнить класс ContractDescription
, на основании которого он сгенерирует там клиент. В принципе, это может сработать, но тогда возникает проблема зависимости от System.ServiceModel.dll
на стороне клиента. Либо билд одного проекта будет класть сгенерированные *.cs-файлики в другой проект, который и будет "настоящей" сборкой с клиентами. Но, с этой библиотекой это невозможно, я с автором обсуждал, его позиция заключается в том, что это поведение "by design": сборка должна быть самодостаточной, и не должна модифицироваться при билде каких-либо других проектов.
Таким образом, принципиально это возможно, но могут возникнуть сложности не меньше, чем те, что мы пытались избежать, воспользовавшись функционалом "из коробки".
Хотя я все еще не понимаю чем ситуация с кодогенерацией отличается для 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-депенденси и он не будет требоваться для пользователей клиентов этого апи. То есть клиенты вообще не будут затронуты тем, что что-то генерируется, тогда как референс по-моему так выкидывать нельзя.
Разумеется, после кодогенерации должна получиться отдельная клиентская библиотека. Почему вы думаете что это невозможно без референса на 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 делать что угодно. Когда он во время своей компиляции начинает подкладывать артефакты в чужие проекты, это не очень.
А я что, предлагаю подкладывать файлы в чужие проекты?
но меня больше беспокоят пустые проекты в солюшене, которые ни в коем случае нельзя удалять.
Ну блин, что вам все-таки нужно? Вот три варианта:
- все помержили в одну сборку, проект тоже один — этот вариант вам не нравится потому что зависимость от WCF;
- две сборки, которые генерирует один проект — этот вариант вам не нравится потому что хардкод и, наверное, потому что студия такое плохо понимает;
- две сборки и два проекта — этот вариант вам не нравится потому что второй проект — "пустой".
Но больше-то вариантов нет в принципе! Я не понимаю какой из вариантов у вас сделан (в статье не написано никакой конкретики) — но явно один из приведенных выше. Почему возражение, которое вы мне приводите, не применимо к вашему коду?
По здравому размышлению, с помощью core-проекта и опции privateAssets можно добиться подобного итога с решением, которое вы предложили. И тогда ответ такой: да, так можно было сделать. Я сделал иначе, потому что это было два равноценных подхода, и этот для меня лично выглядел проще. Его плюс в том, что у нас есть больше контроля над тем, как выполняется сериализация, и он работает с любыми сервисами, а не только с WCF. То есть, вы просто учите его работать с атрибутами HttpGet/HttpPost и Route, и пожалуйста, можете генерировать клиента для ASP.Net приложения без каких-либо изменений. Ну и просто, было интересно покопаться в Roslyn. Например, для меня было удивительным открытием, что однострочный комментарий написать сложнее, чем объявить класс-наследник интерфейса и перечислить в нем методы.
В который раз у меня в голове один вопрос: REST придумали как замену SOAP. И к чему всё свелось в итоге? Недо-SOAP построен. Одна проблема — работает так себе.
Но, как сказано в статье, не все так плохо — берете сваггер и он вам генерирует что угодно, быстро и без проблем. Ну или берете такой вот генератор и у вас все из коробки. Ведь все, что есть у SOAP — это количество инструментов, которые его поддерживают. Логично, что у (сравнительно) новой технологии инструментов будет поменьше. Но это не беда технологии, инструменты появятся, как только она докажет свою жизнеспособность. Пример выше — это как раз такая тулза. Просто ставите нугет-пакет и все заводится из коробки. Что там под капотом — да какая разница? Главное, что работает, и работает хорошо.
Разве того же самого нельзя добиться простым проксированием в рантайме?
Единственная проблема это необходимость иметь доступ к реализации интерфейса при создании прокси, но, с другой стороны, реализация сама по себе является интерфейсом, поскольку именно там описано по какому урлу идти с каким хттп методом.
tpcg.io/Aw5FAU
Service это чистый клиентский интерфейс.
ServiceImpl это реализация сервиса с хттп маппингом.
Фабрика генерит проксю удовлетворяющую интерфейсу Service и занимающуюся конвертацией параметров в пригодный для реализации вид, будь это хттп или еще какой-то протокол, главное чтобы оно конфигурилось атрибутами.
Клиент зависит от Service интерфейса, когда интерфейс меняется, код перестает компилиться.
В общем-то это почти то же самое что и в статье, только чисто в рантайме, без коодгенерации.
Ну, вы написали эквивалент примера №2. Недостатки у него, соответственно, те же:
- клиент обязан реализовывать тот же интерфейс, что и сервис
- нельзя понять, что что-то не так, пока вы не запустите программу. Можно конечно в начале проверять все методы всех клиентов всех сервисов, но это не очень удобно, и увеличивает время билда/запуска с каждым новым сервисом.
1. Это да, ок. С другой стороны, мы же не знаем, какой апи хочет конкретный клиент. По идее, если клиенту надо, он докрутит там где надо, и тут уже нет принципиально разницы, докрутит ли он правила кодогенерации чтобы получить особенную версию клиента, либо же завраппит существующий клиент нужным ему образом непосредственно в коде.
2. Я не совсем понимаю, что именно может сломаться? Эта реализация исключительно рантаймовая, просто общим образом реализуется уже существующий интерфейс.
Добавляется новый метод в интерфейс — оно автоматом подхватывает новый метод и продолжает работать. Меняется апи — ошибка компиляции во всех местах где использовалось старое апи.
Единственное что, рефлексия, почему-то некоторые ее не любят. =)
- Ну у меня ломалось, например, когда у метода класса не было атрибута, в вашем случае если у интерфейса нет
@Method("GET")
. В таком случае генерация не знает, что с этим делать, и падает. В вашем случае — в рантайме. В общем, когда меняется что-нибудь, что не является частью сигнатуры метода, но тоже необходимо. Атрибут это отличный пример этого "нечта".
Как написать свой сваггер и не пожалеть об этом