Запросы к веб-сервисам посредством прокси-объектов 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.
При необходимости выполнять действия в расширениях веб-сервисов и других подобных кейсах, регистрируйте зависимости в стандартном контейнере, или же упакуйте код с созданием дополнительного контейнера в синглтон-зависимость.