Перейти к основному содержимому

Веб-разработка и API на C#

Разработчику Архитектору

Веб-разработка и интеграции

Веб-разработка на C# представляет собой целую экосистему, ориентированную на построение масштабируемых, безопасных и поддерживаемых распределённых приложений. В отличие от классических десктопных решений, где взаимодействие ограничено локальной машиной, веб-приложения по своей природе вовлечены в непрерывный обмен данными с внешним миром — браузерами, мобильными клиентами, сторонними сервисами, корпоративными системами и облачными платформами. Поэтому разработка на C# в веб-контексте всегда сопровождается интеграционной составляющей — даже если приложение начинается как автономный микросервис, со временем оно неизбежно вступает в коммуникацию с другими узлами.

Мы не будем повторять описание ASP.NET как платформы — о нём пойдёт речь в отдельной главе. Здесь же акцент сделан на том, как C# позволяет проектировать, реализовывать и поддерживать интеграционные решения в рамках веб-приложений, будь то монолит, микросервис или гибридное облачно-корпоративное решение.

Быстрый маршрут по статье

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


Веб-сервисы в C#

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

Первым промышленным стандартом в .NET стал Windows Communication Foundation (WCF) — фреймворк, разработанный Microsoft в середине 2000‑х годов и представленный в .NET Framework 3.0. Он был задуман как единая платформа для построения сервис-ориентированных архитектур (SOA), объединяющая в себе ранее существовавшие технологии: ASMX (ASP.NET Web Services), .NET Remoting и Enterprise Services. WCF позволяет создавать сервисы, независимо от используемого транспорта — HTTP, TCP, Named Pipes, MSMQ — и формата сообщений — SOAP, XML, двоичный формат, JSON.

Ключевая идея WCF — трёхкомпонентная модель:

  • Контракт (Contract) определяет, что делает сервис — интерфейс операций (ServiceContract), структура передаваемых данных (DataContract), исключения (FaultContract) и поведение при вызове (OperationContract).
  • Привязка (Binding) определяет, как осуществляется связь — какой протокол, кодировка, безопасность, режим обмена (односторонний, двусторонний, запрос-ответ), тайм-ауты и т.д. Существует иерархия привязок: BasicHttpBinding для совместимости с классическим SOAP 1.1, WSHttpBinding для расширенных WS‑стандартов, NetTcpBinding для высокопроизводительных внутренних коммуникаций и другие.
  • Адрес (Endpoint Address) — URI, по которому клиент может обратиться к сервису. Один сервис может иметь несколько эндпоинтов с разными привязками и адресами.

Реализация сервиса в WCF — это обычная классическая реализация интерфейса, помеченного атрибутами [ServiceContract], [OperationContract]. На стороне клиента создаётся прокси-класс, который может быть сгенерирован автоматически (через svcutil.exe или Add Service Reference в Visual Studio) или построен вручную на основе ChannelFactory<T>. Автогенерируемый прокси инкапсулирует всю логику сериализации, десериализации, вызова и обработки ошибок, предоставляя разработчику интерфейс, максимально близкий к локальному вызову метода.

WCF остаётся актуальным в корпоративной среде, особенно там, где требуется строгая согласованность, транзакционность, надёжная доставка и соблюдение WS-стандартов (например, WS-Security, WS-ReliableMessaging). Однако его кроссплатформенность ограничена: хотя существуют реализации на .NET Core (через System.ServiceModel.* пакеты), полная функциональность доступна только на Windows, а экосистема сильно уступает современным REST/gRPC-решениям в гибкости и скорости разработки.


Современные протоколы интеграции

На смену монолитным SOA-подходам пришёл микросервисный стиль, в котором доминируют REST и всё чаще — gRPC. Это не просто "меньше SOAP" — это принципиально иные философии проектирования.


REST (Representational State Transfer)

REST — архитектурный стиль, основанный на ограничениях, которые обеспечивают масштабируемость, кэшируемость и простоту. В C# поддержка REST реализуется через:

  • HttpClient — стандартный класс для выполнения HTTP-запросов. Он потокобезопасен, поддерживает повторное использование соединений (connection pooling), кэширование заголовков и легко интегрируется с DI через IHttpClientFactory.
  • System.Text.Json (или Newtonsoft.Json) — для сериализации и десериализации JSON.
  • ASP.NET Core Web API — для создания RESTful-контроллеров (подробнее — в главе по ASP.NET).

Важно понимать: "RESTful" — это больше, чем "JSON по HTTP". Это использование стандартных методов (GET, POST, PUT, DELETE, PATCH), идемпотентности, гипермедиа (HATEOAS — хотя на практике редко применяется в enterprise-среде), статус-кодов как части договора (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 409 Conflict, 422 Unprocessable Entity и др.). Именно статус-коды становятся ключевым механизмом разветвления логики на стороне клиента — например, через оператор switch по response.StatusCode.


SOAP (Simple Object Access Protocol)

SOAP — протокол на основе XML, строго регламентированный W3C. Он задаёт формат сообщения (envelope, header, body), правила кодирования и обработки ошибок (fault). В отличие от REST, SOAP — это протокол, а не стиль: он не зависит от HTTP, хотя чаще всего использует его как транспорт. В C# работа с SOAP возможна как через WCF (как уже описано), так и через генерацию клиентов на основе WSDL (Web Services Description Language) — например, с помощью dotnet-svcutil.

SOAP сохраняет значимость в legacy-системах, финансовых институтах, государственных интеграциях (например, ЕГАИС, ФСС), где важны:

  • строгая типизация (WSDL описывает интерфейс как контракт);
  • встроенная поддержка безопасности (WS-Security);
  • транзакции и надёжная доставка (WS-AtomicTransaction, WS-ReliableMessaging);
  • возможность цифровой подписи и шифрования на уровне сообщения.

