Refit для C# (.NET): Type-safe REST клиент
1. Что это и зачем нужна
Refit — это библиотека с открытым исходным кодом для платформы .NET, которая автоматически генерирует реализации HTTP-клиентов на основе интерфейсов, описанных разработчиком. Она позволяет превратить обычный C# интерфейс в полноценный REST-клиент, избавляя от необходимости вручную писать шаблонный код для выполнения HTTP-запросов, сериализации/десериализации JSON и обработки ошибок.
Основная проблема, которую решает Refit — это борьба с дублированием кода при работе с REST API. Вместо того чтобы для каждого эндпоинта создавать отдельный метод с HttpClient, настраивать заголовки, обрабатывать ответы и исключения, разработчик просто объявляет интерфейс с методами, помеченными атрибутами Refit. Библиотека сама генерирует реализацию, используя HttpClientFactory (рекомендуемый подход) или напрямую HttpClient.
Refit особенно полезен при разработке микросервисных архитектур, интеграции со сторонними API (GitHub, Stripe, Telegram и т.д.) и в любых проектах, где требуется типобезопасное взаимодействие с HTTP-сервисами. Он поддерживает все основные HTTP-методы (GET, POST, PUT, DELETE, PATCH), работу с заголовками, query-параметрами, телом запроса, мультипарт-формами и файлами.
2. Установка
Refit распространяется через NuGet. Для установки используйте одну из следующих команд:
# Через .NET CLI
dotnet add package Refit
# Через Package Manager Console (Visual Studio)
Install-Package Refit
# Или через NuGet Package Manager в IDE
Для работы с асинхронными методами и интеграции с ASP.NET Core рекомендуется также установить пакет Refit.HttpClientFactory:
dotnet add package Refit.HttpClientFactory
Если вы используете System.Text.Json (по умолчанию в .NET Core 3.1+), дополнительных пакетов не требуется. Для Newtonsoft.Json установите Refit.Newtonsoft.Json.
3. Быстрый старт — минимальный рабочий пример
Создадим простой клиент для получения данных с JSONPlaceholder (тестовый REST API).
Шаг 1: Определите модель данных:
public class Todo
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public bool Completed { get; set; }
}
Шаг 2: Создайте интерфейс с атрибутами Refit:
using Refit;
public interface ITodoApi
{
[Get("/todos/{id}")]
Task<Todo> GetTodoAsync(int id);
[Get("/todos")]
Task<List<Todo>> GetAllTodosAsync();
}
Шаг 3: Используйте Refit для создания клиента:
using Refit;
class Program
{
static async Task Main(string[] args)
{
// Создаем клиент напрямую (без DI)
var todoApi = RestService.For<ITodoApi>("https://jsonplaceholder.typicode.com");
// Получаем один Todo
var todo = await todoApi.GetTodoAsync(1);
Console.WriteLine($"Todo: {todo.Title}, Completed: {todo.Completed}");
// Получаем все Todos
var todos = await todoApi.GetAllTodosAsync();
Console.WriteLine($"Total todos: {todos.Count}");
}
}
Этот пример демонстрирует базовую механику: вы описываете интерфейс, а Refit генерирует реализацию, которая выполняет HTTP-запросы и десериализует ответы.
4. Основные методы/функции/классы
4.1. RestService.For<T>()
Сигнатура: public static T For<T>(string hostUrl, RefitSettings settings = null)
Что делает: Создает реализацию интерфейса T для заданного базового URL. Это основной способ создания клиента без использования DI.
var api = RestService.For<IGitHubApi>("https://api.github.com");
4.2. RestService.For<T>(HttpClient)
Сигнатура: public static T For<T>(HttpClient client, RefitSettings settings = null)
Что делает: Создает реализацию интерфейса на основе существующего HttpClient. Полезно, когда нужно поделить один HttpClient между несколькими клиентами.
var client = new HttpClient { BaseAddress = new Uri("https://api.example.com") };
var api = RestService.For<IMyApi>(client);
4.3. RefitSettings
Сигнатура: public class RefitSettings { ... }
Что делает: Класс для настройки поведения Refit: выбор сериализатора (System.Text.Json или Newtonsoft.Json), обработчики исключений, политики повторных попыток и т.д.
var settings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})
};
var api = RestService.For<IMyApi>("https://api.example.com", settings);
4.4. Атрибут [Get]
Сигнатура: [Get(string path)]
Что делает: Указывает, что метод должен выполнять HTTP GET запрос по указанному пути. Поддерживает шаблоны URL с параметрами.
public interface IUserApi
{
[Get("/users/{userId}")]
Task<User> GetUserAsync(int userId);
[Get("/users?role={role}&active={isActive}")]
Task<List<User>> GetUsersByRoleAsync(string role, bool isActive);
}
4.5. Атрибут [Post]
Сигнатура: [Post(string path)]
Что делает: Указывает, что метод должен выполнять HTTP POST запрос. Тело запроса передается через параметр с атрибутом [Body].
public interface IOrderApi
{
[Post("/orders")]
Task<Order> CreateOrderAsync([Body] Order order);
[Post("/orders/batch")]
Task<List<Order>> CreateOrdersAsync([Body] List<Order> orders);
}
4.6. Атрибут [Put] и [Patch]
Сигнатура: [Put(string path)] / [Patch(string path)]
Что делает: Выполняют HTTP PUT (полная замена ресурса) и PATCH (частичное обновление) запросы.
public interface IProductApi
{
[Put("/products/{id}")]
Task<Product> UpdateProductAsync(int id, [Body] Product product);
[Patch("/products/{id}")]
Task<Product> PatchProductAsync(int id, [Body] Dictionary<string, object> updates);
}
4.7. Атрибут [Delete]
Сигнатура: [Delete(string path)]
Что делает: Выполняет HTTP DELETE запрос для удаления ресурса.
public interface IArticleApi
{
[Delete("/articles/{id}")]
Task DeleteArticleAsync(int id);
[Delete("/articles")]
Task DeleteAllArticlesAsync();
}
4.8. Атрибут [Headers]
Сигнатура: [Headers(string header1, string header2, ...)]
Что делает: Позволяет задать статические или динамические заголовки для всех методов интерфейса или конкретного метода. Поддерживает интерполяцию строк.
[Headers("User-Agent: MyApp/1.0", "Accept: application/json")]
public interface IAuthenticatedApi
{
[Get("/me")]
[Headers("Authorization: Bearer")]
Task<User> GetCurrentUserAsync(string authorization);
[Post("/data")]
[Headers("X-Custom-Header: {value}")]
Task SendDataAsync(string value, [Body] object data);
}
4.9. Атрибут [Query] и [AliasAs]
Сигнатура: [Query(string prefix)] / [AliasAs(string name)]
Что делает: [Query] указывает, что параметр должен быть передан как query-параметр. [AliasAs] позволяет переименовать параметр в URL (полезно для C# имен, не совпадающих с API).
public interface ISearchApi
{
[Get("/search")]
Task<SearchResult> SearchAsync(
[Query] string query,
[Query("page")] int pageNumber,
[AliasAs("per_page")] int pageSize
);
}
4.10. Атрибут [Multipart]
Сигнатура: [Multipart]
Что делает: Указывает, что запрос должен быть отправлен как multipart/form-data. Используется для загрузки файлов.
public interface IFileApi
{
[Multipart]
[Post("/upload")]
Task<UploadResult> UploadFileAsync(
[AliasAs("file")] StreamPart stream,
[AliasAs("description")] string description
);
}
// Использование:
var fileStream = File.OpenRead("photo.jpg");
var streamPart = new StreamPart(fileStream, "photo.jpg", "image/jpeg");
var result = await api.UploadFileAsync(streamPart, "My vacation photo");
5. Реальный практический пример (100-200 строк кода)
Разработаем полноценный клиент для GitHub API, который будет получать информацию о репозиториях, создавать issue и работать с файлами.
Модели данных
public class GitHubRepo
{
public long Id { get; set; }
public string Name { get; set; }
public string FullName { get; set; }
public string Description { get; set; }
public string HtmlUrl { get; set; }
public int StargazersCount { get; set; }
public int ForksCount { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class GitHubIssue
{
public long Id { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string State { get; set; }
public DateTime CreatedAt { get; set; }
}
public class CreateIssueRequest
{
[AliasAs("title")]
public string Title { get; set; }
[AliasAs("body")]
public string Body { get; set; }
[AliasAs("labels")]
public string[] Labels { get; set; }
}
public class GitHubFile
{
public string Name { get; set; }
public string Path { get; set; }
public string Content { get; set; }
public string Encoding { get; set; }
}
Интерфейс клиента
[Headers("User-Agent: RefitDemo/1.0", "Accept: application/vnd.github.v3+json")]
public interface IGitHubApi
{
[Get("/repos/{owner}/{repo}")]
Task<GitHubRepo> GetRepositoryAsync(string owner, string repo);
[Get("/repos/{owner}/{repo}/issues")]
Task<List<GitHubIssue>> GetIssuesAsync(
string owner,
string repo,
[Query] string state = "open",
[Query("per_page")] int perPage = 30
);
[Post("/repos/{owner}/{repo}/issues")]
Task<GitHubIssue> CreateIssueAsync(
string owner,
string repo,
[Body] CreateIssueRequest request
);
[Get("/repos/{owner}/{repo}/contents/{path}")]
Task<GitHubFile> GetFileContentAsync(
string owner,
string repo,
string path
);
[Multipart]
[Put("/repos/{owner}/{repo}/contents/{path}")]
Task CreateOrUpdateFileAsync(
string owner,
string repo,
string path,
[AliasAs("message")] string commitMessage,
[AliasAs("content")] StreamPart fileContent
);
}
Использование клиента
using Refit;
using System.Text.Json;
class GitHubClientDemo
{
private readonly IGitHubApi _gitHubApi;
public GitHubClientDemo(string token = null)
{
var settings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true
})
};
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.github.com")
};
if (!string.IsNullOrEmpty(token))
{
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
_gitHubApi = RestService.For<IGitHubApi>(httpClient, settings);
}
public async Task RunAsync()
{
try
{
// 1. Получаем информацию о репозитории Также в библиотеке
Работа с Excel в C++: библиотеки и инструменты для чтения и записи
CLI фреймворки для PHP: создание консольных приложений с Symfony Console и Laravel Zero
iTextSharp для C# (.NET): полное руководство с примерами
Работа с ZIP/архивами в Go (Golang) — полное руководство
Авторизация и аутентификация в Swift: полное руководство для iOS/macOS