Отдел разработки – Telegram
Отдел разработки
69 subscribers
4 photos
3 links
Заметки о разработке от команды онлайн школы программирования IRON PROGRAMMER
Download Telegram
О - #оптимизация

Этой теме посвятим еще много постов, потому что тема важна, нужная, актуальная и иногда больная ))
Предыстория:
Когда пишется проект с нуля и нужно сделать что-то уже вчера, порой реализуются самые простые и неоптимальные решения, которые просто должны выполнять свою функцию. Со временем, проект разрастается и такие вот "что-то" начинают вылезать и мешать.
Проблема:
Был такой метод:
public async Task<List<long>> GetMissingIdsAsync(bool useStartDate)
{
await using var context = await _contextFactory.CreateDbContextAsync();

var missingAttemptIds = new List<long?>();

if (useStartDate)
{
var existingIds = await context.Attempts
.Where(at => at.Time >= _startDate)
.Select(at => at.Id)
.ToHashSetAsync();

var fromSubmissions = await context.Submissions
.Where(s => s.Time >= _startDate)
.Select(s => s.Attempt)
.ToListAsync();
missingAttemptIds.AddRange(fromSubmissions
.Where(id => id.HasValue && !existingIds.Contains((long)id))
.ToList());
}
else
{
var existingIds = await context.Attempts
.Select(at => at.Id)
.ToHashSetAsync();

var fromSubmissions = await context.Submissions
.Select(s => s.Attempt)
.ToListAsync();
missingAttemptIds.AddRange(fromSubmissions
.Where(id => id.HasValue && !existingIds.Contains((long)id))
.ToList());
}

return missingAttemptIds.Select(x => (long)x!).ToList();
}

Пара слов о структуре моделей. Эти модельки приходят из Степика - те самые ваши решения задач - Submissions и попытки Attempts. У каждого решения может быть одна попытка, но несколько решений могут быть для одной попытки. Объяснить это сложно, но так задумано: при отправке нового решения перезаписывается только попытка - она получается одна для одной задачи.
Продолжение...
🔥1
Начало...
Так вот, нам для актуализации данных на своей стороне нужно загружать и попытки и решения. Сначала мы загружаем решения, потом берем список Attempt и загружаем те, которых у нас еще нет.
Метод выше, работал без малого год и проблем не было, потому что работало.
Работало, пока мы не поймали OutOfMemoryException )).
Из-за того, что выгружались списки, состоящие из нескольких миллионов элементов, объем занимаемой этими списками памяти стал критичным и в конечном итоге закончился.
Логирование помогло локализовать проблему достаточно быстро. Но, что не так и что делать?
В изначальном методе мы извлекаем сначала огромное количество значений в один список, потом еще большее число значений во второй список и затем формируем третий. Ужас-ужас. Особенно, если учесть, что речь идет о списках по несколько миллионов значений в каждом. В конечном итоге, размер потребляемой памяти мог запросто занимать ~200 мб.
Решение:
Решение на самом деле простое - перенести вычисления на сторону БД. Там все операции выполнятся быстрее и не займут столько места, плюс нет ограничений CLR на размер памяти, тем более, что ее там столько не потребуется.
    public async Task<List<long>> GetMissingIdsAsync(bool useStartDate)
{
await using var context = await _contextFactory.CreateDbContextAsync();

try
{
var missingAttempts = context.Submissions!
.Where(s => !useStartDate || s.Time >= _startDate)
.Where(s => s.Attempt != null)
.Where(s => !context.Attempts!
.Any(a => a.Id == s.Attempt))
.Select(s => (long)s.Attempt!)
.Distinct();

return await missingAttempts.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Произошла ошибка при получении id отсутствующих попыток!");
return [];
}
}

Здесь мы формируем список id прямо в БД и получаем уникальные значения без вычислений на стороне приложения. Более того, код стал понятнее и лаконичнее.
*заодно исправили ещё момент с некорректной выборкой элементов, из-за которых каждый раз получали "лишние" данные для загрузки.

Итого: сортировка и выборка данных на стороне БД работает в большинстве случаев быстрее и занимает меньше ресурсов + не отражается на производительности самого приложения. Точнее, не ухудшает его.
🔥2
Проблемы с миграциями или как все вернуть, когда казалось бы ничего не получается.

Как сказал один умный человек:
опыт — сын ошибок трудных.