Однако SOAP-сообщения громоздки, трудны для отладки, плохо кэшируются и плохо ложатся на CDN. Поэтому его применение оправдано только там, где преимущества перевешивают издержки.


gRPC

gRPC — высокопроизводительный RPC-фреймворк, разработанный Google и ставший de facto стандартом для межсервисного взаимодействия в микросервисных архитектурах. Он использует:

  • HTTP/2 как транспорт (множественные потоки, сжатие заголовков, server push);
  • Protocol Buffers (protobuf) — бинарный формат сериализации, компактный и быстрый;
  • строго типизированные контракты, описываемые в .proto-файлах.

В .NET поддержка gRPC встроена начиная с .NET Core 3.0. Сервер реализуется через Grpc.AspNetCore, клиент — через Grpc.Net.Client. Компилятор protoc генерирует C#-классы для сервисов, сообщений и клиентов. gRPC поддерживает четыре типа методов:

  • Unary (запрос-ответ) — аналог REST POST/GET;
  • Server streaming — клиент отправляет запрос, сервер присылает поток ответов;
  • Client streaming — клиент посылает поток запросов, сервер отвечает один раз;
  • Bidirectional streaming — обе стороны взаимодействуют потоками.

gRPC особенно эффективен при высокой частоте вызовов, малых задержках (например, внутри дата-центра) и строгих требованиях к типобезопасности и версионированию. Однако он плохо совместим с браузерами (требуется gRPC-Web и прокси), не поддерживает кэширование и требует дополнительных инструментов для мониторинга (например, OpenTelemetry).


Работа с внешними API в C#

В реальной практике веб-приложение редко существует изолированно. Чаще всего оно взаимодействует с:

  • облачными сервисами (Auth0, SendGrid, AWS S3, Azure Key Vault);
  • государственными системами (ГИС ЖКХ, ЕГРН, Портал госуслуг);
  • платёжными шлюзами (ЮKassa, Tinkoff, Сбербанк);
  • маркетплейсами (Ozon, Wildberries API);
  • внутренними корпоративными системами (1С, SAP, ELMA365, BPMSoft).

Для работы с такими API в C# рекомендуется придерживаться следующих принципов:

  1. Изоляция зависимости — клиентский код не должен напрямую использовать HttpClient. Вместо этого создаётся инкапсулирующий сервис, реализующий интерфейс (например, IWeatherService, IPaymentGateway). Это позволяет легко подменить реализацию (например, для тестов — на мок), внедрять логирование, повторные попытки (retry), ограничение частоты (rate limiting).

  2. Использование IHttpClientFactory — прямое создание new HttpClient() ведёт к утечкам сокетов. HttpClientFactory управляет пулами клиентов, применяет политики (через Polly), поддерживает named/typed clients и корректно обрабатывает жизненный цикл.

  3. Сериализация и валидация — все входящие и исходящие модели должны быть строго типизированы. Используются [JsonPropertyName], [JsonIgnore], [Required], [Range] и другие атрибуты. Валидация выполняется до отправки и после получения (например, через Validator.TryValidateObject из System.ComponentModel.DataAnnotations или FluentValidation). См. Проверка и валидация.

  4. Обработка ошибок — HTTP-статусы не всегда отражают бизнес-ошибку. Например, 400 Bad Request может быть вызван неверным форматом даты, а 409 Conflict — дубликатом записи. Поэтому после проверки IsSuccessStatusCode часто проводится анализ тела ответа — десериализация в ErrorResponse, извлечение кода ошибки, локализованного сообщения, дополнительных метаданных.

  5. Расширяемость через шаблоны проектирования — например, шаблон Template Method (protected abstract void) позволяет вынести общую логику вызова (аутентификация, логирование, тайм-ауты) в базовый класс, а специфическую — в производные. Это особенно удобно в интеграциях с одинаковой структурой, но разными эндпоинтами (например, экспорт в разные CRM).


Планирование задач

В веб-приложениях на C# часто возникает необходимость выполнять операции по расписанию — ежедневная выгрузка отчётов, синхронизация справочников, очистка временных данных, отправка напоминаний. Это задачи, которые требуют надёжного, масштабируемого и наблюдаемого планировщика.

В .NET экосистеме доминирующим решением является Quartz.NET — порт Java-библиотеки Quartz, адаптированный под CLR и интегрированный с современными практиками .NET (включая DI, IHostedService, ILogger).


Основные понятия Quartz.NET

Quartz.NET строится вокруг трёх ключевых абстракций:

  • Job — реализация логики, которая должна быть выполнена. Это класс, реализующий интерфейс IJob, с единственным методом . Внутри Execute размещается вся бизнес-логика — обращение к репозиториям, вызов внешних API, запись в лог. Job-ы должны быть идемпотентными и потокобезопасными, поскольку один и тот же экземпляр класса может быть использован многократно (если не указано DisallowConcurrentExecution).
