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

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

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

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

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

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


Веб-сервисы в 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(IJobExecutionContext context). Внутри Execute размещается вся бизнес-логика: обращение к репозиториям, вызов внешних API, запись в лог. Job-ы должны быть идемпотентными и потокобезопасными, поскольку один и тот же экземпляр класса может быть использован многократно (если не указано DisallowConcurrentExecution).

  • 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:

switch (response.StatusCode)
{
case HttpStatusCode.OK:
var result = await response.Content.ReadFromJsonAsync<SuccessDto>();
return result;

case HttpStatusCode.BadRequest:
var error = await response.Content.ReadFromJsonAsync<ValidationErrorDto>();
throw new IntegrationValidationException(error.Messages);

case HttpStatusCode.Unauthorized:
// возможно, сессия протухла — попытка refresh + повтор
await _authService.RefreshTokenAsync();
return await SendAsync(request, cancellationToken); // повтор

case HttpStatusCode.Conflict:
throw new IntegrationConflictException("Запись уже существует во внешней системе.");

default:
_logger.LogWarning("Необработанный статус {StatusCode} при вызове {Endpoint}",
response.StatusCode, request.Endpoint);
throw new IntegrationUnexpectedResponseException(response.StatusCode);
}

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

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

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

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

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

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

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

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

public abstract class BaseIntegrationService
{
protected readonly IHttpClientFactory _httpClientFactory;
protected readonly ILogger _logger;

public BaseIntegrationService(IHttpClientFactory httpClientFactory, ILogger logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}

public async Task<TResponse> ExecuteAsync<TRequest, TResponse>(
TRequest request,
CancellationToken ct = default)
{
// Шаг 1: Получить/обновить токен
var token = await AcquireTokenAsync(ct);

// Шаг 2: Создать клиент
var client = _httpClientFactory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

// Шаг 3: Подготовить запрос (абстрактный метод)
var httpRequest = PrepareRequest(request);

// Шаг 4: Отправить (можно обернуть в Polly retry policy)
var response = await client.SendAsync(httpRequest, ct);

// Шаг 5: Обработать ответ — делегируем специфическую логику
return await HandleResponseAsync<TResponse>(response, ct);
}

// Защищённые абстрактные/виртуальные методы для кастомизации
protected abstract Task<string> AcquireTokenAsync(CancellationToken ct);
protected abstract HttpRequestMessage PrepareRequest<T>(T request);
protected abstract Task<TResponse> HandleResponseAsync<TResponse>(
HttpResponseMessage response, CancellationToken ct);
}

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

public class PayrollIntegrationService : BaseIntegrationService
{
protected override async Task<string> AcquireTokenAsync(CancellationToken ct)
=> await _payrollAuth.GetTokenAsync(ct);

protected override HttpRequestMessage PrepareRequest<Request>(Request request)
=> new(HttpMethod.Post, "/v1/payroll/upload")
{
Content = JsonContent.Create(request, typeof(Request))
};

protected override async Task<PayrollResponse> HandleResponseAsync<PayrollResponse>(
HttpResponseMessage response, CancellationToken ct)
{
// switch по статус-кодам, десериализация и т.д.
}
}

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


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

Внешние системы — по своей природе ненадёжны. Они могут отвечать с задержкой, возвращать ошибки 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 без идемпотентности повтор может привести к дубликатам.

  2. 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 секунд таймаута, то быстро исчерпываются потоки пула и падает вся система.

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

  4. 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("Интеграция недоступна, кэш пуст.");
    });
  5. 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;
    • dotnet user-secrets — для разработки;
    • зашифрованные конфигурационные провайдеры (например, на основе DPAPI или кастомного AES).

В .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 от Сбербанка».