VEU.WathBody/3-app/1-lib/Kit.Helpers/Http/FluentHttpClient.cs

150 lines
7.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Kit.Helpers.JsonResult;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Kit.Helpers
{
/// <summary>
/// Тонкий fluent-builder HTTP-вызова поверх <see cref="HttpHelper"/> — порт
/// <c>RiskProf.Helpers.FluentHttpClient</c>. Стиль вызова:
/// <c>uri.PrepareHttpPost().WithSecurityKey(..).WithParameter(..)
/// .ExecuteRequestJsonResultModel().JsonDeserialize&lt;T&gt;()</c>.
/// <para>
/// POST-параметры (<see cref="WithParameter"/>) уходят JSON-объектом (контроллеры на <c>[FromBody]</c>).
/// Два способа получить ответ:
/// <list type="bullet">
/// <item><see cref="ExecuteRequestJsonResultModel"/> — разворачивает конверт
/// <see cref="JsonResultModel{T}"/>: <c>Header.IsSuccess=false</c> → исключение с <c>Header.Message</c>,
/// иначе наружу отдаётся JSON поля <c>Result</c>.</item>
/// <item><see cref="ExecuteRequest"/> — отдаёт СЫРОЕ тело без разворота конверта.</item>
/// </list>
/// </para>
/// </summary>
public class FluentHttpClient
{
private readonly Uri _requestUrl;
private readonly HttpMethod _httpMethod;
private readonly List<KeyValuePair<string, string>> _parameters = new();
private string _securityKey;
private string _jsonContent;
public FluentHttpClient(Uri requestUrl, HttpMethod httpMethod)
{
_requestUrl = requestUrl;
if (httpMethod != HttpMethod.Get && httpMethod != HttpMethod.Post)
throw new ArgumentOutOfRangeException(nameof(httpMethod), "only POST and GET methods supported");
_httpMethod = httpMethod;
}
/// <summary>Ключ межсервисного доступа (заголовок <c>X-Requested-Token</c>).</summary>
public FluentHttpClient WithSecurityKey(string securityKey)
{
_securityKey = securityKey;
return this;
}
/// <summary>Параметр запроса: для GET — query-строка, для POST — поле JSON-тела.</summary>
public FluentHttpClient WithParameter<TValue>(string name, TValue value)
{
_parameters.Add(new KeyValuePair<string, string>(name, value?.ToString() ?? string.Empty));
return this;
}
/// <summary>Готовое JSON-тело POST (приоритетнее набора <see cref="WithParameter"/>).</summary>
public FluentHttpClient WithJsonContent<TObject>(TObject obj)
{
if (_httpMethod != HttpMethod.Post)
throw new ArgumentOutOfRangeException(nameof(obj), "JSON content only supported for POST");
_jsonContent = obj.JsonSerialize();
return this;
}
/// <summary>Отправить запрос и вернуть JSON поля <c>Result</c> развёрнутого конверта
/// <see cref="JsonResultModel{T}"/> (<c>Header.IsSuccess=false</c> → исключение). Для эндпоинтов,
/// помеченных <c>JsonResultModelWrapFilter</c> (отдают конверт).</summary>
public Task<string> ExecuteRequestJsonResultModel()
{
return HttpHelper.ExecuteRequest(BuildRequest(), Unwrap, errorHandler: null, securityKey: _securityKey);
}
/// <summary>Отправить запрос и вернуть СЫРОЕ тело ответа БЕЗ разворота конверта. Для эндпоинтов,
/// которые не проходят через <c>JsonResultModelWrapFilter</c> и отдают значение напрямую (без
/// <see cref="JsonResultModel{T}"/>). Бизнес-ошибки тут не распознаются — на не-2xx
/// <see cref="HttpHelper"/> бросит <c>HttpRequestException</c>.</summary>
public Task<string> ExecuteRequest()
{
return HttpHelper.ExecuteRequest(BuildRequest(), responseHandler: null, errorHandler: null, securityKey: _securityKey);
}
// Сборка HTTP-запроса (общая для обёрнутого и сырого вариантов).
private Func<HttpClient, Task<HttpResponseMessage>> BuildRequest()
{
if (_httpMethod == HttpMethod.Get)
{
Uri uri = AppendQuery(_requestUrl, _parameters);
return client => client.GetAsync(uri);
}
string json = _jsonContent ?? ParametersToJson(_parameters);
return client => client.PostAsync(_requestUrl,
new StringContent(json, Encoding.UTF8, "application/json"));
}
// ---- разворот конверта JsonResultModel: Header.IsSuccess=false → исключение, иначе поле Body ----
private static Task<string> Unwrap(HttpResponseMessage response, string body)
{
JsonResultModel<JToken> envelope = body.JsonDeserialize<JsonResultModel<JToken>>();
JsonResultHeader header = envelope?.Header;
if (header == null || !header.IsSuccess)
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(header?.Message) ? "сервис вернул ошибку" : header.Message);
// SerializeObject возвращает валидный JSON и для примитивов ("token"/true/5), и для объектов.
return Task.FromResult(envelope.Body == null ? "null" : JsonConvert.SerializeObject(envelope.Body));
}
// POST-параметры → JSON-объект (контроллеры принимают [FromBody]).
private static string ParametersToJson(IEnumerable<KeyValuePair<string, string>> parameters)
{
var obj = new JObject();
foreach (KeyValuePair<string, string> p in parameters) obj[p.Key] = p.Value;
return obj.ToString(Formatting.None);
}
private static Uri AppendQuery(Uri uri, List<KeyValuePair<string, string>> parameters)
{
if (parameters.Count == 0) return uri;
string query = string.Join("&", parameters.Select(p =>
$"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value ?? string.Empty)}"));
var builder = new UriBuilder(uri);
builder.Query = string.IsNullOrEmpty(builder.Query)
? query
: builder.Query.TrimStart('?') + "&" + query;
return builder.Uri;
}
}
/// <summary>Точки входа в fluent-цепочку (как <c>RiskProf.Helpers.FluentHttpClient_Extensions</c>).</summary>
public static class FluentHttpClientExtensions
{
public static FluentHttpClient PrepareHttpGet(this Uri requestUri)
{
return new FluentHttpClient(requestUri, HttpMethod.Get);
}
public static FluentHttpClient PrepareHttpPost(this Uri requestUri)
{
return new FluentHttpClient(requestUri, HttpMethod.Post);
}
}
}