Execute(IJobExecutionContext context)
  • Trigger — объект, определяющий когда и как часто должен быть запущен Job. Существует несколько типов триггеров:

    • SimpleTrigger — запуск через фиксированный интервал или однократно с задержкой.
    • CronTrigger — наиболее гибкий, использует cron-выражения, совместимые со стандартом Unix, но с расширениями Quartz.

    Формат cron-выражения в Quartz состоит из семи полей:
    секунды минуты часы день_месяца месяц день_недели год (опционально)
    Пример — 0 0 1 */10 * ? — в 01:00:00 каждые 10 дней месяца (день месяца = 1, 11, 21).
    Обратите внимание — использование ? в поле "день недели" означает отсутствие ограничения — это необходимо, когда задан "день месяца", чтобы избежать конфликта (в стандартном Unix-cron таких конфликтов нет, но Quartz строже).
    Другие полезные спецификаторы:

    • * — любое значение;
    • / — шаг (например, 0/5 в минутах — каждые 5 минут);
    • L — последний день месяца или последний день недели (в контексте);
    • W — ближайший рабочий день;
    • # — например, 6#3 — третья суббота месяца.
  • Scheduler — центральный оркестратор. Он управляет жизненным циклом Job-ов и Trigger-ов — регистрирует их, запускает по расписанию, обрабатывает ошибки, сохраняет состояние (если используется персистентное хранилище, например, PostgreSQL или SQL Server через AdoJobStore). В ASP.NET Core IScheduler обычно регистрируется как singleton и запускается при старте приложения через IHostedService.


Интеграция с DI и жизненным циклом приложения

Один из сильных сторон Quartz.NET — его совместимость с системой внедрения зависимостей. Job-ы могут получать зависимости через конструктор, если зарегистрирован JobFactory, поддерживающий DI (например, MicrosoftDependencyInjectionJobFactory). Это позволяет инжектить ILogger<T>, IHttpClientFactory, репозитории, контексты EF Core и другие сервисы — без необходимости ручного разрешения через контейнер.

Важно: жизненный цикл зависимостей внутри Job-а требует внимания. Если Job использует scoped-сервисы (например, DbContext), их следует создавать внутри Execute в новой области видимости (scope), чтобы избежать проблем с потокобезопасностью и утечками памяти.

Quartz.NET также поддерживает кластеризацию — несколько экземпляров приложения могут делить одно расписание, и Scheduler гарантирует, что Job будет выполнен только на одном узле — критически важно для отказоустойчивых систем.


Потоки данных в интеграциях

Интеграция — это не единовременный вызов API. Это процесс, состоящий из этапов — подготовка данных, преобразование, передача, обработка ответа, сохранение результата, логирование, откат в случае ошибки. Для структурирования таких процессов применяется концепция интеграционного потока (integration flow).

Интеграционный поток — это последовательность шагов (steps), каждый из которых отвечает за определённую операцию — выборка, фильтрация, маппинг, сериализация, отправка, десериализация, валидация, сохранение. Поток может быть линейным или разветвлённым (например, при switch по HTTP-статусу), синхронным или асинхронным.

Часто поток разбивается на подпотоки (substreams) — логические блоки, выполняющие автономную подзадачу. Например:

  • подпоток экспорта: выборка → валидация → маппинг → сериализация → отправка;
  • подпоток импорта: получение → десериализация → валидация → маппинг → сохранение.

Такое разделение повышает читаемость, тестируемость и переиспользуемость кода. Подпотоки могут быть реализованы как отдельные классы, методы или компоненты, собранные в цепочку (pipeline).


Типизированные абстракции потоков

В крупных проектах целесообразно вводить обобщающие интерфейсы, чтобы унифицировать работу с потоками. Например:

public interface IOutputIntegrationStream<TModel, TResponse>
{
Task<TResponse> ExecuteAsync(TModel input, CancellationToken ct = default);
}

Здесь TModel — внутренняя модель системы (например, EmployeeEntity), а TResponse — тип ответа внешнего сервиса (например, CreateEmployeeResponseDto). Это позволяет строить декларативные цепочки:
Validate → Map → Serialize → Send → Deserialize → Interpret.

Аналогично, для входящих данных:

public interface IInputIntegrationStream<TRequest, TResult>
{
Task<TResult> ProcessAsync(TRequest payload, CancellationToken ct = default);
}

Такие абстракции особенно полезны при построении фреймворков интеграций внутри продукта — например, в BPM-подобных системах, где десятки интеграций должны следовать единому стандарту обработки ошибок, логирования и транзакционности.


Работа с моделями

Модель — это не просто класс с полями. В контексте интеграций модель проходит несколько стадий преобразования:

  1. Извлечение (Export) — данные выбираются из внутреннего хранилища. Для этого используется ExportRepository<TModel> — интерфейс или абстрактный класс, инкапсулирующий логику запроса — фильтрация по дате изменения, пагинация, выборка связанных сущностей, применение политик безопасности (например, RBAC-фильтрация на уровне SQL). Важно — методы репозитория должны возвращать только то, что необходимо для интеграции, избегая избыточной загрузки.

  2. Преобразование (Mapping) — переход от доменной модели (Employee) к DTO (EmployeeExportDto). Здесь применяются:

    • ручное маппинг (простые случаи);
    • AutoMapper (с профайлами, условными сопоставлениями, валидацией после маппинга);
    • record-типы и with-выражения для иммутабельных преобразований.

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

  3. Валидация — проверка соответствия данных контракту внешней системы. Это может быть:

    • валидация по атрибутам ([StringLength(50)], [RegularExpression]);
    • бизнес-правила ("поле ИНН должно быть 10 или 12 цифр");
    • кросс-валидация ("если тип = юрлицо, то ОГРН обязателен").

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

  4. Сериализация — преобразование объекта в формат передачи — JSON, XML, protobuf. Здесь важны:

    • настройка имен полей ([JsonPropertyName("employee_id")]);
    • обработка null-значений;
    • кастомные конвертеры (например, для DateTime в формат yyyy-MM-ddTHH:mm:ssZ);
    • поддержка версионирования (например, JsonSerializerContext в System.Text.Json).
  5. Сохранение (Import) — после получения данных от внешней системы они проходят обратный путь: десериализация → валидация → маппинг → сохранение через ImportRepository<TModel>. Этот репозиторий отвечает за идемпотентность ("если запись с таким ID уже есть — обнови, иначе создай"), обработку конфликтов, лог аудита изменений.


