Перейти к содержанию

Запросы к веб-сервисам посредством прокси-объектов WebProxy

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

Создание прокси-объекта

Для обращения к веб-сервисам из любых приложений .NET (серверные расширения, команды tadmin, автотесты NUnit, расширения desktop-клиентов и др.), создайте класс-наследник WebProxy (далее - прокси-объект), который отправляет HTTP-запросы посредством строготипизированных методов, используемых в других серверных зависимостях. Этот класс не регистрируется в DI-контейнере, его создание выполняется фабрикой IWebProxyFactory.

Tip

Прокси-объект рекомендуется определить в проекте Tessa.Extensions.Shared, чтобы его можно было использовать в любом другом коде расширений .NET в проектном решении.

Если же отправка запросов вызывается, например, только в контроллерах веб-сервиса для обращения к внешним сервисам, то его можно разместить в соответствующем проекте Tessa.Extensions.Server.Web.

using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web; using Tessa.Platform.Runtime; using Tessa.Platform.Web;

// более полный и подробный пример доступен в сборке // Source/Extensions/Tessa.Extensions.Shared/Services/ServiceWebProxy.cs namespace Tessa.Extensions.Shared.Services { public sealed class ServiceWebProxy : WebProxy { public ServiceWebProxy() : base( controllerRoute: "service", serviceName: RuntimeHelper.DefaultServiceName, defaultRequestFlags: new[] { WebRequestFlags.AddAcceptLanguageHeader, WebRequestFlags.AddInstanceInUri }) { }

public async Task<string> GetDataAsync( string parameter, CancellationToken cancellationToken = default) { return await this.SendAsync<string>( HttpMethod.Get, $"data?p={HttpUtility.UrlEncode(parameter)}", WebRequestFlags.PerRequest() + WebRequestFlags.AddSessionHeader, cancellationToken: cancellationToken).ConfigureAwait(false) ?? string.Empty; }

public async Task<string> LoginAsync( IntegrationLoginParameters parameters, CancellationToken cancellationToken = default) { var token = await this.SendAsync<string>( HttpMethod.Post, "login", WebRequestFlags.PerRequest() + WebRequestFlags.JsonRequest, content: parameters, cancellationToken: cancellationToken).ConfigureAwait(false);

return NotNullOrThrow(token); } } }

В базовый конструктор передаются параметры:

  • controllerRoute - путь к вызываемому контроллеру относительно адреса сервиса, который добавляется к нему для каждого вызываемого метода;
  • serviceName - название веб-сервиса, добавляемое к базовому адресу сервиса. По умолчанию базовый адрес определяется из настройки IConnectionSettings.BaseAddress, и равен строке вида https://server.name/tessa. Используйте константу RuntimeHelper.DefaultServiceName, чтобы добавить к нему имя веб-сервиса web. Укажите null, если адрес уже содержит имя сервиса и переопределяется из другой настройки (например, адрес системы, с которой выполняется интеграция);
  • defaultRequestFlags - флаги, определяющие поведение выполняемых запросов, и добавляемые к флагам во всех методах объекта, вызывающих SendAsync.

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

Рассмотрим, что определяет объявленный выше метод GetDataAsync в примере прокси-объекта:

  1. Параметр-тип SendAsync<T> определяет, что возвращаемое значение должно быть десериализовано как строка string.
    • Если объект при успешном вызове сервиса не возвращается (статус-код 204 No Content), или же возвращённый объект требуется отбросить, то передайте специальный тип Void.
  2. Указывается тип вызываемого HTTP-метода как GET (HttpMethod.Get). Именно такой метод должен ожидать контроллер (или сторонний сервис), обращение к которому выполняется. Вы можете указать другие методы: POST (HttpMethod.Post), DELETE (HttpMethod.Delete), PUT (HttpMethod.Put) и др.
  3. Задаётся адрес метода контроллера data и дополнительные query-параметры вида ?param1=value1&param2=value2&.... Если в строку подставляется строковый параметр (а не число или идентификатор), то обязательно необходимо выполнить эскейпинг его значения вызовом HttpUtility.UrlEncode().
  4. Определяются дополнительные флаги для этого метода запроса, которые объединяются с флагами из параметра конструктора defaultRequestFlags.
    • Если флаги не указать, то для этого вызова будут использованы только флаги из defaultRequestFlags.
    • Флаги всегда начинаются с вызова WebRequestFlags.PerRequest(), и далее они добавляются посредством оператора + из класса WebRequestFlags (или аналогичного класса с флагами, который объявлен в вашем решении).
    • Выражения с флагами, возвращаемыми из WebRequestFlags.PerRequest(), нельзя повторно использовать для других вызовов SendAsync, т.к. объект с флагами автоматически возвращается в пул и используются в других вызовах. Поэтому не кэшируйте их в экземплярных или статических переменных (при такой необходимости явно создайте экземпляр класса WebRequestFlagBuilder).