Бывает такое, что в процессе работы приходится по несколько раз менять одну и ту же модель, выполнять миграции, потом снова менять и так далее по кругу. А потом, в какой-то момент, EF уже не справляется и создать миграцию не может, не видит изменений в моделях, а в БД этих изменений и нет. Начинаются проблемы, ошибки и поиск решения.
Но на самом деле всё не так уж страшно и сложно.
Первое, что нужно понимать - если в БД нет изменений (таблицы, колонки) и нет миграции в _EFMigrationsHistory тогда нужно выполнить ряд манипуляций руками и все будет работать как должно!
Смоделируем ситуацию, когда в таблице Users (EF если не указать имя таблицы руками называет таблицы в PascalCase) не появился столбец, например Courses а таблица UserCourses была переименована из StudentCourses, но в бд этих изменений так же нет.

Итак, по порядку:
1️⃣. Все наши проблемы скрываются в файле DbContextModelSnapshot.cs (имя контекста может отличаться, но ModelSnaphot будет все равно)
2️⃣. Или ищем руками названия столбца и таблицы и удаляем те изменения, которые должны были попасть в миграцию, но не попали,
3️⃣. Либо сравниваем текущую ветку с той, в которой точно не было этих потерянных изменений и там, в сравнении, находим нужные строки и удаляем.
4️⃣. Рекомендуем начинать удалять снизу вверх - тогда номера строк в сравнении и в файле будет совпадать и искать будет проще.
5️⃣. После того, как зачистили файл, убедились, что в истории миграций в БД нет лишних миграций и у себя в проекте нет лишних миграций, создаем новую.
6️⃣. Вот в общем-то и всё. Миграция создается успешно со всеми нужными изменениями. ( если нет — возвращаемся к шагу 1 и повторяем все заново, значит где-то что-то осталось не удаленным)
На всякий случай, хотя нет - всегда (!) проверяйте, что в файле с созданной миграцией написано: все ли изменения там присутствуют. И если чего-то не хватает, какого-то изменения или, например, индекса, всегда можно дописать нужное руками.

Запускаемся => смотрим, что все работает => больше не боимся поломок миграций!

С заботой, команда отдела разработки!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Как не надо делать

В процессе разработки периодически приходится работать с публичными api разных сервисов. И периодически сталкиваемся с плохой, а иногда и откровенно ужасной документации api.
Пока "лидирует" по числу недоуменных взглядов и разочарованных вздохов документация к публичному api Stepik.
В чём вообще суть вопроса? А вот пример:
{
"apiVersion": "",
"swaggerVersion": "1.2",
"basePath": "https://stepik.org",
"resourcePath": "/api/step-snapshots",
"apis": [
{
"denoscription": "StepSnapshot resource",
"path": "/api/step-snapshots",
"operations": [
{
"method": "GET",
"summary": "StepSnapshot resource",
"nickname": "Step_Snapshot_list",
"notes": "StepSnapshot resource.",
"type": "object",
"parameters": [
{
"paramType": "query",
"name": "step",
"denoscription": "id of the step to show snapshots for",
"type": "string",
"format": "string",
"required": false,
"defaultValue": null
}
]
}
]
}
],
"models": {}
}

Что тут можно понять?
1. Нет версии api. Работать — работает, но версия должна быть
2. Только один параметр запроса? Серьёзно? Мы точно знаем, что их как минимум 2 (как минимум page)
3. Модель. Какая модель-то на выходе?

Это прям основное, то, что бросается в глаза. И на самом деле это (плохая документация) может быть проблемой, особенно когда api платное, а для того, чтобы разобраться, приходится экспериментировать за свой счёт.

Поэтому, рекомендация простая: если пишите публичный api, поддерживайте документацию в актуальном состоянии. Задайте себе вопрос: а не пропустил ли я чего-нибудь? Могу ли я понять из документации, что получу и при каком запросе?
👍1
2025 год для отдела разработки

Это был очень интересный и не менее насыщенный год!
Мы начали свой путь в самом конце 2024 года и примерно в эти же, предновогодние дни впервые зарелизили наш внутренний портал.
С тех пор кодовая база росла, штат увеличивался, а количество применяемых технологий и качество кода в целом сделали ощутимый рывок вперед!

Подводя итоги, захотелось как-то формализовать успехи и достижения, чтобы их можно было (внутри команды) посмотреть в читаемом виде. Так появилось небольшое приложение, считающее метрики из репозитория. В предновогодней суете еще не оформили его в полноценное открытое решение, но обязательно сделаем это в самом начале следующего года. Оно поможет собрать статистику с любого репозитория, к которому у пользователя есть доступ и посмотреть, что сделано за нужный период: количество коммитов и строк кода, наиболее активные дни недели и время суток и т.д.

К посту прикрепляем некоторые детали из получившегося отчета, можно оценить нашу продуктивность (и режим дня )))

А еще мы в уходящем году множество раз обновляли библиотеку для работы со Stepik api, написали свое приложение с формами обратной связи (которое как минимум видели все ученики, а многие и воспользовались, заполнив анкету/ты) и готовим еще много всего важного, полезного и интересного как для команды, так и, в большей степени, для наших дорогих учеников!
И, конечно, будем делиться своими наработками и наблюдениями здесь в канале. Обмен опытом всегда интересен, и прикладную информацию по изучаемому языку читать, на наш взгляд, вдвойне интересней.

С наступающим Новым годом!
❤‍🔥3
Кастомная авторизация: быстро и просто

В эпоху, когда микросервисы захватывают умы и сердца разработчиков (Нет. Нет же? ))) зачастую встает вопрос о том, а как организовать авторизацию между несколькими приложениями без необходимости усложнения процесса?
То есть, буквально: есть несколько приложений, работающих вместе, на одном сервере, например, и им надо между собой обмениваться данными, при этом не открывая доступ к эндпоинтам во внешний мир и не заморачиваясь с конфигурацией сервера для каждого маршрута для каждого приложения. Можно организовать авторизацию с JWT токенами, но тогда нужно будет писать кучу сервисов, хранить секреты, обновлять эти самые секреты и т.д.
И вот зачем изобретать сложную систему секретов, ключей, авторизации друг у друга? И какое решение можно применить для упрощения этого процесса?
Выход на самом деле очень простой. На помощь в данном вопросе приходит Middleware - промежуточный слой в приложении, который еще до контроллера проверит, авторизован ли запрос. В общем-то, выполнит примерно такое же действие, что и стандартный [Authorize] , только ключ мы зададим сами, равно как и заголовок запроса.

/// <summary>
/// Класс-атрибут для защиты контроллера TargetController от несанкционированного доступа
/// Работает через фильтр <see cref="IAsyncActionFilter"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class TargetAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var expectedTargetToken = Environment.GetEnvironmentVariable("TARGET_KEY");

if (!context.HttpContext.Request.Headers.TryGetValue("TARGET-TOKEN", out var headerKey)
|| headerKey != expectedTargetToken)
{
context.Result = new UnauthorizedResult();
return;
}
_ = await next();
}
}

Что тут происходит: получаем из переменной окружения значение ключа, сравниваем со значением в заголовке (если такой заголовок существует в запросе) и если все ок - пропускаем запрос дальше, если нет нужного заголовка или значения не совпадают, то запрос даже не доходит до контроллера.
Максимально просто и надежно.

В коде контроллера применение атрибута будет выглядеть так:
[ApiController]
[Target] // здесь мог быть, например, стандартный [Authorize]
[Route("target")]
public class TargetController(
ILogger<TargetController> logger,
ITargetService targetService
) : Controller
{
// код контроллера
}


При старте, например если несколько приложений запускаются в docker-compose, все они получат значение ключа из .env файла (или другим способом, которым выполняется добавление в переменные окружения значений). Извне к этим значениям доступа ни у кого нет, а приложения спокойно обмениваются данными (обращаются к конечным точкам контроллеров).

Для большей надежности:
1. Генерируйте сложные ключи.
Делается это одной командой:
openssl rand -hex 32

Эта команда сгенерирует 256-битный ключ в шестнадцатеричном формате, его можно смело использовать в качестве ключа.
*в Windows ни в командной строке ни в PowerShell openssl по человечески не работает, но зато нормально работает в WSL и git bash.

2. Регулярно меняйте ключи.
Это можно сделать множеством способов, например скриптом по таймеру или если приложение часто обновляется, добавить скрипт прямо в docker-compose.yml
👍3
Атрибут, снимающий головную боль

В период, когда основной проблемой является запуск приложения и попытки заставить все вместе работать как хочется (это про период обучения, если что) о всяких мелочах можно и не задумываться и не углубляться во всякое, казалось бы, не очень то и важное.

А вот когда проект разрастается, и логика выполнения некоторых действий в приложении затрагивает одним вызовом сразу множество сервисов, найти проблему на фронте бывает непросто.
Вот ситуация: на страницу добавляется функционал, меняется или добавляется какой-нибудь стиль и... в приложении ничего не меняется. Локально - возможно, а в проде никак. Куда смотреть, если ошибок нет? Как найти проблему?
Такая же история с js файлами. Вносим изменения, собираем проект, запускаем на сервере и ничего не меняется.

Тут-то и вспоминаем про атрибуты. Речь сегодня про один, но важный и не всегда очевидный - asp-append-version

Как и что он делает

Если не добавить этот атрибут к импорту файла (например стилей) то это будет выглядеть как:
<link rel="stylesheet" href="~/css/site.css"/>


Если если внести изменения в стили и перезапустить приложение, то в браузере может остаться кэшированная версия файла и стили просто не применятся.
Соответственно, для того, чтобы таких проблем избежать, нужно использовать атрибут:
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />


Что здесь меняется и что вообще происходит

ASP при сборке добавляет в имя файла хэш, соответственно при входе на сайт браузер видит различие в используемых именах файлов и если что-то изменилось, значит изменился хэш, значит у файла другое имя и браузер не будет использовать кэшированную версию.
Имя файла в конечной версии может выглядеть, например, вот так:
site.css?v=NnrPGoz4r8WDnFcIkhEcsQCwLzXQq3MNsNyDxAOBfX4


Вывод простой: в MVC приложениях используйте версионирование файлов стилей и скриптов. Это гарантирует, что пользователи будут использовать только актуальные версии продукта!
🔥5
Рефакторинг. Часть 1

Этой теме можно посвящать не только отдельные посты, но и целые книги. Столько всего кроется под одним коротким словом. И тем не менее, путь от написанного кода к хорошему коду лежит через переделки. Так или иначе, но сразу написать максимально аккуратно, чисто, в лучших традициях SOLID - это утопия. Все равно будут сроки, будет спешка, смена фокуса и т.д. То есть, это нормально, когда код может быть улучшен!

Начнем историю рефакторинга с базы. А точнее с моделей в БД.
Перед рефакторингом в классе BaseDbContext (о том, почему именно так и что у нас есть расскажем потом) было чуть более 1000 строк кода из которых на конфигурацию моделей приходилось порядка 900 строк! Исторически это сложилось из-за того, что сначала моделей было немного и надо было быстро запуститься, потом добавлялись еще, а потом их стало много и уже руки не доходили что-то с этим сделать. Понятно, что найти нужную информацию в таком полотне кода сложно. С поиском проще, но все равно некомфортно, да и добавлять новые модели нужно то ли в конец, то ли по смыслу искать ближайшие и вклиниваться между ними. В общем, читаемость активно снижалась со временем.
Пришла пора встряхнуть этот код и привести в порядок настройку моделей.

Выполнили следующим образом:

1. Сгруппировали по смыслу модели
2. Вынесли конфигурации в общие классы

Получилось 9 классов конфигураций с несколькими вложенными классами.
Пример такой конфигурации:
C#
public class UpdateInfoConfiguration : IEntityTypeConfiguration<UpdateInfo>
{
public void Configure(EntityTypeBuilder<UpdateInfo> builder)
{
// конфигурация свойств
}
}

public class DownloadProblemsReportConfiguration : IEntityTypeConfiguration<DownloadProblemsReport>
{
public void Configure(EntityTypeBuilder<DownloadProblemsReport> builder)
{
// конфигурация свойств
}
}

public class DownloadProblemConfiguration : IEntityTypeConfiguration<DownloadProblem>
{
public void Configure(EntityTypeBuilder<DownloadProblem> builder)
{
// конфигурация свойств
}
}


А в OnModelConfiguring вместо ~900 строк получили одну:
C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.HasDefaultSchema("public");
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseDbContext).Assembly);
}


Итого, все конфигурации моделей подтягиваются на этапе сборки, класс освобожден от нагромождения информации, сами конфигурации разделены по смыслу и легко находятся. Сплошные плюсы.
Конечно, можно, следуя всяческим рекомендациям, не объединять классы моделей внутри одного файла, а выделить конфиг каждой модели в отдельный файл. Тогда желательно разложить их по папкам. В текущей же реализации мы получили компромисс между компактностью и удобством, что вполне удовлетворяет нашим потребностям.

И пару слов про непосредственно код.
Для того, чтобы все это провернуть, нужно:

1. Унаследовать класс-конфигурацию от IEntityTypeConfiguration<T>
2. В параметр единственного метода Configure передать EntityTypeBuilder<T> builder
3. в OnModelCreating добавить вызов метода modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseDbContext).Assembly); где BaseDbContext - имя класса вашего контекста

Всё просто ))
🔥42