/
Kovalenko
/
TODO
Обзор
Документация
Войти
/
Kovalenko
/
TODO
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
TaskApi/Controllers/ApiHelpers.cs
217 строк
7 KB
Kovalenko1
pag
07 мар 2026, 13:46
07 мар 2026, 13:46
18fca63
Код
Авторство
О чём код?
using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using TaskApi.Data; using TaskApi.Dtos.V1; using TaskApi.Dtos.V2; using TaskApi.Models; namespace TaskApi.Controllers; internal static class ApiHelpers { internal sealed record TaskField(string Name, Func<TaskItem, object?> Selector); public const int DefaultPageSize = 20; public const int MaxPageSize = 100; public static readonly IReadOnlyDictionary<string, TaskField> TaskFieldsV1 = new Dictionary<string, TaskField>(StringComparer.OrdinalIgnoreCase) { ["id"] = new TaskField("id", task => task.Id), ["title"] = new TaskField("title", task => task.Title), ["description"] = new TaskField("description", task => task.Description), ["status"] = new TaskField("status", task => task.Status), ["dueDate"] = new TaskField("dueDate", task => task.DueDate), ["createdAt"] = new TaskField("createdAt", task => task.CreatedAt), ["updatedAt"] = new TaskField("updatedAt", task => task.UpdatedAt) }; public static readonly IReadOnlyDictionary<string, TaskField> TaskFieldsV2 = new Dictionary<string, TaskField>(StringComparer.OrdinalIgnoreCase) { ["id"] = new TaskField("id", task => task.Id), ["title"] = new TaskField("title", task => task.Title), ["description"] = new TaskField("description", task => task.Description), ["status"] = new TaskField("status", task => task.Status), ["dueDate"] = new TaskField("dueDate", task => task.DueDate), ["assignee"] = new TaskField("assignee", task => task.Assignee), ["priority"] = new TaskField("priority", task => task.Priority), ["createdAt"] = new TaskField("createdAt", task => task.CreatedAt), ["updatedAt"] = new TaskField("updatedAt", task => task.UpdatedAt) }; public static Guid GetUserId(ClaimsPrincipal user) { var value = user.FindFirstValue(ClaimTypes.NameIdentifier); return Guid.TryParse(value, out var id) ? id : throw new InvalidOperationException("Invalid user id."); } public static string? NormalizeRequired(string? value) { var trimmed = value?.Trim(); return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed; } public static string? NormalizeOptional(string? value) { var trimmed = value?.Trim(); return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed; } public static string ComputeRequestHash<T>(T payload) { var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); using var sha = SHA256.Create(); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(json)); return Convert.ToHexString(bytes); } public static async Task<(TaskItem? Task, string? Error)> TryGetIdempotentTaskAsync( AppDbContext db, Guid userId, string idempotencyKey, string requestHash) { var existingKey = await db.IdempotencyKeys.AsNoTracking() .FirstOrDefaultAsync(key => key.UserId == userId && key.Key == idempotencyKey); if (existingKey is null) { return (null, null); } if (!string.Equals(existingKey.RequestHash, requestHash, StringComparison.Ordinal)) { return (null, "Idempotency key already used with a different payload."); } var existingTask = await db.Tasks.AsNoTracking() .FirstOrDefaultAsync(task => task.UserId == userId && task.Id == existingKey.TaskId); if (existingTask is null) { return (null, "Idempotency key already used."); } return (existingTask, null); } public static IReadOnlyList<TaskField>? ParseIncludeFields( string? include, IReadOnlyDictionary<string, TaskField> fieldMap, out string? error) { error = null; if (string.IsNullOrWhiteSpace(include)) { return null; } var parts = include.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (parts.Length == 0) { return null; } var selected = new List<TaskField>(parts.Length); var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (var part in parts) { if (!fieldMap.TryGetValue(part, out var field)) { error = $"Unknown include field '{part}'."; return null; } if (seen.Add(field.Name)) { selected.Add(field); } } return selected; } public static IDictionary<string, object?> ShapeTask(TaskItem task, IReadOnlyList<TaskField> fields) { var shaped = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase); foreach (var field in fields) { shaped[field.Name] = field.Selector(task); } return shaped; } public static bool TryGetPagination( int? page, int? pageSize, out int resolvedPage, out int resolvedPageSize, out string? error) { error = null; resolvedPage = page ?? 1; resolvedPageSize = pageSize ?? DefaultPageSize; if (resolvedPage < 1) { error = "Page must be greater than zero."; return false; } if (resolvedPageSize < 1 || resolvedPageSize > MaxPageSize) { error = $"PageSize must be between 1 and {MaxPageSize}."; return false; } return true; } public static int GetTotalPages(int totalCount, int pageSize) { if (pageSize <= 0) { return 0; } return (int)Math.Ceiling(totalCount / (double)pageSize); } public static TaskResponseV1 MapTaskV1(TaskItem task) => new() { Id = task.Id, Title = task.Title, Description = task.Description, Status = task.Status, DueDate = task.DueDate, CreatedAt = task.CreatedAt, UpdatedAt = task.UpdatedAt }; public static TaskResponseV2 MapTaskV2(TaskItem task) => new() { Id = task.Id, Title = task.Title, Description = task.Description, Status = task.Status, DueDate = task.DueDate, Assignee = task.Assignee, Priority = task.Priority, CreatedAt = task.CreatedAt, UpdatedAt = task.UpdatedAt }; }