В начале разработки нового проекта у команды всегда появлялся вопрос, какую библиотеку выбрать для межсервисного взаимодействия? А какую использовать для походов в сторонние сервисы? А что там с тестированием? В этой статье я постарался вкратце осветить различные обёртки над HttpClient.

Flurl

Начнём с Flurl, данная библиотека предоставляет хорошее сочетание URL конструктора и API клиента, посредствам Fluent Interface синтаксиса.

Например, отправка POST запроса с параметрами, авторизацией и телом, а также десереализация результата в конкретный тип будет выглядеть так:

var person = await "https://api.com"
    .AppendPathSegment("person")
    .SetQueryParams(new { a = 1, b = 2 })
    .WithOAuthBearerToken("my_oauth_token")
    .PostJsonAsync(new
    {
        first_name = "Claire",
        last_name = "Underwood"
    })
    .ReceiveJson<Person>();

Библиотека особенно удобна при тестировании. Например, можно удобно разобрать URL на составляющие:

var url = new Url("https://user:pass@www.mysite.com:1234/with/path?x=1&y=2#foo");
Assert.AreEqual("https", url.Scheme);
Assert.AreEqual("user:pass", url.UserInfo);
Assert.AreEqual("www.mysite.com", url.Host);
Assert.AreEqual(1234, url.Port);
Assert.AreEqual("user:pass@www.mysite.com:1234", url.Authority);
Assert.AreEqual("https://user:pass@www.mysite.com:1234", url.Root);
Assert.AreEqual("/with/path", url.Path);
Assert.AreEqual("x=1&y=2", url.Query);
Assert.AreEqual("foo", url.Fragment);

Или сделать тест в привычным AAA стиле:

using (var httpTest = new HttpTest()) {
    // arrange
    httpTest.RespondWith("OK", 200);
    // act
    await sut.CreatePersonAsync();
    // assert
    httpTest.ShouldHaveCalled("https://api.com/*")
        .WithVerb(HttpMethod.Post)
        .WithContentType("application/json");
}

Можно подстроить тест под нужные условия с помощью различных фильтров:

using var httpTest = new HttpTest();

httpTest
    .ForCallsTo("*.api.com*", "*.test-api.com*") // multiple allowed, wildcard supported
    .WithVerb("put", "PATCH") // or HttpMethod.Put, HttpMethod.Patch
    .WithQueryParam("x", "a*") // value optional, wildcard supported
    .WithQueryParams(new { y = 2, z = 3 })
    .WithAnyQueryParam("a", "b", "c")
    .WithoutQueryParam("d")
    .WithHeader("h1", "f*o") // value optional, wildcard supported
    .WithoutHeader("h2")
    .WithRequestBody("*something*") // wildcard supported
    .WithRequestJson(new { a = "*", b = "hi" }) // wildcard supported in sting values
    .With(call => true) // check anything on the FlurlCall
    .Without(call => false) // check anything on the FlurlCall
    .RespondWith("all conditions met!", 200);

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

Всего настроек не так много, но в наличие все основные: Timeout, AllowedHttpStatusRange, JsonSerializer, UrlEncodedSerializer, Redirects, BeforeCall, AfterCall, OnError, OnRedirect.

 

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

 

NSwagStudio

Думаю, много кто сталкивался с NSwagStudio. На этот инструмент написано достаточно много материала, поэтому буду краток.

С помощью NSwagStudio можно:

Всё перечисленное делает из NSwagStudio действительно удобный инструмент для создания API клиентов, но есть и большой минус, он генерирует только то, что умеет. Например, если у вас в проекте используется хитрая система обработки ошибок, то скорей всего этот инструмент вам не подойдёт.

RestSharp

RestSharp позиционирует себя как легковесная REST API Client библиотека поддерживаемая AWS.

Типичный сервис с использованием RestSharp будет выглядеть так:

public class TwitterClient : ITwitterClient, IDisposable {
    readonly RestClient _client;

    public TwitterClient(string apiKey, string apiKeySecret) {
        var options = new RestClientOptions("https://api.twitter.com/2");

        _client = new RestClient(options) {
            Authenticator = new TwitterAuthenticator("https://api.twitter.com", apiKey, apiKeySecret)
        };
    }

    public async Task<TwitterUser> GetUser(string user) {
        var response = await _client.GetJsonAsync<TwitterSingleObject<TwitterUser>>(
            "users/by/username/{user}",
            new { user }
        );
        return response!.Data;
    }

    record TwitterSingleObject<T>(T Data);

    public void Dispose() {
        _client?.Dispose();
    }
}

 Запрос достаточно просто обогатить параметрами, телом, Cookie, заголовками и т.д.