Note

Если прокси-объект объявлен в проекте Tessa.Extensions.Shared, и он может использоваться в desktop-приложениях, то добавляйте .ConfigureAwait(false) ко всем асинхронным вызовам с оператором await.

Результирующий адрес запроса выглядит как GET https://server.name/tessa/web/tessa/service/data?p=123, где:

  • GET - HTTP-метод в соответствии с HttpMethod.Get;
  • https://server.name/tessa - базовый адрес из IConnectionSettings.BaseAddress;
  • web - имя сервиса (serviceName в конструкторе);
  • tessa - имя экземпляра сервиса из IConnectionSettings.InstanceName (обычно соответствует константе RuntimeHelper.DefaultInstanceName; в веб-сервисе web это имя удаляется из пути, поэтому оно не учитывается при роутинге контроллеров);
  • service - имя контроллера (controllerRoute в конструкторе);
  • data - метод контроллера;
  • ?p=123 - параметр, описанный как [FromQuery] в контроллере (в примере значение string parameter равно строке "123").

С запросом передаются HTTP-заголовки Accept-Language с текущим языком интерфейса из LocalizationManager.CurrentUICulture, и Tessa-Session с сериализованным токеном сессии.

Рассмотрим пример ещё одного метода LoginAsync:

  1. Ответ на запрос десериализуется как строка.
  2. Вызывается HTTP-метод POST.
  3. Метод контроллера login.
  4. Запрос в параметре content сериализуется как нетипизированный JSON с указанием mime-типа application/json.
  5. В теле запроса передаётся объект IntegrationLoginParameters. Поскольку он является наследником StorageSerializable, то для него сначала вызывается метод сериализации в Dictionary<string, object?>, а затем выполняется сериализация хеш-таблицы посредством TessaSerializer.Json.

В результате формируется запрос вида POST https://server.name/tessa/web/tessa/service/login с телом запроса вида {"Login":"admin","Password":"passw0rd"} с mime-типом application/json.

С запросом передаётся HTTP-заголовок Accept-Language, но не передаётся заголовок Tessa-Session, поскольку отсутствует соответствующий флаг.

Флаги WebRequestFlags

Рассмотрим флаги из класса WebRequestFlags:

  • JsonRequest - использовать тип данных application/json для сериализации тела запроса из параметра content в текстовом формате Json. При этом остальные параметры игнорируются. Типы данных .NET при передаче будут искажены в соответствии со стандартом Json. Используйте флаг TypedJsonRequest для сохранения типов;
  • TypedJsonRequest - использовать тип данных application/json для сериализации тела запроса из параметра content в текстовом формате Typed Json, который сохраняет типы данных .NET при передаче. При этом остальные параметры игнорируются. Если также указан флаг JsonRequest, то приоритетным является флаг TypedJsonRequest;
  • TypedJsonResponse - обработать ответ от сервиса с типом application/json как Typed Json, который сохраняет типы данных .NET при передаче. Если флаг не указан, то такой ответ обрабатывается как стандартный (нетипизированный) Json;
  • ForceRequestStreaming - указать, что тело запроса передаётся без буферизации как поток. По умолчанию потоковая передача выполняется только при указании типа Stream в качестве параметра-типа запроса WebProxy.SendAsync;
  • AddAcceptLanguageHeader - добавить заголовок запроса HttpRequestHeaders.AcceptLanguage с текущим языком локализации;
  • AddSessionHeader - добавить заголовок запроса с токеном сессии из объекта IWebProxy.SessionTokenHolder, если сессия открыта на клиенте;
  • AddInstanceInUri - добавить имя экземпляра сервера IWebProxy.InstanceName в URI-адрес запроса. Это необходимо для всех запросов к адресам контроллеров TESSA в приложении web, за исключением служебных адресов, таких как /hcheck;
  • Background - указать, что запрос является фоновым. При этом будет передан заголовок запроса, который обеспечивает неизменной дату последней активности сессии. Флаг актуален совместно с передачей токена сессии, в т.ч. посредством флага AddSessionHeader.

Дополнительные возможности метода SendAsync

Рекомендуется реализовать методы прокси-объекта т.о., чтобы каждый метод отправлял какой-либо один запрос к сервису посредством вызова метода SendAsync.

Note

Вызов нескольких методов сервиса и обработку запроса и ответа на запрос в соответствии с бизнес-логикой и другими зависимостями из DI рекомендуется выполнить в классе, который вызывает прокси-объект.

Задача методов прокси-объекта должна быть только в инкапсуляции логики вызова сервисов вместе с сериализацией отправляемых и десериализацией получаемых объектов.

Также в методе SendAsync присутствуют опциональные параметры:

  • prepareContextFuncAsync - действие, подготовливающее контекст IWebProxyContext перед вызовом первых обработчиков. В действии обычно устанавливают функции в контексте, выполняющие фильтрацию обработчиков (ContentHandlerFilterFunc и др.). Выполняется перед обработкой запроса в объектах IWebProxyRequestHandler;
  • modifyRequestFuncAsync - действие, изменяющее запрос к серверу context.Request перед отправкой (где context - параметр действия). В действии обычно устанавливают заголовки сообщения-запроса requestMessage.Headers. Выполняется после обработки запроса в объектах IWebProxyRequestHandler;
  • modifyResponseFuncAsync - действие, изменяющее ответ на запрос к серверу context.Response сразу после того, как он был получен (где context - параметр действия). В действии можно получить заголовки сообщения-ответа responseMessage.Headers. Выполняется перед обработкой ответа на запрос в объектах IWebProxyResponseHandler.

Пример метода, добавляющего заголовок запроса:

public async Task<ContentTokenResponse> GetTokenByJwtAuthAsync( ContentTokenRequest request, string jwt, CancellationToken cancellationToken = default) { var response = await this.SendAsync<ContentTokenResponse>( HttpMethod.Post, "token-by-jwt-auth", WebRequestFlags.PerRequest() + WebRequestFlags.TypedJsonRequest, request, modifyRequestFuncAsync: async ctx => { ctx.Request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt); }, cancellationToken: cancellationToken).ConfigureAwait(false);

return NotNullOrThrow(response); }

Пример метода, получающего содержимое ответа на запрос без десериализации как HttpContent, а затем возвращающего поток с файлом и информацией по mime-типу файла, его имени и размера из HTTP-заголовков.

public async Task<ContentResult> GetContentAsync( string type, Guid contentID, string? token, Guid? userID = null, Func<IWebProxyContext, ValueTask>? modifyRequestFuncAsync = null, CancellationToken cancellationToken = default) { ThrowIfNullOrEmpty(type);

var responseContent = await this.SendAsync<HttpContent>( HttpMethod.Get, GetRoute(type, contentID, token, userID), modifyRequestFuncAsync: modifyRequestFuncAsync, cancellationToken: cancellationToken).ConfigureAwait(false); ThrowIfNull(responseContent);

Stream? stream = null; try { var contentStream = stream = await responseContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var fileName = ContentHelper.TryGetFileName(responseContent.Headers) ?? "file"; var mimeType = responseContent.TryGetContentType() ?? MediaTypeNames.Application.Octet; var contentLength = responseContent.Headers.ContentLength ?? (contentStream.CanSeek ? contentStream.Length : -1L); stream = null;

return new(contentStream, fileName, mimeType, contentLength); } finally { if (stream is not null) { await stream.DisposeAsync().ConfigureAwait(false); } } }

Алгоритм выполнения метода SendAsync

Опишем алгоритм выполнения метода SendAsync, выполняемого при каждой отправке запроса через прокси-объект:

  1. Если свойство content не равно null, то вызываются обработчики IWebProxyContentHandler для подготовки контента в объекте сообщения. Если ни один обработчик не установил свойство context.Request.Content (т.е. оно равно null), то выбрасывается исключение InvalidOperationException.
  2. Вызывается выражение prepareContextFuncAsync из параметра метода SendAsync для подготовки контекста IWebProxyContext перед вызовом первых обработчиков.
  3. Вызываются обработчики IWebProxyRequestHandler для изменения запроса перед отправкой.
  4. Вызывается выражение modifyRequestFuncAsync из параметра метода SendAsync для изменения запроса перед отправкой.
  5. Выполняется запрос по сети через HttpClient (который получен из пула в вызове UseProxyAsync<T>). На этом этапе выбрасывается исключение, только если возникла сетевая ошибка. Если от сервера получен ответ на запрос, то алгоритм читает его заголовки, но не читает содержимое, и продолжает обработку, даже если статус-код HTTP неуспешный.
  6. Вызывается выражение modifyResponseFuncAsync из параметра метода SendAsync для изменения ответа на запрос перед обработкой.
  7. Вызываются обработчики IWebProxyResponseHandler для изменения ответа на запрос перед обработкой.
  8. Если ответ на запрос имеет успешный HTTP-код:
    1. Если метод SendAsync<T> вызван с параметром-типом Void, то метод возвращает null.
    2. Если получен статус-код HTTP 204 No Content, то метод возвращает default(T) (null для ссылочных типов).
    3. Иначе вызываются обработчики IWebProxyContext для десериализации ответа на запрос context.Result. Если десериализация выполнена одним из обработчиков context.ResultIsKnown=true, то возвращается этот результат (даже если он null). В противном случае выбрасывается исключение NotSupportedException.
    4. Если обработчик IWebProxyContext не установил свойство context.SkipResponseDisposal, то объект сообщения HttpResponseMessage освобождается.
  9. Если ответ на запрос имеет неуспешный HTTP-код:
    1. Выполняются обработчики IWebProxyErrorHandler. Обработчик, который распознаёт ошибку как свою, выбрасывает желаемое исключение. Например, если в ответе на запрос пришло значение с mime-типом application/json, то для него десериализуется PlainValidationResult и выбрасывается ValidationException с полученным результатом валидации.
    2. Если ни один обработчик ошибки не выбросил исключение, то выбрасывается HttpRequestException с кодом ошибки, в соответствии с логикой метода HttpResponseMessage.EnsureSuccessStatusCode().
  10. Также в процессе обработки могут быть выброшены исключения:
    1. OperationCanceledException в случае асинхронной отмены по cancellationToken.
    2. TimeoutException при отмене запроса, если наступил клиентский таймаут.
  11. Освобождается объект HttpResponseMessage, если не установлено свойство context.SkipResponseDisposal.
  12. Освобождаются флаги из параметра flags, что возвращает их в пул, если они были получены из пула.

Выполнение запроса через прокси-объект

Для отправки запроса требуется получить настроенный экземпляр прокси-объекта, который ссылается на базовый адрес BaseUri, экземпляр HttpClient, список обработчиков IWebProxyHandlerExecutor и другие настройки. Для этого используйте объект IWebProxyFactory, полученный из DI-контейнера, и вызовите на нём метод UseProxyAsync<T>, передав тип прокси объекта как параметр-тип метода.

Для описанного выше прокси-объекта ServiceWebProxy следующим образом выглядит зависимость для вызова методов, отправляющих запрос:

using System; using System.Threading; using System.Threading.Tasks; using Tessa.Cards; using Tessa.Platform.Web;

namespace Tessa.Extensions.Shared.Services { public sealed class ServiceClient { public ServiceClient(IWebProxyFactory proxies) => this.proxies = NotNullOrThrow(proxies);

private readonly IWebProxyFactory proxies;

public async Task<string> GetDataAsync(string parameter, CancellationToken cancellationToken = default) { var proxy = await this.proxies.UseProxyAsync<ServiceWebProxy>(cancellationToken: cancellationToken).ConfigureAwait(false); await using var _ = proxy.ConfigureAwait(false); return await proxy.GetDataAsync(parameter, cancellationToken).ConfigureAwait(false); }

public async Task<string> LoginAsync(IntegrationLoginParameters parameters, CancellationToken cancellationToken = default) { var proxy = await this.proxies.UseProxyAsync<ServiceWebProxy>(cancellationToken: cancellationToken).ConfigureAwait(false); await using var _ = proxy.ConfigureAwait(false); return await proxy.LoginAsync(parameters, cancellationToken).ConfigureAwait(false); } } }

Note

Если зависимость может вызываться из desktop-клиента или других приложений с установленным SynchronizationContext, то укажите ConfigureAwait(false) для каждого асинхронного вызова с оператором await.

Объект регистрируется в контейнере, и затем используется в других клиентских зависимостях. Чтобы это можно было делать из любых клиентских приложений (команды tadmin, тесты NUnit, desktop-клиенты), объект и его регистрацию разместите в проекте Tessa.Extensions.Shared.

using Tessa.Platform.Runtime; using Unity;

namespace Tessa.Extensions.Shared.Services { [Registrator(Type = SessionType.Client, Tag = RegistratorTag.DefaultForClientAndConsole)] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterSingleton<ServiceClient>(); } } }

Tip

Для абстрагирования логики вызова для класса ServiceClient можно создать и реализовать интерфейс IService с сигнатурами методов GetDataAsync и LoginAsync, и по этому методу выполнить регистрацию.

У метода IWebProxyFactory.UseProxyAsync<T> есть опциональные параметры:

  • httpClient - использовать указанный объект HttpClient для выполнения запросов. Если не задан, то используется объект из пула IHttpClientPool, который указан для этой фабрики;
  • modifyProxyFuncAsync - выражение для изменения свойств прокси-объекта перед тем, как он будет возвращён из метода, но после того, как свойства инициализированы фабрикой IWebProxyFactory. Поскольку возвращённый объект защищается от изменений вызовом Seal(), то только с использованием этого выражения возможно настроить BaseUri и прочие свойства только этого вызова UseProxyAsync.

Фабрика IWebProxyFactory

Фабрика IWebProxyFactory регистрируется в DI-контейнере как синглтон по интерфейсу. Зависимость по умолчанию предназначена для выполнения запросов к контроллерам сервиса web, а также её возможно использовать для обращения к другим веб-приложениям (таким, как микросервис jinni).

В проектном решении допустимо создать фабрику для обращения к другим веб-приложениям (например, интеграционным сервисам REST API):

  • класс фабрики необходимо унаследовать от WebProxyFactoryBase;
  • получить через конструктор пул объектов HttpClient - IHttpClientPool, зависимости фабрики IWebProxyFactoryDependencies и объект для регистрации освобождения IUnityDisposableContainer, эти параметры возможно получить из DI или переопределить;
  • далее объект регистрируется по интерфейсу с определённым именем, по которому доступно его получение в классах сервисов.

Рассмотрим пример фабрики WebbiWebProxyFactory для обращения к веб-приложению webbi:

using System; using System.Threading; using System.Threading.Tasks; using Tessa.Platform; using Tessa.Platform.Web; using Unity;

namespace Tessa.Webbi { public class WebbiWebProxyFactory : WebProxyFactoryBase { public WebbiWebProxyFactory( IWebbiConnectionSettings webbiConnectionSettings, [OptionalDependency] IWebProxyFactoryDependencies? dependencies = null, [OptionalDependency] IUnityDisposableContainer? unityDisposableContainer = null) : base(new HttpClientPool(NotNullOrThrow(webbiConnectionSettings)), dependencies) { this.DisposeHttpClientPool = true; this.WebbiConnectionSettings = NotNullOrThrow(webbiConnectionSettings); unityDisposableContainer?.Register(this); }

protected IWebbiConnectionSettings WebbiConnectionSettings { get; }

protected override ValueTask InitializeProxyParametersAsync(IWebProxy proxy, CancellationToken cancellationToken = default) { proxy.BaseUri = new Uri(this.WebbiConnectionSettings.BaseAddress);

if (proxy is IWebbiWebProxy webbiProxy) { webbiProxy.ManagementRoute = this.WebbiConnectionSettings.ManagementRoute; }

return base.InitializeProxyParametersAsync(proxy, cancellationToken); } } }

Здесь в конструкторе:

  • принимается дополнительная зависимость из DI IWebbiConnectionSettings с настройками подключения к сервису webbi. Этот интерфейс наследуется от IHttpClientCreationOptions, т.е. он содержит настройки объектов HttpClient (таймаут запросов и прокси System.Net.IWebProxy);
  • создаётся пул HttpClientPool с настройками из webbiConnectionSettings, который передаётся в базовый объект, и устанавливается DisposeHttpClientPool = true, чтобы пул был освобождён при освобождении WebbiWebProxyFactory;
  • зависимость IWebProxyFactoryDependencies запрашивается из Unity и передаётся в базовый объект без изменений;
  • зависимость IUnityDisposableContainer запрашивается из Unity для регистрации текущего объекта WebbiWebProxyFactory, чтобы его метод DisposeAsync (объявленный в базовом классе) был вызван при освобождении контейнера (стандартный способ освобождения зависимостей-синглтонов).

Далее метод InitializeProxyParametersAsync определяет, какая настройка прокси-объекта выполняется в методе UseProxyAsync<T>:

  • устанавливается базовый адрес BaseUri веб-приложения webbi;
  • определяется дополнительное свойство прокси-объекта ManagementRoute.

Регистрация выполняется по интерфейсу IWebProxyFactory и имени "WebbiWebProxyFactory":

unityContainer .RegisterSingletonWithClass<IWebProxyFactory, WebbiWebProxyFactory>(nameof(WebbiWebProxyFactory));

Далее получить объект возможно в параметре конструктора с атрибутом:

[Dependency(nameof(WebbiWebProxyFactory))] IWebProxyFactory webbiProxyFactory

Обработчики прокси-объектов

Обработчики прокси-объектов - это классы, реализующие один из интерфейсов обработчиков, которые регистрируются в DI-контейнере Unity как синглтоны по интерфейсу и уникальному имени, а затем методы всех зарегистрированных обработчиков вызываются в процессе выполнения метода IWebProxy.SendAsync<T>. Каждый метод получает объект контекста IWebProxyContext с информацией о контексте производимого действия.

Перечислим типы интерфейсов и методы обработчиков:

  1. IWebProxyContentHandler.SetupContentAsync - устанавливает содержимое запроса HttpRequestMessage.Content перед отправкой на сервер в соответствии с объектом ContentValue (он передан в параметре content метода SendAsync);
  2. IWebProxyRequestHandler.ModifyRequestAsync - изменяет запрос HttpRequestMessage перед отправкой (например, добавляет заголовки запроса);
  3. IWebProxyResponseHandler.ModifyResponseAsync - изменяет ответ на запрос HttpResponseMessage перед его дальнейшей обработкой (независимо от успешности запроса);
  4. IWebProxyResultHandler.SetupResultAsync - читает тело успешно выполненного запроса в соответствии с mime-типом содержимого (ResponseContentType в контексте), типом ожидаемого результата (свойство Type в контексте соответствует типу typeof(T) для метода SendAsync<T>) и другими параметрами, и устанавливает результат вызова метода SendAsync в свойстве контекста Result;
  5. IWebProxyErrorHandler.HandleErrorAsync - выбрасывает исключение с определённой информацией для неуспешно выполненного запроса (статус-код HTTP >= 300).

При регистрации каждого обработчика укажите на классе атрибут [Order(123)], где 123 - число, определяющее порядок выполнения этого обработчика по отношению к другим зарегистрированным обработчикам. Порядок типовых обработчиков начинается с 1, для выполнения после них укажите 100 и более, для выполнения перед ними - 0 или отрицательное значение.


Пример обработчика IWebProxyContentHandler для передачи параметра content метода SendAsync в виде строки с mime-типом plain/text:

using System.Net.Http; using System.Threading.Tasks; using Tessa.Platform.Runtime;

namespace Tessa.Platform.Web { [Order(100)] public class TextWebProxyContentHandler : WebProxyContentHandlerBase { public override async ValueTask SetupContentAsync(IWebProxyContext context) { if (context is { ContentValue: string contentValue, Request.Content: null }) { context.Request.Content = TessaHttpContent.FromText(contentValue); } } } }

Его регистрация:

unityContainer .RegisterSingleton<IWebProxyContentHandler, TextWebProxyContentHandler>(nameof(TextWebProxyContentHandler));


Пример обработчика IWebProxyRequestHandler для добавления заголовка запроса Accept-Language с текущим языком при наличии флага запроса:

using System.Net.Http.Headers; using System.Threading.Tasks; using Tessa.Localization;

namespace Tessa.Platform.Web { [Order(100)] public class AcceptLanguageWebProxyRequestHandler : WebProxyRequestHandlerBase { public override async ValueTask ModifyRequestAsync(IWebProxyContext context) { if (context.Has(WebRequestFlags.AddAcceptLanguageHeader)) { context.Request.Headers.AcceptLanguage.Add(new(LocalizationManager.CurrentUICulture.Name)); } } } }

Его регистрация:

unityContainer .RegisterSingleton<IWebProxyRequestHandler, AcceptLanguageWebProxyRequestHandler>(nameof(AcceptLanguageWebProxyRequestHandler));


Пример обработчика IWebProxyResultHandler, который возвращает поток при вызове SendAsync<Stream>, при этом освобождение содержимого HttpResponseMessage.Content предотвращается:

using System.IO; using System.Threading.Tasks;

namespace Tessa.Platform.Web { [Order(-1)] public class StreamWebProxyResultHandler : WebProxyResultHandlerBase { public override async ValueTask SetupResultAsync(IWebProxyContext context) { if (!context.ResultIsKnown && context.ResultType == typeof(Stream)) { context.SkipResponseDisposal = true; context.Result = await context.Response!.Content.ReadAsStreamAsync(context.CancellationToken).ConfigureAwait(false); } } } }

Его регистрация:

unityContainer .RegisterSingleton<IWebProxyResultHandler, StreamWebProxyResultHandler>(nameof(StreamWebProxyResultHandler));


Пример обработчика ошибок IWebProxyErrorHandler, который при получении сообщения с заголовком режима тех. обслуживания выбрасывает специализированное исключение:

using System.Net; using System.Net.Mime; using System.Threading.Tasks; using Tessa.Platform.Runtime; using Tessa.Platform.Validation;

namespace Tessa.Platform.Web { [Order(-1)] public class MaintenanceWebProxyErrorHandler : WebProxyErrorHandlerBase { public override async ValueTask HandleErrorAsync(IWebProxyContext context) { if (context.ResponseContentType == MediaTypeNames.Text.Html && context.Response!.StatusCode == HttpStatusCode.ServiceUnavailable // 503 && context.Response.Headers.Contains(SessionHttpRequestHeader.Maintenance)) { await context.Response!.Content.ReadAsStringAsync(context.CancellationToken).ConfigureAwait(false);

var result = ValidationSequence .Begin() .SetObjectName(context.WebProxy) .ErrorDetails( ValidationKeys.Maintenance, ValidationKeys.Maintenance.Message ?? "System is in the maintenance mode", context.WebProxy.GetRequestUri(null, context.Has(WebRequestFlags.AddInstanceInUri)).ToString()) .End() .Build();

throw new ValidationException(result) { StatusCode = context.Response.StatusCode, BaseUri = context.WebProxy.BaseUri }; } } } }

Его регистрация:

unityContainer .RegisterSingleton<IWebProxyErrorHandler, MaintenanceWebProxyErrorHandler>(nameof(MaintenanceWebProxyErrorHandler));

Создание DI-контейнера для использования прокси-объектов

Прокси-объекты WebProxy опираются на DI-зависимости, в т.ч. на различные обработчики IWebProxyXyzHandler, зависимости IHttpClientPool, IWebProxyFactory, IConnectionSettings и др.

Рассмотрим, как выглядит создание минимального контейнера Unity, в котором регистрируется новый обработчик в дополнение к типовым RegisterWebDefaultHandlers, и выполняется запрос через прокси-объект CheckWebProxy.

var unityContainer = new UnityContainer() .RegisterPlatformSharedDependencies() .RegisterConnectionSettingsFromConfiguration() .RegisterWeb() .RegisterWebDefaultHandlers();

if (checkHealth) { unityContainer .RegisterSingleton<IWebProxyErrorHandler, HealthCheckWebProxyEventHandler>(nameof(HealthCheckWebProxyEventHandler)); }

try { var proxies = unityContainer.Resolve<IWebProxyFactory>(); await using var proxy = await proxies.UseProxyAsync<CheckWebProxy>();

string? result = await proxy.GetAsync(checkHealth); return result; } finally { if (unityContainer.TryResolve<IUnityDisposableContainer>() is { } disposableContainer) { await disposableContainer.DisposeAllAsync(); } }

Important

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

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

Back to top