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

Почему полезно предоставлять клиентскую библиотеку для вашего API?

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

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

Поговорим о коде ...

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

[ApiController]
[Route("api/users/")]
public class UserController : ControllerBase
{
    private List<User> _inMemoryUsers;
    public UserController()
    {
        _inMemoryUsers = new List<User>()
        {
            new User(1, "John", "[email protected]", true),
            new User(2, "Lisa", "[email protected]", true),
            new User(3, "Bernard", "[email protected]", false)
        };
    }
    [HttpGet("all")]
    public IEnumerable<User> GetAll()
    {
        return _inMemoryUsers;
    }
    [HttpGet("{id}")]
    public User GetById(int id)
    {
        return _inMemoryUsers.SingleOrDefault(u => u.Id == id);
    }
}

Как видите, моя «база данных» представляет собой просто список User и Group.

[Route("api/groups")]
[ApiController]
public class GroupController : ControllerBase
{
    private List<Group> _inMemoryGroups;
    public GroupController()
    {
        _inMemoryGroups = new List<Group>()
        {
            new Group(1, "Manager", new []{ 1 }),
            new Group(2, "Developers", new []{ 2, 3 })
        };
    }
    [HttpGet("all")]
    public IEnumerable<Group> GetAll()
    {
        return _inMemoryGroups;
    }
    [HttpGet("{id}")]
    public Group GetById(int id)
    {
        return _inMemoryGroups.SingleOrDefault(u => u.Id == id);
    }
}

Задача состоит в том, чтобы предоставить клиентский интерфейс, который дает вам доступ ко всем доступным конечным точкам.

Клиентская Архитектура

У нас есть главный интерфейс IApiClient, который содержит следующее определение:

public interface IApiClient
{
    IUserClient UserClient { get; }
    IGroupClient GroupClient { get; }
}

Его реализация тоже очень проста:

public class MyApiClient : IApiClient
{
    public MyApiClient(IUserClient userClient, 
                       IGroupClient groupClient)
    {
        UserClient = userClient;
        GroupClient = groupClient;
    }
    public IUserClient UserClient { get; }
    public IGroupClient GroupClient { get; }
}

С этим интерфейсом у нас будет доступ ко всем интерфейсам для взаимодействия с существующими контроллерами. В следующем блоке кода показаны интерфейсы от IUserClient, который знает, как взаимодействовать с конечной точкой /users, и IGroupClient, который знает нечто подобное, но в отношении конечной точки /groups.

public interface IUserClient
{
    Task<IEnumerable<User>> GetAll();
    Task<User> GetById(int id);
}
public interface IGroupClient
{
    Task<IEnumerable<Group>> GetAll();
    Task<Group> GetById(int id);
}

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

Что, если я добавлю новую конечную точку в свой API?

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

Итак, на данный момент у нас есть:

  • API реализован с двумя контроллерами
  • Клиентская библиотека с двумя интерфейсами, по одному для каждого контроллера.
  • Основной интерфейс, содержащий все доступные интерфейсы для взаимодействия с существующими конечными точками.

Какова цель класса MyHttpClient?

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

public class MyHttpClient
{
    private readonly HttpClient _httpClient;
    private const string BASE_URL = "http://localhost:5000/api/";
    public MyHttpClient()
    {
        _httpClient = new HttpClient();
    }
    public async Task<string> GetRequest(string url)
    {
        string endpointPath = BASE_URL + url;
        
        // Make the request
        HttpResponseMessage response = await _httpClient.
                                       GetAsync(endpointPath);
        if (!response.IsSuccessStatusCode)
            throw new ArgumentException($"The path {endpointPath}          gets the following status code: " + response.StatusCode);
        return await response.Content.ReadAsStringAsync();
    }
}

Чтобы прояснить цель этого класса, давайте посмотрим на реализацию IUserClient:

public class UserClient : MyHttpClient, IUserClient
{
    public async Task<IEnumerable<User>> GetAll()
    {
        string result = await GetRequest("users/all");
        return JsonConvert.
               DeserializeObject<IEnumerable<User>>(result);
    }
    public async Task<User> GetById(int id)
    {
        string result = await GetRequest($"users/{id}");
        return JsonConvert.DeserializeObject<User>(result);
    }
}

Как настроить реестр внедрения зависимостей?

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

public static void AddApiClient(this IServiceCollection services)
{
    services.AddSingleton<IUserClient, UserClient>();
    services.AddSingleton<IGroupClient, GroupClient>();
    services.AddSingleton<IApiClient, MyApiClient>();
}

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

Как пользоваться клиентской библиотекой?

В этом примере я буду использовать шаблон проекта WorkerService. С точки зрения того, кто будет использовать вашу клиентскую библиотеку, просто необходимо:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args).
    ConfigureServices((hostContext, services) =>
    {
        services.AddApiClient();
        services.AddHostedService<Worker>();
    });

В вашем Worker классе вы можете использовать IApiClient для получения данных от обоих существующих клиентов. Давайте посмотрим на метод ExecuteAsync.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    IEnumerable<User> users = await _apiClient.UserClient.GetAll();
    Group group = await _apiClient.GroupClient.GetById(2);
    (...)
}

Заключение

На мой взгляд, это простой и эффективный способ создания клиентов для API. Факт наличия нескольких контроллеров здесь не создает никаких проблем, вам просто нужно добавить и реализовать новый интерфейс и объявить его в основном интерфейсе библиотеки (IApiClient в этом примере).

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