var request = new RestRequest("beavers");
request.AddParameter("status", 1);
request.AddHeader("name", "value");

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

RestClient client = new("https://localhost:44376/");
client.AddDefaultHeader("MyHeader", "default");

Вызов метода можно осуществить двумя способами:

Например, вызвав ExecuteGetAsync или GetAsync. В первом случае мы получим промежуточный объект RestResponse, из которого можно вытащить дополнительную информацию, полученную в ответе (например заголовки). Кроме того, вызовы с префиксом Execute не генерируют исключения, его вам придётся считать из соответствующего поля.

 

RestSharp отличный инструмент для создания REST API клиента, который позволяет детально настроить запрос и получить подробный ответ. Из минусов я могу отметить отсутствие системы обработки ошибок из коробки, вам придётся написать её самому.

 

Refit

Refit – REST библиотека которая упрощает создания Api клиента до минимума, при этом предоставляя обширный функционал кастомизации.

Создание клиента происходит по средствам описания интерфейса:

public interface IGitHubApi
{
    [Get("/users/{user}")]
    Task<User> GetUser(string user);
}

Получить API клиент можно через метод RestService.For<T>:

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");

Или зарегистрировать его в IoC, через Refit.HttpClientFactory:

services
    .AddRefitClient<IGitHubApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));

 Функционал библиотеки позволяет детально настроить поведение.

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new SnakeCasePropertyNamesContractResolver()
            }
        )});

var otherApi = RestService.For<IOtherApi>("https://api.example.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
            }
        )});

 

В библиотеке реализован механизм обработки исключений. По умолчанию доступны классы исключений ValidationApiException и ApiException. Создать свои объекты для исключений и поменять поведение можно через параметр ExceptionFactory.

 

Стоит упомянуть полезный инструмент Refitter, который активно развивается и обновляется. Refitter — это небольшая утилита для генерации Refit клиента из OpenApi спецификации. Это может быть очень полезно, если вам захотелось мигрировать на Refit, но на создание клиентов не хватает ресурсов.

Ещё одна библиотека под названием Generate AspNetCore Client позволяет генерировать Refit клиенты из готового приложения без необходимости OpenApi спецификации. Достигается это путём запуска приложения и вытягивания информации о контроллерах. Библиотека подойдёт не всем, так как сделана довольно криво, особенно если ваш проект не так просто запустить без танцев с бубном.

 

RestEase

На первый взгляд RestEase мало чем отличается от Refit, и, более того, почти все API клиенты, которые вы напишете на Refit, запустятся и на RestEase. Но по мимо схожего синтаксиса, библиотека предоставляет свои фичи.

Например, мне понравилась возможность передать с помощью атрибута [QueryMap] словарь параметров, это удобный способ передать данные, которые заранее не известны или формируются динамически.

Другая не менее интересная фича это Query Properties. Всё просто, необходимо добавить свойство и отметить его атрибутом [Query].

public interface ISomeApi
{
    [Query("foo")]
    string Foo { get; set; }

    [Get("thing")]
    Task ThingAsync([Query] string foo);
}

var api = RestClient.For<ISomeApi>("https://api.example.com");
api.Foo = "bar";

// Requests https://api.example.com?foo=baz&foo=bar
await api.ThingAsync("baz");

 

Ещё одним преимуществом данной библиотеки является Source Generator, который позволит обнаружить ошибки на этапе компиляции и немного ускорить выполнение.

Сгенерировать клиент из OpenApi спецификации можно с помощью расширения для Visual Studio - RestEase Client Generator. Библиотека не обновлялась с конца 2019 года

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

  

Kiota

Kiota – это набор из библиотек и утилиты командной строки для генерации Api клиентов на основе OpenApi спецификации. Библиотека активно развивается под крылом Microsoft.

После небольшой команды

kiota generate -l CSharp -c PostsClient -n KiotaPosts.Client -d ./posts-api.yml -o ./Client

Получаем готовый набор API клиента.

Вызов методы выглядит примерно так:

var authProvider = new AnonymousAuthenticationProvider();
var adapter = new HttpClientRequestAdapter(authProvider);
adapter.BaseUrl = "https://localhost:44376";

var client = new BeaversServer_Kiota(adapter);

var bernarId = await client.Beavers.PostAsync(
    new RefClient.ApiClients.Kiota.Models.CreateBeaver
    {
        Eat = 10,
        Name = "Bernar"
    });

 

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

Kiota выглядит перспективной библиотекой, тем более поддерживает не только C#, но и Go, Java, PHP, Python, Ruby, Swift, TypeScript/JavaScript, но на текущий момент предоставляет довольно мало возможностей.

 

REST API Client Code Generator