Сессии интеграции

Многие внешние системы требуют установления сессии перед началом обмена данными — особенно это характерно для SOAP-сервисов, старых REST API и государственных интеграций (например, ЕГАИС, МЧД).

Сессия — это контекст взаимодействия, характеризуемый:

  • временем жизни (TTL);
  • уникальным идентификатором (session ID, token);
  • правами доступа (scope, roles);
  • метаданными (IP, user agent, timestamp создания).

Этапы работы с сессией

  1. Открытие сессии — вызов метода аутентификации (например, /auth/login, LoginAsync() в WCF). В ответ система возвращает токен или session ID. Эта операция может включать:

    • формирование тела запроса (логин/пароль, сертификат, SAML-assertion);
    • подпись запроса (HMAC, RSA);
    • обработку многофакторной аутентификации (редко, но бывает).
  2. Хранение и использование — токен сохраняется в памяти (для короткоживущих интеграций) или в распределённом кэше (Redis — для масштабируемых решений). При каждом последующем вызове он передаётся в заголовках:

    • Authorization: Bearer <token> — для OAuth/JWT;
    • Authorization: Basic <base64(login:pass)> — для базовой аутентификации;
    • X-Session-ID: abc123 — для кастомных схем.
  3. Обновление и продление — если токен имеет ограниченный срок, до истечения срока производится refresh (например, через /auth/refresh). Quartz.NET может запускать фоновый Job для своевременного обновления токена.

  4. Закрытие сессии — явный вызов logout или автоматическое завершение по тайм-ауту. Это важно для систем с лицензированием по количеству одновременных сессий.


Авторизационные схемы в C#

В .NET поддержка авторизации интеграций осуществляется через:

  • HttpClient.DefaultRequestHeaders.Authorization — для простых случаев;
  • DelegatingHandler — для добавления заголовков, подписи, логгирования на уровне HTTP-стека;
  • IAuthenticationService — для сложных сценариев с OAuth2 (client credentials flow, authorization code flow), где требуется получение токена через HttpClient → парсинг ответа → кэширование.

Для OAuth2 рекомендуется использовать библиотеки вроде IdentityModel, которая предоставляет ClientCredentialsTokenRequest, TokenClient, управление refresh-токенами и автоматический retry при 401.


Настройка эндпоинтов

Адреса внешних сервисов никогда не должны быть зашиты в код. Они выносятся в конфигурацию:

  • appsettings.jsonappsettings.Production.json;
  • переменные окружения (для sensitive-данных: API__URL, API__CLIENT_SECRET);
  • секреты через Azure Key Vault / HashiCorp Vault.

Затем они инжектятся через IOptions<T> или IConfiguration. Например:

public class ExternalApiOptions
{
public string BaseUrl { get; set; } = default!;
public string AuthEndpoint { get; set; } = default!;
public string ClientId { get; set; } = default!;
}

Регистрация в DI:

services.Configure<ExternalApiOptions>(configuration.GetSection("ExternalApi"));
services.AddHttpClient<IExternalService, ExternalService>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<ExternalApiOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl);
});

Обработка ответов

После выполнения HTTP-запроса или WCF-вызова приложение получает ответ, который может быть:

  • успешным (2xx, 200 OK);
  • клиентской ошибкой (4xx);
  • серверной ошибкой (5xx);
  • не-HTTP-ошибкой (тайм-аут, DNS failure, SSL handshake error).

Ветвление по статус-кодам

Наиболее прозрачный и поддерживаемый способ — использование switch по response.StatusCode:

Код ITЗагрузка примера кода…

Такой подход делает логику явной, легко тестируемой и соответствует принципу fail fast.


Импорт данных через подпотоки

При получении массива данных (например, список сотрудников от HR-системы) применяется подпоток импорта:

  1. Десериализация всего массива или построчная обработка (если данные большие — через JsonDocument или Utf8JsonReader);
  2. Параллельная или последовательная обработка каждой записи:
    • валидация;
    • маппинг в доменную модель;
    • проверка существования (по уникальному ключу);
    • идемпотентное сохранение (upsert);
  3. Фиксация транзакции (если требуется атомарность пакета);
  4. Отправка событий (например, EmployeeImportedEvent) для дальнейшей обработки (рассылки, кэширования).

Важно: импорт должен быть отказоустойчивым. Если при обработке 1000-й записи произошла ошибка, предыдущие 999 не должны быть откачены (если это не требование бизнеса). Часто применяется паттерн "batch with checkpointing" — фиксация каждых N записей, логирование позиции последней успешной обработки, возобновление с этой точки при перезапуске. Подробнее — Пакетная работа с данными.


Шаблон "Template Method" для кастомизации

Для унификации базовой логики интеграций (аутентификация, логирование, тайм-ауты, retry) и при этом сохранения гибкости для конкретных реализаций, широко применяется шаблон Template Method.

Пример базового класса:

Код ITЗагрузка примера кода…

Конкретный сервис наследуется и реализует только специфичные части:

Код ITЗагрузка примера кода…

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


Надёжность интеграций

Внешние системы — по своей природе ненадёжны. Они могут отвечать с задержкой, возвращать ошибки 503 Service Unavailable, временно блокировать запросы по лимитам, менять контракт без уведомления. Полагаться на "всё будет работать" — техническая и организационная ошибка. Надёжность интеграции закладывается на этапе проектирования и реализуется через систему защитных механизмов.


