/
dOJer113
/
Insect
Обзор
Документация
Войти
/
dOJer113
/
Insect
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
server
UI/Services/Api/InsectApiClient.cs
280 строк
9 KB
alex
исправлена валидация
23 дек 2025, 16:14
23 дек 2025, 16:14
e46a3db
Код
Авторство
О чём код?
using System.Net; using System.Net.Http; using System.Text; using System.Text.Json; using insects; using OpenApiValidator; namespace UI.Services.Api; /// <summary> /// HTTP-клиент для работы с REST API насекомых с валидацией OpenAPI. /// </summary> public sealed class InsectApiClient { private const string ApiPrefix = "/v1"; private readonly HttpClient _client; private readonly OpenApiSchemaValidator _validator; private readonly JsonSerializerOptions _jsonOptions; public InsectApiClient(string baseAddress, string schemaPath) { var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, UseProxy = false }; var normalizedBase = baseAddress.EndsWith("/", StringComparison.Ordinal) ? baseAddress : $"{baseAddress}/"; _client = new HttpClient(handler) { BaseAddress = new Uri(normalizedBase, UriKind.Absolute), Timeout = TimeSpan.FromSeconds(5) }; _validator = new OpenApiSchemaValidator(schemaPath); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; _jsonOptions.Converters.Add(new InsectJsonConverter()); } public async Task<ApiResult<IReadOnlyList<Insect>>> GetInsectsAsync(CancellationToken cancellationToken) { try { const string schemaPath = "/insects/list"; var response = await _client.GetAsync($"{ApiPrefix}{schemaPath}", cancellationToken); var content = await response.Content.ReadAsStringAsync(); var statusCode = ((int)response.StatusCode).ToString(); if (string.IsNullOrWhiteSpace(content)) { return ApiResult<IReadOnlyList<Insect>>.Fail($"Пустой ответ ({response.StatusCode})"); } var validation = _validator.ValidateResponse("GetInsectList", "get", schemaPath, content, statusCode); if (!validation.IsValid) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] WARN: /insects/list не прошло валидацию: {validation.ErrorMessage}"); } if (!response.IsSuccessStatusCode) { var errorMessage = GetErrorFromContent(content); if (errorMessage == null && response.StatusCode == HttpStatusCode.InternalServerError) { errorMessage = "Файл данных на сервере поврежден или недоступен"; } errorMessage ??= $"Ошибка при получении списка: {response.StatusCode}"; return ApiResult<IReadOnlyList<Insect>>.Fail(errorMessage); } var envelope = JsonSerializer.Deserialize<InsectListEnvelope>(content, _jsonOptions); if (envelope?.Insects is { } insects) { return ApiResult<IReadOnlyList<Insect>>.Success(insects); } return ApiResult<IReadOnlyList<Insect>>.Fail("Некорректный ответ сервера"); } catch (Exception ex) { return ApiResult<IReadOnlyList<Insect>>.Fail(FormatException(ex)); } } public async Task<ApiResult<Insect>> CreateAsync(Insect insect, CancellationToken cancellationToken) { const string schemaPath = "/insects"; const string operationId = "CreateInsect"; var payload = JsonSerializer.Serialize(insect, insect.GetType(), _jsonOptions); try { var response = await _client.PostAsync($"{ApiPrefix}{schemaPath}", CreateContent(payload), cancellationToken); var content = await response.Content.ReadAsStringAsync(); var statusCode = ((int)response.StatusCode).ToString(); if (!string.IsNullOrWhiteSpace(content)) { var responseValidation = _validator.ValidateResponse(operationId, "post", schemaPath, content, statusCode); if (!responseValidation.IsValid) { return ApiResult<Insect>.Fail($"Ответ не прошел валидацию: {responseValidation.ErrorMessage}"); } } if (!response.IsSuccessStatusCode) { var err = GetErrorFromContent(content); if (err == null && response.StatusCode == HttpStatusCode.InternalServerError) { err = "Файл данных на сервере поврежден или недоступен"; } return ApiResult<Insect>.Fail(err ?? $"Ошибка создания: {response.StatusCode}"); } if (!string.IsNullOrWhiteSpace(content)) { var envelope = JsonSerializer.Deserialize<CreatedEnvelope>(content, _jsonOptions); if (envelope?.Id > 0) { insect.Id = envelope.Id; } } return ApiResult<Insect>.Success(insect); } catch (Exception ex) { return ApiResult<Insect>.Fail(FormatException(ex)); } } public async Task<ApiResult<Insect>> UpdateAsync(Insect insect, CancellationToken cancellationToken) { const string schemaTemplate = "/insects/{id}"; const string operationId = "UpdateInsect"; var httpPath = $"{ApiPrefix}/insects/{insect.Id}"; var payload = JsonSerializer.Serialize(insect, insect.GetType(), _jsonOptions); try { var response = await _client.PutAsync(httpPath, CreateContent(payload), cancellationToken); var content = await response.Content.ReadAsStringAsync(); var statusCode = ((int)response.StatusCode).ToString(); if (!string.IsNullOrWhiteSpace(content)) { var responseValidation = _validator.ValidateResponse(operationId, "put", schemaTemplate, content, statusCode); if (!responseValidation.IsValid) { return ApiResult<Insect>.Fail($"Ответ не прошел валидацию: {responseValidation.ErrorMessage}"); } } if (!response.IsSuccessStatusCode) { var err = GetErrorFromContent(content); if (err == null && response.StatusCode == HttpStatusCode.InternalServerError) { err = "Файл данных на сервере поврежден или недоступен"; } return ApiResult<Insect>.Fail(err ?? $"Ошибка обновления: {response.StatusCode}"); } return ApiResult<Insect>.Success(insect); } catch (Exception ex) { return ApiResult<Insect>.Fail(FormatException(ex)); } } public async Task<ApiResult<bool>> DeleteAsync(Insect insect, CancellationToken cancellationToken) { const string schemaTemplate = "/insects/{id}"; const string operationId = "DeleteInsect"; var httpPath = $"{ApiPrefix}/insects/{insect.Id}"; try { var response = await _client.DeleteAsync(httpPath, cancellationToken); var content = await response.Content.ReadAsStringAsync(); var statusCode = ((int)response.StatusCode).ToString(); if (!string.IsNullOrWhiteSpace(content)) { var responseValidation = _validator.ValidateResponse(operationId, "delete", schemaTemplate, content, statusCode); if (!responseValidation.IsValid) { return ApiResult<bool>.Fail($"Ответ не прошел валидацию: {responseValidation.ErrorMessage}"); } } if (!response.IsSuccessStatusCode) { var err = GetErrorFromContent(content); if (err == null && response.StatusCode == HttpStatusCode.InternalServerError) { err = "Файл данных на сервере поврежден или недоступен"; } return ApiResult<bool>.Fail(err ?? $"Ошибка удаления: {response.StatusCode}"); } return ApiResult<bool>.Success(true); } catch (Exception ex) { return ApiResult<bool>.Fail(FormatException(ex)); } } private static StringContent CreateContent(string payload) => new(payload, Encoding.UTF8, "application/json"); private string? GetErrorFromContent(string content) { if (string.IsNullOrWhiteSpace(content)) { return null; } try { var document = JsonSerializer.Deserialize<ErrorEnvelope>(content, _jsonOptions); if (document?.Errors?.Length > 0) { return string.Join(", ", document.Errors); } return document?.Status?.Description; } catch { return null; } } private static string FormatException(Exception ex) { if (ex is TaskCanceledException) { return "Истек тайм-аут выполнения запроса"; } if (ex is HttpRequestException) { return $"Проблема с HTTP-запросом: {ex.Message}"; } return ex.InnerException is null ? ex.Message : $"{ex.Message} ({ex.InnerException.Message})"; } private sealed record StatusEnvelope(int Code, string Description); private sealed record InsectListEnvelope { public StatusEnvelope? Status { get; init; } public List<Insect>? Insects { get; init; } } private sealed record CreatedEnvelope { public StatusEnvelope? Status { get; init; } public int Id { get; init; } } private sealed record ErrorEnvelope { public StatusEnvelope? Status { get; init; } public string[]? Errors { get; init; } } }