Стоить упомянуть удобный плагин для Visual Studio, который позволяет генерировать различные клиенты через графический интерфейс.

Сводная таблица

Flurl

NSwag

RestSharp

Refit

RestEase

Kiota

HttpClient

+

+

v107+

+

+

+

Реализация Api клиента

Явная

Явная

Явная

Неявная

Неявная

Явная

Генерация из OpenApi спецификации

-

+

+

+

+

+

⭐⭐⭐

3.6k

5.9k

9.1k

7.3k

~1k

~800

Заключение

В заключение хочется сказать, что представленные инструменты отлично справляются с поставленной задачей. У меня не получилось выявить явного лидера, поэтому выбирать инструмент следует из потребностей проекта и личного предпочтения. Если вам нужен максимальный контроль над реализацией, то следует обратить внимание на Flurl, NSwag, RestSharp, если вы придерживаетесь минимализма и не хотите перегружать свой код деталями реализации вам больше подойдёт Refit или RestEase.

На этом у меня всё, возможно вы знаете ещё парочку интересных библиотек, которые я не упомянул, с радостью прочитаю о них в комментариях

Комментарии (7)


  1. mikegordan
    09.09.2023 05:55
    +1

    Самый удобный выглядят Refit\RestEase , строготипизированный , вызов через интерфейс. Думаю удобно делать вызовы между микросервисами если шарнуть библиотеку с интерфейсом между ними.

    Вот только атрибуты лишне выглядит, неужели нельзя без атрибутов чтобы /url шел по названию метода? чтобы интерфейс оставался абсолютно чистым

    var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");

    var octocat = await gitHubApi.Post.UpdateUser(new { id = 1, name = "octocat" });

    // url https://api.github.com/UpdateUser

    var octocat = await gitHubApi.Get.GetUser(new { id = 1 });

    // url https://api.github.com/GetUser?id=1

    Да и вообще страно что никто не реализовал библиотеку через экспрешены чтобы параметры тоже были строготипизированы

    var octocat = await gitHubApi.Post(call => call.UpdateUser(1, "octocat"));


    1. AgentFire
      09.09.2023 05:55

      Можно пойти еще дальше, и избавиться от xxxApi-класса.

      Будет просто await IOtherService.UpdateUser(...., cancellationToken);

      Но вместо ручной имплементации IOtherService, маппинг метода на URL, само обращение, ретраи, логирование, метрики, получение зависимостей, и т.д. — всё это можно сделать кодогенерацией и/или AOP

      Правда, теперь это напоминает WCF, а ему уже сколько лет-то. Может, есть что готовенькое и поновее?


  1. Vasjen
    09.09.2023 05:55
    +2

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


  1. iamkisly
    09.09.2023 05:55
    +1

    Если бы вы были подписаны на подкаст radiodotnet, то статья бы родилась на много месяцев раньше. Там как раз было обсуждение статьи andrewlock на тему генерации httpClient и refit в том числе.


    1. vgreat
      09.09.2023 05:55
      +1

      А можно ссылочку на статью andrewlock про генерацию httpClient и refit?


  1. PuerteMuerte
    09.09.2023 05:55

    Честно говоря, так и не понял, какое вообще преимущество дают все эти библиотеки? Экономию кода или хотя бы более чистый синтаксис? Нет, по идее, не дают (генератор из openApi не в счёт, ибо это можно делать, не добавляя лишнего кода собственно в приложение). Как по мне, это просто ещё обёртка для тех, кто любит юзать обёртки. Сравните чистый httpClient:

    var client = httpClientFactory.CreateClient("SomeClientName");
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", SomeUserToken);
    
    HttpResponseMessage response = await client.GetAsync(SomeEndpointAddress);
    //получить данные
    var SomeData = await response.Content.ReadFromJsonAsync<SomeDataType>();
    //или потом отправить в обратную сторону:
    response = await client.PostAsJsonAsync(SomeOtherEndpointAddress, SomeData);

    Разве это нуждается в дополнительных упрощаторах/улучшаторах?


    1. 1kvin Автор
      09.09.2023 05:55

      Спасибо за комментарий, например Refit и RestEase позволяют достаточно удобно шарить контактом для межсервисного взаимодействия. Достаточно удобно держать вместе с микросервисом библиотеку с Api клиентом, для общения с ним, на моей практике это Refit интерфейс и модельки. Шарить HttpClient не так удобно, получается слишком сильная привязка к реализации. Например в кейсах, когда ваш API поддерживает несколько вариантов авторизации или допустим нужно добавить какой то кастомный хедер, то тут на мой взгляд однозначный лидер Refit, проще отредактировать один конфиг Refit, чем переписывать все реализации с HttpClient.