Як покликати HTTP REST сервіс в .NET, якщо маєте його OpenAPI-специфікацію
Мене звати Юрій Івон, я співпрацюю з компанією EPAM як Senior Solution Architect. Цього разу хочу поділитися інформацією про достатньо просту функцію Visual Studio, про яку, на мій подив, досить часто не знають навіть досвідчені розробники.
Традиційно взаємодія між REST-сервісами в .NET базувалася на клієнтських бібліотеках, які надають усі необхідні інтерфейси, класи та методи, приховуючи під капотом деталі серіалізації, десеріалізації та комунікації. Якщо ви працюєте з сервісом стороннього розробника, який вже надає свою клієнтську бібліотеку, це може бути прийнятним рішенням. Але що робити, якщо ви розробляєте багатосервісне рішення чи взаємодієте з зовнішніми сервісами, що не надають готових бібліотек? Чи взагалі варто розробляти клієнтські бібліотеки вручну?
В часи SOAP такого питання майже не виникало. Можна було згенерувати клієнта на основі відповідної WSDL-схеми, і мало хто замислювався про написання SOAP-клієнтів вручну. Можливо, в окремих випадках згенеровані клієнти розповсюджувалися у вигляді бібліотек, але це було швидше винятком, ніж правилом. Було багато проблем, викликаних складністю SOAP і несумісністю між різними інструментами, але не було потреби витрачати час на шаблонний код. То що ж змінилося?
Головною зміною стало те, що REST спочатку не пропонував жодної автоматизації для роботи з контрактами. Спершу для цієї цілі був запропонований WADL, але він не набув популярності. Пізніше з’явився Swagger, який став популярним, проте його генератор коду довгий час не привертав уваги. Оскільки контракти REST були технічно простими, багато проєктів не переймалися автоматизацією і підтримували сервіcні контракти вручну.
Специфікація OpenAPI та Swagger сьогодні добре відомі, але здається, що багато розробників досі вважають їх лише засобами для документування та створення сторінок, де з API можна «погратися» вручну. Що стосується клієнтських бібліотек — багато команд досі пишуть їх вручну та розповсюджують серед інших сервісів через NuGet. Припиніть це робити!
На щастя, Visual Studio вже підтримує розширення, яке дозволяє генерувати клієнтські бібліотеки без написання жодного рядка коду. Подивімось, як це працює, використовуючи приклад із мого GitHub. Ви можете відкрити його в Visual Studio, запустити WeatherService без дебагу та додати ще один проєкт типу Web API. Коли новий проєкт буде готовий, натисніть «Add -> Service Reference» у його контекстному меню та виберіть «OpenAPI». Потім заповніть необхідні поля у діалоговому вікні «Add new OpenAPI service reference». Поле «URL» потрібно вказати, як показано на скріншоті нижче https://localhost:7278/swagger/v1/swagger.json, решта може бути змінена, але для зручності в статті далі використовуватимуться значення зі скріншота.
Переконайтеся, що WeatherService запущений і працює перед тим, як натискати кнопку «Finish».
Після того, як ви натиснете «Finish», Visual Studio завантажить цю JSON-специфікацію, включить її в проєкт і створить наступний розділ у файлі проєкту:
<OpenApiReference Include="OpenAPIs\swagger.json" CodeGenerator="NSwagCSharp" Namespace="AggregatorService.Clients" ClassName="WeatherServiceClient"> <SourceUri> https://localhost:7278/swagger/v1/swagger.json </SourceUri> </OpenApiReference>
Для генерації клієнтського коду на основі завантаженої специфікації, Visual Studio викличе NSwag через командний рядок. Мене порадувало, що тут використовується NSwag, адже я мав дуже позитивний досвід роботи з ним в минулому для вирішення такої ж задачі. Це був найкращий варіант серед усіх альтернатив, які я тоді розглядав, тому не здивований, що Microsoft вирішила використати саме його для генерації клієнтів.
Коли це буде зроблено, ви зможете взаємодіяти з Weather Service в коді:
var client = new WeatherServiceClient("https://localhost:7278", new HttpClient()); var forecast = await client.GetWeatherForecastAsync();
Вам може знадобитися інтерфейс для цього клієнта, щоб мати можливість створювати його моки у модульних тестах. І хоча інтерфейс за замовчуванням не генерується, існує спосіб його отримати. Для цього в наведеному раніше діалозі додавання сервісу достатньо вписати «/GenerateClientInterfaces:true» в поле «Additional code generation options». Для вже доданих сервісів цей діалог можна знову показати, обравши пункт контекстного меню «Manage Connected Services» на розділі проєкту «Connected Services» і потім — «Edit» в меню відповідного підключеного сервісу. Генерувати інтерфейси чи ні — це тільки одне з багатьох доступних налаштувань генератора, трохи пізніше в статті наведу більше інформації про інші можливості.
Після зміни параметрів генерації найнадійніший спосіб їх застосувати — видалити каталог obj і запустити компіляцію проєкту заново.
Як тільки код буде згенеровано з новими налаштуваннями, клієнт вже можна буде реєструвати в
builder.Services.AddSingleton<IWeatherServiceClient>(services => new WeatherServiceClient("https://localhost:7278", new HttpClient()));
Базовий URL, звісно, має бути параметризованим, але такого прикладу достатньо для демонстрації загальної ідеї. Тепер ви можете інжектити Weather Service всюди, куди треба:
[ApiController] [Route("api/data")] public class DataController : ControllerBase { private readonly IWeatherServiceClient _weatherClient; public DataController(IWeatherServiceClient weatherClient) { _weatherClient = weatherClient; } [HttpGet(Name = "GetData")] public async Task<AggregatedData> Get() { var forecast = await _weatherClient.GetWeatherForecastAsync(); return new AggregatedData { AverageTemperature = forecast.Average(d => d.TemperatureC) }; } }
Отже, ми переконалися, що .NET вебсервіс може викликати будь-який інший вебсервіс, маючи лише його OpenAPI-специфікацію, причому якість коду від такої автоматизації не страждає.
На цьому етапі може бути не зовсім зрозуміло, як передавати токени аутентифікації за допомогою згенерованого клієнта, але відповідь проста — обробка токенів має бути прив’язана до HttpClient, який передається в конструктор клієнта. Більше деталей про цей підхід можна знайти в наступних статтях: перша, друга, третя.
Робочий приклад .NET-клієнта на основі OpenAPI можна знайти в проєкті AggregatorService, який є частиною мого демонстраційного рішення.
Ось кілька додаткових параметрів NSwag, які можуть бути важливими для реального проєкту (на основі власного досвіду). Як і у випадку з прикладом вище, значення параметра має бути вказано через двокрапку після його імені.
- ClientBaseClass — повне ім’я базового класу. Може знадобитися, якщо ви хочете мати спільного предка для всіх клієнтів.
- GenerateOptionalParameters — визначає, чи потрібно впорядковувати параметри (обов’язкові спочатку, необов’язкові в кінці) та взагалі створювати необов’язкові параметри.
- GenerateExceptionClasses — вказує, чи потрібно створювати класи винятків (exceptions). Якщо ви хочете використовувати існуючий клас винятків, встановіть цей параметр у false і скористуйтеся наступним параметром в цьому списку для вказання імені існуючого класу.
- ExceptionClass — ім’я для генерованих класів винятків (можна використовувати плейсхолдер {controller}). Або повне ім’я існуючого класу, якщо GenerateExceptionClasses встановлений в false.
- DateTimeType — тип для значень дати та часу, який буде використовуватись у контрактах. Оскільки за замовчуванням це DateTimeOffset, вам може знадобитися зміна на DateTime.
Усі доступні параметри для генератора C# коду можна знайти за наступними посиланнями:
- CSharpGeneratorSettings
- ClientGeneratorBaseSettings
- CSharpGeneratorBaseSettings
- CSharpClientGeneratorSettings
Кожного разу, коли відбуваються зміни в API, від якого залежить ваш код, ви можете оновити визначення, відкривши сторінку «Connected Services» у Visual Studio, знайти посилання на потрібний сервіс, і потім в меню сервісу клікнути «Refresh»:
Я використовував NSwag для генерації .NET клієнтів на основі OpenAPI специфікації, коли в Visual Studio ще не було вбудованої підтримки цих можливостей, і такий підхід суттєво зменшив зусилля на розробку, пов’язані з міжсервісною комунікацією.
Також варто зазначити, що нам доводилося дотримуватися кількох простих правил, щоб гарантувати зручність згенерованих контрактів:
- Вказувати властивість Name в «Http*» атрибутах на методах контролера —
[HttpGet(Name = "GetData")]
. Без цієї властивості згенеровані імена методів будуть виглядати погано, оскільки їх буде згенеровано на основі відповідних URL-суфіксів. - Позначати кожен enum у контракті сервісу атрибутом JsonConverter з типом конвертера StringEnumConverter.
- Не повертати атомарні значення (числа, рядки, енами та інші) з публічних методів контролера. Замість цього обгортати їх у спеціальний клас, що репрезентує відповідь, навіть якщо в цьому класі буде тільки одна властивість.
Усі ці правила можна легко впровадити за допомогою спеціальних модульних тестів, які аналізують метадані контролерів і відповідні моделі даних.
Також варто зазначити, що не всі особливості OpenAPI-специфікації можуть підтримуватися цим інструментом на даний момент. З того, що мені відомо, — є проблеми з підтримкою поліморфних сутностей в контрактах, що виражаються в специфікації через oneOf (swagger.io/...ls/oneof-anyof-allof-not). Тобто не будь-який OpenAPI-контракт може буде перетворений на робочій C#-код за допомогою NSwag. Але в більшості випадків існуючих можливостей цього інструменту цілком достатньо для спрощення взаємодії з REST-сервісами.
Ця стаття доступна також англійською на Medium.
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів