Запросы к веб-сервисам посредством прокси-объектов 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
в примере прокси-объекта:
- Параметр-тип
SendAsync<T>
определяет, что возвращаемое значение должно быть десериализовано как строкаstring
.- Если объект при успешном вызове сервиса не возвращается (статус-код
204 No Content
), или же возвращённый объект требуется отбросить, то передайте специальный типVoid
.
- Если объект при успешном вызове сервиса не возвращается (статус-код
- Указывается тип вызываемого HTTP-метода как
GET
(HttpMethod.Get
). Именно такой метод должен ожидать контроллер (или сторонний сервис), обращение к которому выполняется. Вы можете указать другие методы:POST
(HttpMethod.Post
),DELETE
(HttpMethod.Delete
),PUT
(HttpMethod.Put
) и др. - Задаётся адрес метода контроллера
data
и дополнительные query-параметры вида?param1=value1¶m2=value2&...
. Если в строку подставляется строковый параметр (а не число или идентификатор), то обязательно необходимо выполнить эскейпинг его значения вызовомHttpUtility.UrlEncode()
. - Определяются дополнительные флаги для этого метода запроса, которые объединяются с флагами из параметра конструктора
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
:
- Ответ на запрос десериализуется как строка.
- Вызывается HTTP-метод
POST
. - Метод контроллера
login
. - Запрос в параметре
content
сериализуется как нетипизированный JSON с указанием mime-типаapplication/json
. - В теле запроса передаётся объект
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
, выполняемого при каждой отправке запроса через прокси-объект:
- Если свойство
content
не равноnull
, то вызываются обработчикиIWebProxyContentHandler
для подготовки контента в объекте сообщения. Если ни один обработчик не установил свойствоcontext.Request.Content
(т.е. оно равноnull
), то выбрасывается исключениеInvalidOperationException
. - Вызывается выражение
prepareContextFuncAsync
из параметра методаSendAsync
для подготовки контекстаIWebProxyContext
перед вызовом первых обработчиков. - Вызываются обработчики
IWebProxyRequestHandler
для изменения запроса перед отправкой. - Вызывается выражение
modifyRequestFuncAsync
из параметра методаSendAsync
для изменения запроса перед отправкой. - Выполняется запрос по сети через
HttpClient
(который получен из пула в вызовеUseProxyAsync<T>
). На этом этапе выбрасывается исключение, только если возникла сетевая ошибка. Если от сервера получен ответ на запрос, то алгоритм читает его заголовки, но не читает содержимое, и продолжает обработку, даже если статус-код HTTP неуспешный. - Вызывается выражение
modifyResponseFuncAsync
из параметра методаSendAsync
для изменения ответа на запрос перед обработкой. - Вызываются обработчики
IWebProxyResponseHandler
для изменения ответа на запрос перед обработкой. - Если ответ на запрос имеет успешный HTTP-код:
- Если метод
SendAsync<T>
вызван с параметром-типомVoid
, то метод возвращаетnull
. - Если получен статус-код HTTP
204 No Content
, то метод возвращаетdefault(T)
(null
для ссылочных типов). - Иначе вызываются обработчики
IWebProxyContext
для десериализации ответа на запросcontext.Result
. Если десериализация выполнена одним из обработчиковcontext.ResultIsKnown=true
, то возвращается этот результат (даже если онnull
). В противном случае выбрасывается исключениеNotSupportedException
. - Если обработчик
IWebProxyContext
не установил свойствоcontext.SkipResponseDisposal
, то объект сообщенияHttpResponseMessage
освобождается.
- Если метод
- Если ответ на запрос имеет неуспешный HTTP-код:
- Выполняются обработчики
IWebProxyErrorHandler
. Обработчик, который распознаёт ошибку как свою, выбрасывает желаемое исключение. Например, если в ответе на запрос пришло значение с mime-типомapplication/json
, то для него десериализуетсяPlainValidationResult
и выбрасываетсяValidationException
с полученным результатом валидации. - Если ни один обработчик ошибки не выбросил исключение, то выбрасывается
HttpRequestException
с кодом ошибки, в соответствии с логикой методаHttpResponseMessage.EnsureSuccessStatusCode()
.
- Выполняются обработчики
- Также в процессе обработки могут быть выброшены исключения:
OperationCanceledException
в случае асинхронной отмены поcancellationToken
.TimeoutException
при отмене запроса, если наступил клиентский таймаут.
- Освобождается объект
HttpResponseMessage
, если не установлено свойствоcontext.SkipResponseDisposal
. - Освобождаются флаги из параметра
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
с информацией о контексте производимого действия.
Перечислим типы интерфейсов и методы обработчиков:
IWebProxyContentHandler.SetupContentAsync
- устанавливает содержимое запросаHttpRequestMessage.Content
перед отправкой на сервер в соответствии с объектомContentValue
(он передан в параметреcontent
методаSendAsync
);IWebProxyRequestHandler.ModifyRequestAsync
- изменяет запросHttpRequestMessage
перед отправкой (например, добавляет заголовки запроса);IWebProxyResponseHandler.ModifyResponseAsync
- изменяет ответ на запросHttpResponseMessage
перед его дальнейшей обработкой (независимо от успешности запроса);IWebProxyResultHandler.SetupResultAsync
- читает тело успешно выполненного запроса в соответствии с mime-типом содержимого (ResponseContentType
в контексте), типом ожидаемого результата (свойствоType
в контексте соответствует типуtypeof(T)
для методаSendAsync<T>
) и другими параметрами, и устанавливает результат вызова методаSendAsync
в свойстве контекстаResult
;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.
При необходимости выполнять действия в расширениях веб-сервисов и других подобных кейсах, регистрируйте зависимости в стандартном контейнере, или же упакуйте код с созданием дополнительного контейнера в синглтон-зависимость.