Паттерны устойчивости

Библиотека Polly (входит в .NET Foundation) является де-факто стандартом для реализации политик устойчивости в .NET. Она позволяет декларативно определять, как приложение должно реагировать на исключения и HTTP-статусы, не засоряя бизнес-логику условиями.

Основные типы политик:

  1. Retry — повторный вызов при временных сбоях.
    Пример — повторить до 3 раз с экспоненциальной задержкой (1 с, 2 с, 4 с), если статус 5xx или исключение HttpRequestException.
var retryPolicy = Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.Or<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (outcome, timespan, attempt, context) =>
_logger.LogWarning("Попытка {Attempt} провалена. Повтор через {Delay}",
attempt, timespan));

Важно — повторять следует только идемпотентные операции (GET, PUT, DELETE с idempotency key). Для POST без идемпотентности повтор может привести к дубликатам.

  1. Circuit Breaker — "аварийное отключение" при массовых сбоях.
    Если за короткий интервал (например, 30 секунд) зафиксировано N неудачных попыток, Circuit Breaker переходит в состояние Open, и все последующие вызовы мгновенно завершаются исключением BrokenCircuitException — без обращения к внешней системе. Через заданный период (например, 30 секунд) он переходит в состояние Half-Open: пропускает один тестовый запрос. Если успешен — возвращается в Closed; если нет — снова в Open.
var circuitBreaker = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.InternalServerError)
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));

Это защищает от каскадных сбоев — если сервис X падает, и каждый вызов к нему ждёт 30 секунд таймаута, то быстро исчерпываются потоки пула и падает вся система.

  1. Timeout — принудительное прерывание долгого вызова.
    Даже если внешний сервис "завис", CancellationToken с таймаутом гарантирует освобождение ресурсов.

  2. Fallback — альтернативное поведение при окончательном провале.
    Например — вернуть данные из кэша, использовать устаревшую версию справочника, поставить задачу в очередь отложенной обработки, сформировать уведомление оператору.

var fallback = Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.OrResult(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)
.FallbackAsync(async (context, ct) =>
{
var cached = await _cache.GetAsync<ReportData>("last_report", ct);
if (cached != null)
return new HttpResponseMessage(HttpStatusCode.OK) { Content = ... };
throw new IntegrationUnavailableException("Интеграция недоступна, кэш пуст.");
});
  1. Bulkhead Isolation — ограничение количества одновременных вызовов к одной зависимости.
    Аналогично пулу потоков: даже если сервис X "захлебнулся", он не сможет занять все потоки приложения — остальные интеграции продолжат работать.

Все политики можно комбинировать через PolicyWrap:
fallbackPolicy.Wrap(circuitBreakerPolicy.Wrap(timeoutPolicy.Wrap(retryPolicy))).

Polly интегрируется с IHttpClientFactory через AddPolicyHandler, что позволяет централизованно управлять надёжностью на уровне именованных клиентов.


Наблюдаемость

Без наблюдаемости интеграция — "чёрный ящик". Проблема может проявиться только тогда, когда пользователь пожалуется, а к тому моменту данные уже потеряны. Наблюдаемость строится на трёх китах: логи, трассировки (traces) и метрики.


Структурированное логирование

Использование ILogger<T> с семантическими свойствами — обязанность. Пример:

_logger.LogInformation(
"Интеграция {IntegrationName} запущена. Количество записей: {ItemCount}. CorrelationId: {CorrelationId}",
nameof(PayrollExport), items.Count, correlationId);

_logger.LogWarning(
"Предупреждение при валидации элемента {Index}: {ValidationErrors}. Элемент пропущен.",
i, string.Join("; ", errors), correlationId);

_logger.LogError(
exception,
"Критическая ошибка в интеграции {IntegrationName} на этапе {Stage}. CorrelationId: {CorrelationId}",
nameof(EmployeeSync), "SendToExternalSystem", correlationId);

Ключевые практики:

  • Correlation ID — уникальный идентификатор, передаваемый через заголовок X-Correlation-ID во все внутренние и внешние вызовы (если поддерживается). Позволяет собрать полную цепочку событий по одной операции.
  • Стадии операции — явное логирование этапов (ExportStart, ExportComplete, SendStart, SendRetry, ImportStart и т.д.).
  • Контекстные данныеItemCount, Endpoint, StatusCode, DurationMs, TokenExpiryTime.
  • Чувствительные данныеникогда не логировать пароли, токены, персональные данные (PII). Использовать маскировку: ***.

Логи должны отправляться в централизованную систему (ELK, Grafana Loki, Splunk), где возможен поиск по CorrelationId, фильтрация по IntegrationName, построение дашбордов.


Распределённая трассировка (Distributed Tracing)

Трассировка — это сбор информации о пути запроса через микросервисы. В .NET поддержка реализована через OpenTelemetry — vendor-независимый стандарт.

Подключение OpenTelemetry в ASP.NET Core:

services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("CustomIntegration")
.AddOtlpExporter(o => o.Endpoint = new Uri("http://jaeger:4317")));

В коде интеграции можно создавать активный спан:

using var activity = _activitySource.StartActivity("EmployeeExport");
activity?.SetTag("integration.type", "payroll");
activity?.SetTag("item.count", items.Count);

// внутри — вызовы HttpClient, которые автоматически дочерят свои спаны

Результат — в Jaeger или Zipkin можно увидеть:
запрос → API Gateway → AuthService → PayrollService → External HR System, с длительностями каждого этапа, ошибками, аннотациями.

Трассировка особенно важна при диагностике "длинных хвостов" — когда 99% вызовов быстрые, но 1% "зависают" на минуты.


Метрики

Метрики позволяют отслеживать состояние интеграций в реальном времени и строить alert-уведомления:

  • integration_requests_total{integration="payroll", status="success|failure|timeout"} — счётчик вызовов;
  • integration_duration_seconds{integration="payroll", quantile="0.5|0.95|0.99"} — гистограмма задержек;
  • integration_queue_length{integration="payroll"} — длина очереди отложенных задач;
  • integration_active_sessions{System="egais"} — количество открытых сессий.

В .NET метрики собираются через IMeterFactory (из System.Diagnostics.Metrics) и экспортируются в Prometheus, StatsD или Application Insights.


Безопасность интеграций

Безопасность интеграций — не опция. Особенно когда речь идёт о передаче персональных данных, финансовой информации или взаимодействии с государственными системами.


Хранение и управление секретами

  • Никогда не хранить токены, пароли, ключи в коде, appsettings.json или переменных окружения напрямую в контейнере.
  • Использовать средства управления секретами:
    • Azure Key Vault / AWS Secrets Manager / HashiCorp Vault — для production;
    • user-secrets — для разработки (см. блок ниже);
    • зашифрованные конфигурационные провайдеры (например, на основе DPAPI или кастомного AES).
dotnet user-secrets set "ApiKey" "dev-only-value"

В .NET секреты инжектятся через IConfiguration, но сам механизм получения скрыт:

services.AddAzureKeyVault(
new Uri("https://myvault.vault.azure.net/"),
new DefaultAzureCredential());

Защита канала передачи

  • TLS 1.2/1.3 — обязательное требование. Отключать старые версии на уровне HttpClientHandler:
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
};
  • Проверка сертификатов — отключать ServerCertificateCustomValidationCallback только в sandbox-средах и никогда в production.
  • Пinning сертификатов (Certificate Pinning) — для высококритичных интеграций (например, с ЦБ), чтобы предотвратить MITM даже при доверии к CA.

Подпись запросов (Request Signing)

Некоторые API (например, Сбербанк, МПК) требуют криптографической подписи тела запроса. Обычно это:

  1. Формирование канонической строки (например, method\nurl\nbody_hash\ntimestamp);
  2. Вычисление HMAC-SHA256 с секретным ключом;
  3. Передача подписи и timestamp в заголовках (X-Signature, X-Timestamp).

Пример:

var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var bodyJson = JsonSerializer.Serialize(request);
var bodyHash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(bodyJson)));
var message = $"{method}\n{path}\n{bodyHash}\n{timestamp}";
var signature = SignHmac(message, secretKey);

requestMessage.Headers.Add("X-Timestamp", timestamp);
requestMessage.Headers.Add("X-Signature", signature);

Такая подпись защищает от подделки и replay-атак (если сервер проверяет timestamp ±5 минут).


Защита от replay-атак

Помимо подписи, используются:

  • Idempotency Keys — клиент генерирует уникальный ключ (Guid.NewGuid().ToString()) и передаёт в заголовке Idempotency-Key. Сервер кэширует результат первого вызова с этим ключом и возвращает его при повторах.
  • Nonce — одноразовый номер, который сервер проверяет на уникальность в течение сессии.

Аудит и соответствие

Все интеграционные операции, затрагивающие ПДн, финансовые транзакции или критичные изменения, должны логироваться в аудит-журнал с указанием:

  • кто инициировал (система, пользователь);
  • что изменено (до/после — если возможно);
  • когда (timestamp UTC);
  • почему (CorrelationId, бизнес-контекст);
  • результат (успех/ошибка, код).

Хранение должно соответствовать требованиям (например, 6 месяцев по 152-ФЗ для ПДн).


Тестирование интеграций

Тестирование интеграций принципиально отличается от unit-тестов. Здесь проверяется взаимодействие между системами. Поэтому применяется многоуровневый подход.


1. Контрактные тесты (Contract Tests)

Цель — убедиться, что клиент и сервер понимают друг друга. Проверяются:

  • соответствие DTO структуре, описанной в OpenAPI/Swagger/WSDL;
  • обработка крайних случаев (null, пустые массивы, длинные строки);
  • корректность статус-кодов и формата ошибок.

Инструменты:

  • Pact.NET — создаёт "контракт" (JSON-файл), который проверяется как на стороне потребителя, так и поставщика;
  • TestServer + HttpClient — для интеграций с собственными API.

2. Тесты с реальными sandbox-средами

Многие внешние системы предоставляют песочницы (Sberbank Business, Tinkoff, Ozon Seller API, ЕГАИС Тест). Тесты против них:

  • запускаются в CI/CD (но не на каждом коммите — дорого и медленно);
  • используют отдельные учётные записи и тестовые данные;
  • проверяют полный цикл: аутентификация → отправка → получение → сверка результата.

Важно: тесты должны быть идемпотентными и не оставлять мусора (очищать созданные сущности в finally или через IDisposable).


3. Моки и stub-сервисы

Для локальной разработки и unit-тестов внешние зависимости заменяются:

  • Moq — для моков репозиториев, IHttpClientFactory;
  • WireMock.NET — создаёт локальный HTTP-сервер, эмулирующий внешний API по заданным правилам (маппинг запрос → ответ);
  • Testcontainers — запуск Docker-контейнера с mock-сервисом (например, mockserver/mockserver).

Пример WireMock:

_server = WireMockServer.Start();
_server.Given(Request.Create().WithPath("/auth/login").UsingPost())
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithBody(@"{ ""token"": ""mock_jwt_token"" }"));

4. Тесты на отказоустойчивость

Проверяют, как интеграция ведёт себя при:

  • имитации таймаута (Task.Delay(Timeout.InfiniteTimeSpan));
  • возврате 500 Internal Server Error;
  • разрыве соединения.

Это делается через кастомные DelegatingHandler, перехватывающие запросы и возвращающие ошибки по правилу.


5. Нагрузочное тестирование

Инструменты — k6, JMeter, Locust. Проверяются:

  • пропускная способность (req/sec);
  • стабильность под нагрузкой (длительность сессий, утечки памяти);
  • поведение при исчерпании лимитов (rate limiting).

Особое внимание — отложенным интеграциям (Quartz-задачи): не должна возникать "лавина" одновременных вызовов после простоя.


Архитектурные шаблоны взаимодействия

Выбор модели взаимодействия — один из самых важных архитектурных решений. Он определяет техническую реализацию и требования к SLA, сложность отладки, стоимость сопровождения и устойчивость к сбоям.


1. Request–Response (синхронный вызов)

Наиболее привычный и простой шаблон: клиент отправляет запрос и ждёт ответа. Используется в:

  • веб-API (REST, gRPC unary);
  • WCF-вызовах;
  • формах с немедленной проверкой (валидация ИНН, расчёт доставки).

Преимущества — простота отладки, чёткий контракт, естественная обработка ошибок.
Недостатки — жёсткая связанность по времени (client и server должны быть онлайн одновременно), риск таймаутов, невозможность обработки долгих операций (>30 сек в HTTP), каскадное влияние сбоев.

Рекомендации:

  • ограничивать длительность вызова (таймауты на клиенте и сервере);
  • использовать только для идемпотентных или короткоживущих операций;
  • применять Circuit Breaker, чтобы изолировать сбои.

2. Fire-and-Forget (асинхронный односторонний вызов)

Клиент отправляет сообщение и не ожидает подтверждения. Примеры:

  • отправка логов или метрик;
  • уведомления (email, push), где доставка "лучше позже, чем никогда";
  • триггеры событий в слабосвязанных системах.

В C# реализуется через:

  • фоновые задачи (IHostedService, BackgroundService);
  • отложенную отправку через Task.Run (с осторожностью — без гарантии выполнения);
  • запись в очередь (RabbitMQ, Azure Queue Storage), с последующей асинхронной обработкой.

Преимущества — высокая производительность, отсутствие блокировок, изоляция сбоев.
Недостатки — отсутствие обратной связи, риск потери сообщений (если нет персистентности), сложность в отладке и аудите.

Критически важно — обеспечить надёжную доставку — например, запись в БД перед отправкой, подтверждение получения (ack/nack), dead-letter queue для повторной обработки.


3. Polling (опрос)

Клиент периодически опрашивает сервер на предмет изменений. Используется, когда:

  • внешняя система не поддерживает push-уведомления;
  • требуется синхронизация состояния (например, загрузка новых заказов из маркетплейса).

Реализуется через Quartz.NET или PeriodicTimer. Пример цикла:

  1. Получить метку последнего изменения (timestamp, sequence number);
  2. Запросить записи, изменённые после этой метки;
  3. Обработать и сохранить;
  4. Обновить метку.

Преимущества: простота, совместимость со старыми API.
Недостатки — избыточный трафик ("пустые" запросы), задержка реакции (до интервала опроса), нагрузка на сервер.

Оптимизации:

  • адаптивный интервал (увеличивать при отсутствии изменений);
  • long polling (сервер удерживает соединение до появления данных);
  • использование If-Modified-Since / ETag для минимизации трафика.

4. Webhooks (обратные вызовы)

Сервер инициирует вызов клиенту при наступлении события. Это push-модель:

  1. Клиент регистрирует callback URL (POST /webhooks/register { url: "https://myapp.com/events" });
  2. При событии (новый платёж, изменение статуса заказа) сервер отправляет HTTP-запрос на этот URL.

В C# обработка webhook-ов:

  • контроллер с методом [HttpPost("events")];
  • валидация подписи (обычно HMAC заголовка X-Signature);
  • идемпотентность по Idempotency-Key или Event-Id;
  • асинхронная обработка (запись в очередь, ответ 202 Accepted).

Преимущества: минимальная задержка, эффективное использование ресурсов.
Недостатки — необходимость публично доступного endpoint (проблема для on-premise-систем), сложность обеспечения безопасности, риск DDoS через подделку событий.

Безопасность webhook-ов:

  • подпись запроса (обязательно);
  • проверка source IP (если известен);
  • подтверждение подписки (challenge-response при регистрации);
  • ограничение частоты вызовов (rate limiting).

5. Событийно-ориентированная архитектура (Event-Driven)

Наивысший уровень декомпозиции — системы общаются через поток событий (event stream), например, Kafka или Azure Event Hubs. События:

  • неизменяемы (immutable);
  • имеют временнУю упорядоченность;
  • могут быть прочитаны многократно (replayability).

Пример:
OrderCreated → PaymentRequested → PaymentConfirmed → ShipmentScheduled

В .NET обработка событий:

  • IHostedService, подписанный на топик;
  • десериализация события (Avro, JSON Schema);
  • применение логики (например, обновление агрегата);
  • публикация новых событий.

Преимущества — максимальная слабая связанность, масштабируемость, аудит по событиям, поддержка аналитики (event sourcing).
Недостатки — сложность отладки (распределённые транзакции), необходимость управления состоянием (например, через Saga pattern), задержки при обработке.

Выбор модели определяется бизнес-требованиями:
— нужен ли мгновенный ответ? → Request–Response;
— допустима ли задержка в минуты/часы? → Polling / Queue;
— важна ли гарантия доставки? → Queue с ack;
— требуется ли историческая воспроизводимость? → Event Sourcing.


Версионирование API и стратегии миграции

Внешние интеграции зависят от окружения. API меняются — добавляются поля, удаляются методы, меняются контракты. Без стратегии версионирования это приводит к регрессиям и простою систем.


Подходы к версионированию

  1. Версия в URL (/api/v1/orders) — самый распространённый, простой для маршрутизации, явный для клиента.
    Минус: изменение версии требует изменения вызывающего кода, несовместим с кэшированием CDN по URL.

  2. Версия в заголовке (Accept: application/vnd.myapi.v2+json) — чище с точки зрения REST (ресурс один, представление разное), но сложнее для отладки и инструментов (Swagger требует кастомной настройки).

  3. Параметр запроса (?api-version=2.0) — не рекомендуется: нарушает идемпотентность GET, мешает кэшированию.

  4. Семантическое версионирование (SemVer) в теле ответа или метаданных — дополняет другие методы, позволяет клиенту адаптироваться динамически.


Стратегии обратной совместимости

  • Добавление полей — безопасно. Удаление или изменение типа — ломает совместимость.
  • Устаревшие (deprecated) поля — помечаются атрибутом [Obsolete] и документируются; удаляются только в мажорной версии.
  • Новые обязательные поля — вводятся как опциональные в текущей версии, становятся обязательными в следующей.
  • Поддержка нескольких версий параллельно — в течение 6–12 месяцев после релиза новой версии.

Миграция клиентов

  • Постепенный переход: клиенты обновляются по графику, с контролем через заголовок X-API-Client-Version.
  • Feature flags: включение новой логики по флагу, даже при старой версии API.
  • Канареечный релиз — новая версия API доступна части трафика (по IP, клиенту), мониторинг метрик.
  • Автоматическое обновление клиентов — если интеграция управляется централизованно (например, в ELMA365-плагинах).

Важно: вести регистр изменений (changelog) с указанием:

  • дата ввода;
  • описание изменения;
  • совместимость (breaking/non-breaking);
  • срок поддержки старой версии.

Документирование интеграций

Документация — часть контракта. Плохая документация ведёт к ошибкам, задержкам внедрения и росту стоимости поддержки.


Уровни документации

  1. Машинно-читаемый контракт:

    • OpenAPI (Swagger) — для REST — описывает endpoint, методы, параметры, схемы запроса/ответа, примеры, ошибки.
    • WSDL — для SOAP.
    • .proto — для gRPC.

    Генерируется автоматически из кода (например, через Swashbuckle.AspNetCore), но требует аннотаций ([ProducesResponseType], summary, example).

  2. Человеко-читаемая документация:

    • Описание сценариев — "как создать заказ — сначала авторизоваться, затем создать черновик, затем подтвердить".
    • Поля со смысломstatus — 1 — новый, 2 — в обработке, 4 — отменён (см. справочник Statuses).
    • Ограничения и лимиты — количество запросов в минуту, размер тела, время хранения сессии.
    • Типичные ошибки400 — поле amount < 0, 409: заказ уже оплачен, 429: превышен лимит 100 req/min.

    Форматы — Markdown (для MkDocs, Docusaurus), Confluence, GitBook.

  3. Живые примеры:

    • Postman-коллекции с переменными окружения;
    • curl-примеры с подстановкой токенов;
    • C#-сниппеты с HttpClient, включая обработку ошибок.

Поддержка актуальности

  • Документация как код — хранение в Git, review через PR, CI-проверка на наличие изменений в API без обновления docs.
  • Генерация из тестов: если тест описывает сценарий — он может быть исходником для документации.
  • Обратная связь от интеграторов: формы "сообщить об ошибке в документации", регулярные опросы.

Сопровождение интеграций

Интеграция не "разработал и забыл". Это живой компонент, требующий:

  • Регулярного аудита — раз в квартал проверять:
    • актуальность учётных данных;
    • соответствие контракту (изменения в OpenAPI);
    • логи ошибок (возросло ли число 4xx/5xx);
    • нагрузку (увеличилось ли время выполнения).
  • Плана отката — если новая версия API ломает интеграцию, должен быть механизм быстрого возврата (feature flag, переключение версии в конфигурации).
  • Управления долгом — технический долг в интеграциях особенно опасен — устаревшие протоколы, отсутствие retry, захардкоженные токены. Вести реестр и выделять время на рефакторинг.
  • Обучения команды — проводить демо-сессии: "Как работает интеграция с ЕГАИС", "Как отладить webhook от Сбербанка".

Практический план внедрения интеграции

Чтобы материал статьи превратить в рабочий процесс, используйте последовательность:

  1. Описать контракт и версию API.
  2. Сделать минимальный клиент с обработкой ошибок и логированием.
  3. Добавить retry, timeout и circuit breaker.
  4. Подключить метрики и трассировку.
  5. Написать контрактные тесты и тесты отказоустойчивости.
  6. Подготовить runbook сопровождения для команды.

Так интеграция из "единичной задачи" становится поддерживаемым инженерным компонентом.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Веб-разработкаC# — о разделе, ASP.NET - фреймворк для веб-приложений, ASP.NET - веб-платформа Microsoft, Документация и практика ASP.NET (Microsoft Learn), PHP — о разделе, Приложение с S3, PostgreSQL и ASP.NET Core Web API.