uz
Feedback
.NET Разработчик

.NET Разработчик

Kanalga Telegram’da o‘tish

Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#. Для связи: @SBenzenko Поддержать канал: - https://boosty.to/netdeveloperdiary - https://patreon.com/user?u=52551826 - https://pay.cloudtips.ru/p/70df3b3b

Ko'proq ko'rsatish
6 696
Obunachilar
+124 soatlar
-57 kunlar
+430 kunlar
Postlar arxiv
💥Зачем Senior-разработчики каждый год ездят на DotNext? Когда вы уже Senior или Team Lead, все больше времени уходит на план
💥Зачем Senior-разработчики каждый год ездят на DotNext? Когда вы уже Senior или Team Lead, все больше времени уходит на планирование, синки, оценку задач и помощь другим. При этом код и разговоры про то, как все устроено под капотом, иногда отходят на второй план. DotNext 2026 — это возможность на пару дней вернуться в эту часть разработки: обсуждать архитектуру, разбирать сложные инженерные решения и снова погружаться в .NET с коллегами, которым интересны те же вопросы. В кулуарах встречаются разработчики разных уровней — от Middle до Senior: сравнивают подходы, делятся опытом и обсуждают задачи, которые редко помещаются в обычный рабочий созвон. На сцене — доклады для тех, кому интересно разбираться глубже: от низкоуровневых оптимизаций и внутренних механизмов .NET до практических решений, которые можно забрать в свой стек. ☄️В этом году среди спикеров — Александр Поломодов, Станислав Выщепан, Андрей Цветцих и другие участники .NET-сообщества, включая представителей DotNetRu и SPBDotNet. 🗓Конференция пройдет 25–26 сентября в Москве + онлайн. Если хочется на пару дней сменить контекст и снова поговорить про инженерные вопросы с людьми своего уровня — самое время запланировать поездку. Билет можно приобрести на сайте. 🔔А по промокоду: NetDeveloperDiary у подписчиков моего канала есть уникальная возможность получить скидку 15%!

День 2703. #ЗаметкиНаПолях Версионирование API Должно Быть Последним Средством. Окончание Начало Продолжение Новая операция часто дешевле, чем новая версия Иногда сценарий использования действительно меняется настолько, что добавление новых флагов и необязательных параметров к существующей конечной точке становится запутанным. Вот плохой путь:
POST /orders?validateOnly=true&includeTaxEstimate=true&reserveInventory=true
Тут нет одной чистой операции, есть несколько рабочих процессов, скрывающихся за одной конечной точкой. В этом случае лучше добавить новую операцию или ресурс. Это сохраняет стабильность старого контракта, предоставляя новому поведению чистое место. Например: - POST /orders остаётся простой конечной точкой «разместить заказ». - POST /orders/quote становится операцией «скажите, сколько это будет стоить». Это обычно намного дешевле, чем создание /v2/orders и перенос всего остального API вместе с этим. Устаревание по-настоящему Это недостающая часть управления изменениями API. Большинство случаев устаревания — это обман. Они существуют в документации, но ничего не происходит в процессе работы. Реальный процесс устаревания должен включать: - Пометку старого поля или конечной точки как устаревшей в описании OpenAPI; - Сообщение об устаревании во время выполнения; - Предоставление пользователям пути миграции; - Измерение фактического использования перед удалением чего-либо. Если вы используете HTTP, то сообщения во время выполнения могут быть в заголовке ответа:
Deprecation: true
Sunset: Wed, 31 Dec 2026 23:59:59 GMT
Link: <https://docs.example.com/migrations/orders-total>; rel="deprecation"
Теперь информация об устаревании видна в документации, видна в реальном трафике и связана с фактическим руководством по миграции. И вот здесь телеметрия имеет значение. Если вы не знаете, какие клиенты всё ещё используют устаревшее поле или конечную точку, вы не управляете изменениями. Вы гадаете. Отслеживайте использование по ID клиента, ключу API, тенанту или имени приложения. Затем дождитесь, пока использование фактически исчезнет, прежде чем что-либо удалять. Когда версионирование - правильное решение: - старая и новая семантика не могут безопасно сосуществовать; - модель ресурсов изменилась коренным образом; - правила совместимости приводят к контракту, в котором никто не может разобраться. А обдуманное версионирование означает выбор наименьшего возможного нарушения, которое вы можете оправдать. Иногда это новая форма конечной точки. Иногда это вариант представления. Иногда, особенно для публичных API, это прямое версионирование URL-адресов, поскольку оно явное и легко объяснимое. Если вы используете версионирование, сочетайте его с реальным процессом устаревания (см. выше). Настоящая работа не в создании версии 2, а в том, чтобы перевести потребителей с версии 1. Итого Разрабатывайте контракты с учётом возможности их развития. Рассматривайте клиентов как долгосрочные интеграции, а не просто как сегодняшний код. И оставляйте версионирование для случаев, когда возможности совместимости действительно исчерпаны. Источник: https://www.milanjovanovic.tech/blog/api-versioning-should-be-your-last-resort

День 2701. #ЗаметкиНаПолях Версионирование API Должно Быть Последним Средством. Продолжение Начало Правила совместимости - Сохраняйте существующие поля и поведение; - Не превращайте необязательные данные запроса в обязательные; - Не меняйте то, что делает существующая операция; - Делайте всё новое в добавление и необязательным по умолчанию. 1. Добавляйте, а не заменяйте Самое безопасное изменение обычно является аддитивным. Допустим, первоначальный ответ от GET /orders/{id} был таким:
{
  "id": "ord_123",
  "status": "paid",
  "total": 100
}
Вместо замены поля total добавим новое:
{
  "id": "ord_123",
  "status": "paid",
  "total": 100,
  "totalMoney": {
    "amount": 100,
    "currency": "USD"
  }
}
Существующие клиенты продолжают использовать total. Новые могут перейти на totalMoney. Помечаем старое поле как устаревшее и удалим его только после реального периода миграции. Иногда некрасивый контракт — это цена совместимости. 2. Делайте клиентов толерантными Хорошо работающий клиент не должен выдавать ошибку из-за того, что сервер добавил поле, которое он не понимает. Если ответ изменился с:
{
  "id": "ord_123",
  "status": "paid"
}
на:
{
  "id": "ord_123",
  "status": "paid",
  "estimatedDeliveryDate": "2026-05-29"
}
существующие клиенты должны игнорировать дополнительное свойство и продолжать работу. В System.Text.Json неизвестные свойства игнорируются по умолчанию. Реальный риск обычно заключается в чрезмерно строгой проверке JSON (об этом позже на канале). Это одна из распространённых проблем. Команды заявляют о желании обратной совместимости, а затем генерируют клиентские модели, которые отклоняют любое неожиданное поле в ответе. Клиенты должны быть достаточно толерантными, чтобы игнорировать то, чего они не понимают. 3. Не меняйте то, что делает существующая операция Самые опасные критические изменения скрываются в поведении. URL, тело запроса, формат ответа те же. Но то, что делает операция на сервере, отличается. Например, DELETE /orders/{id} сначала реализовывал мягкое удаление, и заказ переходил в «архив», но по-прежнему отображался в отчётах аудита и мог быть восстановлен службой поддержки. Затем команда решила «почистить базу» и изменить поведение на жёсткое удаление. Ни один клиент сразу этого не заметит, но данные теперь «по-тихому» уничтожаются. Аналогично: - POST /orders раньше был идемпотентным, а затем незаметно перестал им быть; - POST /orders/{id}/cancel раньше автоматически возвращал деньги, а затем перестал это делать, потому что «возвраты должны быть отдельным вызовом»; - PUT /orders/{id} раньше был полной заменой, а теперь стал частичным слиянием; - Веб-хук раньше срабатывал один раз для каждого заказа, а теперь срабатывает для каждой позиции заказа; и т.п. Каждый из этих вариантов сохраняет стабильность URL, метода и структуры JSON, но нарушает все существующие интеграции таким образом, что это не будет видно при сравнении схем. Безопасный шаг тот же: добавлять, а не изменять. Например, параметр в конечной точке для жёсткого удаления DELETE /orders/{id}?purge=true Как только операция выпущена, её поведение становится частью контракта. Вы можете добавлять новые операции, можете объявить её устаревшей, но не можете незаметно изменять её работу. 4. Будьте осторожны с валидацией Существует два варианта одной и той же ошибки: - Сделать обязательным существующее необязательное поле; - Добавить новое обязательное поле. Оба варианта ломают существующих клиентов. Путь к конечной точке не меняется, но запросы, которые раньше выполнялись успешно, теперь отклоняются. Более безопасный путь — определить значения по умолчанию или ввести новую операцию для более строгого рабочего процесса. Изменения в ответах обычно тщательно проверяются с точки зрения проектирования. Изменения в проверке запросов заслуживают такого же внимания. Главное правило: то, что вы добавляете в контракт, должно быть необязательным, и всё, что было необязательным, должно оставаться необязательным. Окончание следует… Источник: https://www.milanjovanovic.tech/blog/api-versioning-should-be-your-last-resort

День 2700. #ЗаметкиНаПолях Версионирование API Должно Быть Последним Средством. Начало Про версионирование API уже была серия постов на канале. Но более важный вопрос не как это делать, а когда. Каждая команда разработчиков API в итоге приходит к одному выводу: «Просто создадим версию 2». Звучит ответственно. За исключением того, что теперь нужно поддерживать два API, два набора документации, два варианта поведения и проект миграции, которую клиенты будут откладывать как можно дольше. Версионирование — это инструмент совместимости, а не стратегия проектирования. Большинство изменений API не требуют новой версии. Они требуют более эффективного управления изменениями. Если вы рассматриваете каждое изменение контракта как проблему версионирования, вы в итоге плодите клоны своего API. Если же вы рассматриваете это как проблему управления изменениями, вы начинаете задавать более правильные вопросы: - Можно ли добавить, а не заменить? - Может ли старое и новое поведение сосуществовать некоторое время? - Можно ли ввести новую операцию вместо изменения старой? - Можно ли безопасно удалить что-то с помощью миграции и основываясь на данных телеметрии? Такой подход приводит к созданию API, которые гораздо лучше выдерживают проверку временем. Что на самом деле ломает код клиентов? Изменения, приводящие к сбоям, обычно касаются не только URL-адреса. Это также: - удаление или переименование поля, - изменение значения существующих данных, - ужесточение проверки запросов, - изменение формата пагинации или ошибок, - предположение, что перечисления – закрытый для изменений тип. Это ломает клиента так же, как и удаление конечной точки:
// До
{ "total": 100 }

// После
{ "total": { "amount": 100, "currency": "USD" } }
Вы не изменили путь, не переименовали конечную точку, но всё равно сломали работу клиентов. Поэтому вместо вопроса: «Должна ли это быть версия 2?», спросите: «Могут ли старый и новый контракты безопасно сосуществовать?» Продолжение следует… Источник: https://www.milanjovanovic.tech/blog/api-versioning-should-be-your-last-resort

День 2699. #ВопросыНаСобеседовании Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании. 36. Управление состоянием «Расскажите о различных стратегиях управления состоянием, доступных в приложениях .NET. Приведите примеры сценариев, в которых вы бы использовали каждую стратегию». Хороший ответ В приложениях .NET эффективное управление состоянием имеет решающее значение для поддержания удобства использования и производительности приложения. Существует несколько стратегий, каждая из которых подходит для разных сценариев. 1. Куки: для хранения пользовательских предпочтений или данных сессии на стороне клиента. Куки легко реализовать в .NET с помощью класса HttpContext:
public void SetCookie(string key, string value, int? expireTime)
{
  CookieOptions option = new();
  
  option.Expires = expireTime.HasValue ? 
    DateTime.Now.AddMinutes(expireTime.Value) :
    DateTime.Now.AddMilliseconds(10);

  HttpContext.Response.Cookies.Append(key, value, option);
}
2. Скрытые поля: полезны для сохранения состояния на стороне клиента при POST-запросах. Они могут содержать данные, которые не должны быть видимыми или изменяемыми пользователями, но должны сохраняться между запросами. 3. Сессии: данные пользователя хранятся на сервере между HTTP-запросами. В .NET 8 состояние сессии можно включить и получить к нему доступ через HttpContext:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSession(opts =>
{
  opts.IdleTimeout = TimeSpan.FromMinutes(10);
  opts.Cookie.HttpOnly = true;
  opts.Cookie.IsEssential = true;
});

app.UseSession();

// Сохраняем данные в сессию
HttpContext.Session.SetString("SessionKey", "Value");
4. База данных: используется, когда данные должны сохраняться между различными сессиями или когда объём данных слишком велик для хранения в куки или сессии. Это включает в себя хранение информации о состоянии в БД, к которой можно обращаться по мере необходимости. 5. Кэш: кэширование данных может значительно повысить производительность приложения за счёт снижения нагрузки на БД. В .NET кэширование данных может быть реализовано с использованием кэша в оперативной памяти, распределённого или гибридного кэша:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHybridCache();

// Сохраняем данные в кэш
cache.Set("CacheKey", "Cached Value");
Выбор правильной стратегии зависит от конкретных требований приложения, включая такие факторы, как масштабируемость, безопасность и характер хранимых данных. Часто встречающийся неверный ответ: «Проще всего использовать сессию для хранения всех данных. Всё хранится на сервере, и не нужно беспокоиться об управлении состоянием на стороне клиента.» Почему это неправильно: - Чрезмерное использование сессии: ответ предлагает использовать состояние сессии без разбора для всех потребностей управления состоянием, что нецелесообразно из-за проблем с масштабируемостью и производительностью, особенно в распределённых средах, где данные сессии могут не сохраняться на разных серверах без дополнительной настройки. - Отсутствие масштабируемости: полагаясь исключительно на состояние сессии, можно значительно снизить масштабируемость приложения по мере увеличения числа пользователей, что приводит к увеличению использования памяти и потенциальной потере данных в сценариях веб-ферм, если сессии не настроены на использование сервера состояния или БД. - Игнорирование других вариантов: не учитываются преимущества других стратегий, которые могут быть более подходящими в зависимости от конкретных требований приложения, таких как необходимость сохранения данных или совместного доступа между сессиями. Этот часто встречающийся неверный ответ может быть следствием недостаточного понимания последствий различных стратегий управления состоянием или опыта работы в средах, где управление сессиями не являлось узким местом. Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md

День 2698. #ЗаметкиНаПолях Перенос Строки — Это не Только \r и \n Когда мы говорим о символах переноса строки, большинство разработчиков думают о \r\n (Windows) и \n (Unix). В большинстве случаев это работает, но это не полная картина. Юникод и несколько механизмов регулярных выражений распознают дополнительные символы переноса строки. Если ваше приложение обрабатывает пользовательский ввод, логи, CSV-файлы или кроссплатформенные данные, это может иметь большее значение, чем вы думаете. Переносы строк, которые следует знать Наиболее распространённые символы завершения строки:
\r\n (CRLF, U+000D U+000A)
\n (LF, U+000A)
\r (CR, U+000D)
Unicode Technical Report #18 (RL1.6) также отмечает эти ограничители:
\u0085 (NEL, Next Line)
\u2028 (LS, Line Separator)
\u2029 (PS, Paragraph Separator)
Вы также можете считать эти пробельные символы переносами строки в некоторых контекстах:
\v (VT, Vertical Tab, U+000B)
\f (FF, Form Feed, U+000C)
Поэтому полноценное «разбиение по строкам» не должно подразумевать только CR и LF. Почему это важно в реальном коде Эти символы могут поступать из: - Скопированного текста из офисных программ или устаревших систем; - Файлов, преобразованных с помощью нескольких этапов кодирования и нормализации. Если вы разделяете текст только по символам \r\n или \n, некоторые записи могут остаться объединёнными в одну строку, что может нарушить синтаксический анализ, проверку или формирование отчётов. Более безопасный синтаксический анализ в .NET Если вам нужно разделить текст на строки, отдавайте предпочтение шаблону, который обрабатывает последовательности символов новой строки Unicode.
using System.Text.RegularExpressions;

string input = "A\u2028B\u0085C\r\nD";
string[] lines = Regex.Split(input, @"(?>\r\n|[\n\v\f\r\u0085\u2028\u2029])");

// lines = ["A", "B", "C", "D"]
В .NET шаблон \R пока не поддерживается, поэтому приходится использовать более явный шаблон. Если вы хотите избежать аллокации массива строк и строковой переменной на каждую строку, в .NET 9 представлен Regex.EnumerateSplits:
using System;
using System.Text.RegularExpressions;

ReadOnlySpan<char> input = "A\u2028B\u0085C\r\nD";

foreach (Range split in Regex.EnumerateSplits(input, @"(?>\r\n|[\n\v\f\r\u0085\u2028\u2029])"))
{
  ReadOnlySpan<char> line = input[split];
  ProcessLine(line);
}

static void ProcessLine(ReadOnlySpan<char> line)
{
  // обработка line без аллокаций
}
Для нормализации .NET также предоставляет метод string.ReplaceLineEndings, который полезен, когда необходимо преобразовать все символы конца строки в единое соглашение перед обработкой:
string input = "A\u2028B\u0085C\r\nD";
string normalized = input.ReplaceLineEndings("\n");
string[] lines = normalized.Split('\n');

// normalized = "A\nB\nC\nD"
// lines = ["A", "B", "C", "D"]
Источник: https://www.meziantou.net/new-lines-are-more-than-r-and-n.htm

День 2696. #ЧтоНовенького Просмотр Пул-Реквестов в Visual Studio Интеграция пул-реквестов в VS — одна из самых востребованных
+2
День 2696. #ЧтоНовенького Просмотр Пул-Реквестов в Visual Studio Интеграция пул-реквестов в VS — одна из самых востребованных функций Git. Разработчики просили о возможности открывать PR, просматривать изменения, обсуждать отзывы и завершать проверку, не переключаясь в браузер. Создавать PR в VS можно с 2024 года. Теперь также можно просматривать, комментировать и утверждать PR из GitHub и Azure DevOps, не выходя из IDE. Обновление доступно в VS 18.7. Поиск и открытие PR Вы можете просмотреть список PR для открытого репозитория в окнах Git Repository, Git Changes или из меню Git (см. картинку 1). Если в текущей ветке уже есть активный PR, вы также можете открыть его непосредственно из окна Git Changes. При открытии PR вы можете увидеть обзор, изменения, коммиты и рецензентов в одном месте. Здесь вы можете выбрать, насколько глубоко хотите изучить код. Вы можете проверить PR, не переключаясь на ветку, что позволяет проверить изменения, сохраняя при этом вашу текущую ветку, незафиксированные изменения и рабочее состояние. Если хотите изучить код более подробно, переключитесь на ветку PR и используйте инструменты навигации, сборки и отладки VS для более глубокого анализа. Просмотр изменений Режим просмотра PR разработан для быстрого просмотра. Откройте любой изменённый файл, чтобы увидеть изменения непосредственно в коде или рядом с ним, или используйте режим сводки по нескольким файлам, чтобы увидеть все изменения сразу (см. картинку 2). Вы также можете просматривать коммиты по отдельности, что полезно, когда PR охватывает несколько логических шагов, и вы хотите понять, как развивалось изменение. Комментирование и обсуждение Вы можете оставлять комментарии к определённым строкам, отвечать на ветки обсуждений и завершать обсуждения после их окончания. Файлы с активными комментариями отмечаются в списке изменений, поэтому легко определить, где происходят обсуждения. Все синхронизируется между VS и браузером (см. картинку 3). При проверке PR в отредактированном коде вы можете одним щелчком применить изменение к вашей рабочей копии. Также Copilot может сгенерировать исправление на основе комментария и окружающего кода, чтобы вы могли сразу же оценить и протестировать его. Утверждение, завершение и слияние Когда вы готовы принять/отвергнуть PR, вы можете увидеть необходимую информацию и действовать, не покидая страницу проверки. На вкладке Overview (Обзор) вы можете увидеть проверки статуса, конфликты слияния и информацию о том, нужны ли ещё необходимые подтверждения. Вы можете утвердить PR из представления сравнения изменений, с дополнительными параметрами голосования для PR Azure DevOps. Вы также можете завершить или слить PR прямо в IDE. Если планы изменятся, вы можете преобразовать его в черновик или закрыть. После открытия PR вы можете пройти весь процесс проверки в одном месте. Источник: https://devblogs.microsoft.com/visualstudio/review-pull-requests-without-leaving-visual-studio/

День 2696. #ЗаметкиНаПолях Планирование Мощностей для API. Окончание Начало Продолжение Следим за результатами - Задержка p95 - Остается ли скорость в выбранных рамках? - Частота ошибок 429 - Срабатывает шлюз нагрузки, как и было задумано. - pendingOutbox (длина очереди обработки – в примере выше в http://localhost:5080/api/metrics) - Если это число продолжает расти и никогда не уменьшается, ваши фоновые процессы не справляются со скоростью записи, а это тоже ограничение пропускной способности — только в фоновых сервисах. Цель здесь никогда не состоит в нулевом количестве ошибок. Цель — предсказуемое снижение производительности — точное знание того, что делает API, когда вы выходите за его предел, чтобы отказ был плавным, а не катастрофическим. Рекомендуемые пороговые значения Это отправные точки, а не законы. Ваши показатели зависят от оборудования, запросов и SLO. Но при отсутствии другой информации, вот примерные цифры: - Менее ~100 запросов в секунду Пока не стоит обращаться к инфраструктуре. Сначала избавьтесь от N+1 запросов и лишних аллокаций. Большинство API на этом уровне работают медленно из-за кода, а не из-за нагрузок на оборудование. - ~100-1000 запросов в секунду Здесь оправдывает себя кэширование, и имеет смысл настройка пула соединений. Правильно определите размер пула и разместите кэш перед наиболее часто выполняемыми операциями чтения. - 1000+ запросов в секунду на операции записи Прекратите синхронную запись в БД на пути запроса. Перейдите к выравниванию нагрузки на основе очередей: быстро отвечайте на запросы, обрабатывайте их в фоновом режиме (см. паттерн Outbox в демо-проекте). - Высокие пиковые нагрузки + строгий уровень SLO Добавьте явное ограничение скорости. Встроенный ограничитель скорости в ASP.NET Core или шлюз, подобный описанному выше. Определите точку отказа целенаправленно, а не после обнаружения её в проде. Общий принцип для всех вариантов: по мере роста нагрузки работа перемещается за пределы пути запроса. Операции чтения кэшируются, операции записи ставятся в очередь, а синхронная критическая секция становится минимально возможной. Контрольный список по планированию мощностей 1. Для каждого класса конечной точки существует записанный SLO (целевой показатель p95 + бюджет ошибок). 2. Скрипты нагрузочного тестирования находятся в системе контроля версий рядом с кодом и запускаются при каждом значимом изменении. 3. Для каждого класса конечной точки существует известный, задокументированный максимальный безопасный RPS. 4. Существует руководство по масштабированию вверх и вниз — именно при масштабировании вниз скрываются неожиданности. 5. Оповещения срабатывают при p95, частоте тайм-аутов и задержке очереди — а не только по CPU и памяти. Если хотя бы один из этих параметров отсутствует, вам снова придется гадать при следующем релизе. Часто задаваемые вопросы Какой показатель важнее всего? Задержка p95, почти всегда. Она отражает то, что чувствует реальный пользователь. Сочетайте её с частотой таймаутов и частотой ошибок, чтобы отличать «медленную» работу от «сбоев». Как часто следует повторно запускать тесты производительности? При каждом значимом изменении архитектуры или БД, и как минимум один раз за цикл выпуска. Самый быстрый способ потерять показатель производительности — это выпустить изменения, копившиеся три месяца, и предположить, что он всё ещё актуален. Является ли возврат кода 429 под нагрузкой ошибкой? Нет — если это сделано намеренно, система защищает запросы, которые она может обработать. Ошибка - принимать неограниченную нагрузку и снижать производительности для всех пользователей вместо того, чтобы избавиться от чрезмерных запросов. Источник: https://thecodeman.net/posts/capacity-planning-for-dotnet-apis-from-guessing-to-measured-scaling

День 2695. #ЗаметкиНаПолях Планирование Мощностей для API. Продолжение Начало Практика Проект production-scaling-lab — это небольшой API на ASP.NET Core 8, чтобы наблюдать за этими сбоями. В нём 2 примера конечных точек: - io-bound – для эмуляции нагрузки на соединения с БД, - orders – для эмуляции нагрузки на запись. Нагрузка на соединения В конечной точке io-bound ожидание заменяет собой вызов нижестоящего узла или медленный запрос — ресурсоёмкая работа, которая незаметно потребляет потоки и соединения под нагрузкой:
app.MapGet("/api/io-bound", 
 async (int delayMs, CancellationToken ct) =>
 {
   var boundedDelay = Math.Clamp(delayMs, 5, 5000);
   await Task.Delay(boundedDelay, ct);
   return Results.Ok(
    new { delayMs = boundedDelay, at = DateTime.UtcNow });
 });
Скрипт k6 постепенно увеличивает количество виртуальных пользователей с 200 до 1000 и устанавливает целевой уровень p95. Это базовый тест — он показывает, где начинает расти задержка:
// k6/connections.js
export const options = {
  stages: [
    { duration: '30s', target: 200 },
    { duration: '1m', target: 1000 },
    { duration: '30s', target: 0 }
  ],
  thresholds: {
    http_req_duration: ['p(95)<400'],
    http_req_failed: ['rate<0.01']
  }
};
 
export default function () {
  const response = http.get('http://localhost:5080/api/io-bound?delayMs=30');
  check(response, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}
Запустите его с выводом отчёта и посмотрите на p95:
set K6_WEB_DASHBOARD=true
set K6_WEB_DASHBOARD_EXPORT=report.html
k6 run k6/connections.js
По мере роста параллельных запросов время ответа начинает превышать порог в 400мс. Это ваше реальное значение пропускной способности для этой конечной точки — не момент, где возникает ошибка, а момент, где скорость работы снижается. Путь записи Обычно нельзя просто позволять неограниченному количеству записей накапливаться в БД и надеяться на лучшее. В проекте перед конечной точкой order устанавливается шлюз, так что после превышения лимита параллелизма она возвращает 429 вместо того, чтобы зависнуть:
app.MapPost("/api/orders", async (
  CreateOrderRequest request,
  AppDbContext db,
  WriteGate writeGate,
  CancellationToken ct) =>
{
  … 
  
  if (!await writeGate.TryEnterAsync(ct))
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
 
  try
  {
    …
    db.Orders.Add(order);
    …
    await db.SaveChangesAsync(ct);
    …
  }
  finally
  {
    writeGate.Exit();
  }
});
Сам шлюз представляет собой обычный SemaphoreSlim с коротким временем ожидания. Если за 250мс не удаётся получить слот, запрос отбрасывается, а не ставится в очередь:
public sealed class WriteGate
{
  private readonly SemaphoreSlim _semaphore;
  private int _inflight;
 
  public WriteGate(IConfiguration conf)
  {
    var max = conf.GetValue<int?>("MaxConcurrentWrites") ?? 64;
    _semaphore = new SemaphoreSlim(max, max);
  }
 
  public int CurrentInflight => _inflight;
 
  public async Task<bool> TryEnterAsync(CancellationToken ct)
  {
    var acq = await _semaphore.WaitAsync(
      TimeSpan.FromMilliseconds(250), ct);
    if (acq) Interlocked.Increment(ref _inflight);
      return acq;
  }
 
  public void Exit()
  {
    Interlocked.Decrement(ref _inflight);
    _semaphore.Release();
  }
}
Вот что сбивает с толку новичков: ошибка 429 при экстремальной нагрузке — это хороший результат. Т.е. система предпочла защитить запросы, которые она может обработать, вместо того чтобы принимать все запросы и не зависать. Планирование нагрузки частично заключается в том, чтобы заранее определить, где проходит эта граница. Тест на пиковые нагрузки записи увеличивает скорость поступления запросов до 800 в секунду, чтобы найти эту границу:
// k6/write-spike.js
export const options = {
  scenarios: {
    write_spike: {
      executor: 'ramping-arrival-rate',
      startRate: 50,
      timeUnit: '1s',
      preAllocatedVUs: 100,
      maxVUs: 800,
      stages: [
        { target: 100, duration: '30s' },
        { target: 500, duration: '1m' },
        { target: 800, duration: '30s' },
        { target: 0,   duration: '20s' }
      ]
    }
  },
  thresholds: {
    http_req_duration: ['p(95)<700'],
    http_req_failed: ['rate<0.02']
  }
};
Кроме того, пример API имеет конечную точку с метриками http://localhost:5080/api/metrics, где можно посмотреть общее число заказов, длину очереди на обработку, количество прочитанных заказов, количество обрабатываемых заказов. Окончание следует… Источник: https://thecodeman.net/posts/capacity-planning-for-dotnet-apis-from-guessing-to-measured-scaling

День 2694. #ЗаметкиНаПолях Планирование Мощностей для API. Начало Представьте митинг по релизу. Кто-то спрашивает: «Сможет ли API справиться с "чёрной пятницей"?» - тишина. Старший инженер говорит что-то вроде: «Скорей всего. Мы сейчас используем более мощные серверы», - и все переходят к следующему вопросу. Эта фраза — догадка, произнесённая уверенным голосом, а через 3 недели она превращается в инцидент в 2 часа ночи. Планирование мощностей — то, что поможет в этом случае дать реальный ответ. Не ощущение, а измеренное число с указанием условий измерения рядом с ним. Рассмотрим, как это сделать на практике в ASP.NET Core API. Метрики CPU и памяти обманчивы Многие планы загрузки ресурсов представляют собой всего два графика: CPU и память. У сервера есть запас по обоим параметрам, поэтому вывод: «всё в порядке». Затем трафик резко возрастает, и API сбоит, в то время как CPU загружен на 40%. Дело в том, что проблемы, которые действительно возникают первыми, редко проявляются как перегрузка CPU: 1. Исчерпание пула соединений - приложение исчерпывает количество соединений с БД задолго до того, как БД исчерпает ресурсы CPU, и очередь запросов будет ожидать свободного соединения. 2. Голодание пула потоков - несколько блокирующих вызовов под нагрузкой приводят к тому, что пул потоков не может достаточно быстро расти, и задержка резко возрастает, даже если ни один ресурс не выглядит перегруженным. 3. Конкуренция за блокировки - горячая блокировка превращает параллельную работу в непрерывную обработку одного файла. 4. Задержка в очереди - фоновая обработка отстаёт, поэтому запись "успешно выполняется", но пользователь не видит результата в течение 30 секунд. 5. Коллапс задержки p95 - среднее значение по-прежнему выглядит отлично, но 1 запрос из двадцати выдаёт тайм-аут. Таким образом, вопрос, на который отвечает планирование мощностей, не в том, "есть ли у нас свободные ресурсы CPU". Он более узкий и полезный: «Какой объем трафика может выдержать этот API, прежде чем качество обслуживания пользователей начнёт ухудшаться?» Все остальное служит для ответа на этот вопрос. Полезные для измерения показатели Самая большая ошибка - отслеживание средней задержки. Средние значения скрывают тех, кто действительно страдает. Если средний ответ 80мс, а p99 — 4 секунды, значимая часть пользователей испытывает серьёзные проблемы. Вот что важно отслеживать (в порядке убывания значимости для выявления реальных проблем): 1. Задержки p95 и p99 - в хвосте задержек источник проблем. p95 — ваш SLO; p99 – насколько всё плохо, в худших случаях. 2. Запросы в секунду (RPS) - для каждого класса конечных точек, а не одно глобальное число. Конечная точка чтения и конечная точка записи не имеют ничего общего. 3. Одновременные пользователи/запросы в процессе выполнения - сколько запросов одновременно выполняется, что фактически создаёт нагрузку на пулы и потоки. 4. Частота тайм-аутов и частота ошибок - разница между «медленно» и «сломано». 5. Использование пула соединений с БД - наиболее распространённый скрытый потолок в API. 6. Глубина очереди и задержка обработки - для всего асинхронного, насколько отстают фоновые обработчики. Если хотите добавить только один параметр на панель мониторинга, пусть это будет p95 для каждой конечной точки. Это сразу же меняет ситуацию. Базовый цикл Планирование мощностей — не документ, который вы пишете один раз. Это цикл, который вы запускаете, и он всегда выглядит одинаково: 1. Сначала определите SLO: «p95 менее 300мс, частота ошибок менее 1%» — это целевой показатель, по которому можно проводить тестирование. Зафиксируйте это число, прежде чем что-либо запускать, иначе вы будете изменять правила игры в зависимости от результатов. 2. Проведите тест на установившееся состояние (steady-state) при нагрузке, которая, по вашему мнению, отражает нормальный трафик. Это точка отсчёта. 3. Проведите тест на пиковую нагрузку с резким и быстрым нарастанием. Steady-state-тест показывает крейсерскую высоту; пиковая нагрузка показывает, что произойдёт, когда начнётся «чёрная пятница». 4. Найдите первое узкое место и устраните только его. Когда система даёт сбой, что-то выходит из строя в первую очередь. Исправьте это, затем проведите повторное тестирование — потому что исправление обычно просто перемещает потолок к следующему узкому месту, и нужно это заметить. 5. Заложите запас прочности, затем публикуйте. После того, как вы окажетесь в пределах SLO, заложите 30-50% запаса сверх ожидаемой пиковой нагрузки и запишите полученное число. Пиковая нагрузка, значения которой никто не знает, равносильна её отсутствию. Продолжение следует… Источник: https://thecodeman.net/posts/capacity-planning-for-dotnet-apis-from-guessing-to-measured-scaling

День 2693. #Курсы День Модернизации с Помощью Агентов в .NET 16 июля 2026г. с 19:00 до 23:00 мск. Формат: онлайн Язык: английский День модернизации с помощью агентов помогает разработчикам .NET модернизировать существующие приложения без переписывания кода — таким образом, вы можете тратить меньше времени на борьбу с устаревшим кодом и больше времени на разработку новых функций, повышение производительности и создание приложений на базе ИИ в Azure. 19:00 - Modernize .NET Apps with GitHub Copilot (Mika Dumont, Microsoft) В этом докладе покажут, как модернизация с GitHub Copilot помогает командам оценивать, планировать и выполнять обновления .NET, используя ИИ. Продемонстрируют реальные сценарии миграции, как ИИ сокращает ручные усилия, риски и время. Вы получите практические стратегии, чтобы начать работу уже сегодня. 19:30 - Stop Rewriting. Start with Aspire (David Pine, Microsoft) Мы рассмотрим процесс внедрения Aspire — aspire init и новый навык агента Aspireify — и позволим ему модернизировать существующее приложение .NET в режиме реального времени. 20:00 - Cold Case or Active Asset? A 2005 WinForms LoB Application (Merrie McGaw, Klaus Loeffelmann, Microsoft) Приложение непрерывно используется уже почти 20 лет: специализированная бизнес-система, построенная на SQL Server, и .NET Framework 2.0 с использованием ранних версий Windows Forms. Мерри и Клаус исследуют, что необходимо для развития устаревшего приложения WinForms. Только реальный код с реальной историей и прагматичный анализ того, насколько далеко может зайти модернизация. 20:30 - Cloud-Native Data Without Starting Over (Jerry Nixon, Microsoft) Многие бизнес-приложения тесно связаны с данными, поэтому миграция в облако кажется рискованной, дорогостоящей и сопряженной с трудностями. Но что, если сначала можно перенести БД? Мы рассмотрим практические шаблоны для поэтапной миграции без переписывания кода или архитектурного хаоса. Благодаря стабильным API-слоям, современным шаблонам доступа к данным и новым инструментам мы продемонстрируем, как приложения могут развиваться с помощью стратегий, разработанных для реальных систем, бюджетов и инженерных команд. 21:00 - Modernize Fast, Migrate Faster: .NET Apps to Azure with Modernize CLI (Kaushaya Ganguly, Microsoft) Мы пройдемся по реальному .NET-приложению с Modernize CLI от начала до конца, а затем используем тот же инструмент для модернизации кодовой базы, чтобы подготовить её к работе в Azure, выделения инфраструктуры Azure и развёртывания в Azure App Service. Рассмотрим пакетную модернизацию для параллельного выполнения одного и того же рабочего процесса во многих репозиториях, интеграцию с конвейером CI/CD для превращения модернизации в повторяемый этап сборки, а также пользовательские навыки для интеграции шаблонов миграции вашей организации, внутренних библиотек и стандартов кодирования. 21:30 - Modernize .NET Apps and Add Agentic Functionality in Minutes (Gaurav Seth, Andrew Westgarth, Jordan Selig, Microsoft) Модернизируйте устаревшие приложения, разместив их в управляемом экземпляре в Azure App Service без переписывания кода, а затем мгновенно предоставьте к ним доступ как к инструментам, управляемым ИИ-агентами, используя встроенный MCP. 22:00 - App Modernization Done. Now Let's Make It Smarter (Bruno Capuano, Microsoft) Итак, вы перенесли свое приложение, что дальше? Мы рассмотрим практические способы внедрения ИИ в существующее приложение, размещённое в Azure, с помощью Microsoft Foundry. Рассмотрим доступные сценарии после миграции, такие как анализ журналов приложения, извлечение полезной информации или создание более интеллектуальных отчетов — без необходимости перестраивать всё с нуля. Регистрация по ссылке: https://developer.microsoft.com/en-us/reactor/events/27243/

День 2692. #ВопросыНаСобеседовании Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании. 35. SignalR для веб-функциональности в реальном времени «Расскажите, как реализовать систему уведомлений в реальном времени с использованием SignalR в приложении .NET? Опишите ключевые компоненты и как вы обеспечите масштабируемость и производительность системы». Хороший ответ Во-первых, нужно добавить библиотеку SignalR в проект .NET:
dotnet add package Microsoft.AspNetCore.SignalR
После этого создадим класс Hub, который выступает в качестве центрального координатора для входящих и исходящих сообщений. Здесь мы можем определить методы, которые клиенты будут вызывать для отправки сообщений на сервер. Сервер затем может транслировать эти сообщения другим подключённым клиентам:
public class NotificationHub : Hub
{
  public async Task SendNotification(string message)
  {
    await Clients.All.SendAsync("ReceiveNotification", message);
  }
}
В Program.cs зарегистрируем маршруты SignalR и убедимся в правильной конфигурации для использования WebSockets как метода транспортировки для лучшей производительности и снижения задержек:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();
app.MapHub<NotificationHub>("/notifications");

app.Run();
На стороне клиента в проект необходимо добавить клиентскую js-библиотеку signalr. Затем используем JavaScript-клиент SignalR для подключения к хабу и определения функций отправки и получения сообщений:
const conn = new signalR.HubConnectionBuilder()
  .withUrl("/notifications")
  .build();

conn.on("ReceiveNotification", function(message) {
  console.log("New notification: " + message);
});

async function start() {
  try {
    await connection.start();
    console.log("SignalR Connected.");
  } catch (err) {
    console.log(err);
    setTimeout(start, 5000);
  }
};

conn.onclose(start);
start();
Для масштабирования приложения SignalR можно использовать Azure SignalR Service, который переносит управление подключениями и масштабирование на управляемый сервис, способный обрабатывать миллионы одновременных подключений. В конфигурации приложения просто изменим настройки подключения для интеграции с Azure SignalR Service:
builder.Services
 .AddSignalR()
 .AddAzureSignalR("строка подключения");
Эта реализация охватывает создание хаба, конфигурацию в приложении и взаимодействие на стороне клиента, обеспечивая надёжные и масштабируемые возможности работы в режиме реального времени. Использование Azure SignalR Service помогает управлять и масштабировать приложение по мере увеличения нагрузки пользователей, обеспечивая бесперебойную работу в режиме реального времени. Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md

День 2691. #ЗаметкиНаПолях DRY — Самый Неправильно Понимаемый Принцип Программирования Разработчики рано усваивают принцип DRY, и почти все - неправильно. Видишь два одинаковых фрагмента кода, выдели метод, удали дубликат. Такая практика может привести к очень плохому коду: - Вспомогательный метод, в который каждый спринт добавляется новый логический параметр. - Базовый класс, к которому никто не хочет прикасаться, т.к. от него наследуются шесть других несвязанных между собой. - «Общий» модуль, от которого зависят две разные части системы, поэтому ни одна из них не может измениться без другой. Каждый случай начинался как невинная попытка не повторяться. Что на самом деле говорит DRY Оригинальное определение из книги «Программист-прагматик»: «Каждое знание должно иметь единственное, однозначное, авторитетное представление в системе.» Речь о знаниях, а не о коде. Один факт о предметной области (правило скидки или формат номера счёта), должен храниться ровно в одном месте. Когда факт меняется, вы меняете его в одном месте. Ошибка: Дедупликация кода, а не знаний Два фрагмента кода могут выглядеть идентично, но представлять совершенно разные знания. Допустим, вы проверяете два адреса: адрес доставки клиента и адрес склада. Сегодня правила идентичны:
public bool IsValid(Address addr) =>
 !string.IsNullOrWhiteSpace(addr.Street) &&
 !string.IsNullOrWhiteSpace(addr.City) &&
 …
DRY призывает выделить один валидатор и вызывать его из обоих мест. Но это разные концепции, которые пока(!) имеют общие правила. Когда складу понадобится номер погрузочных ворот, вы вернётесь в общий метод и добавите флаг, чтобы другой вызывающий код продолжал работать:
public bool IsValid(Address addr, bool hasDock = false) =>
 !string.IsNullOrWhiteSpace(addr.Street) &&
 …
 (!hasDock || !string.IsNullOrWhiteSpace(addr.DockCode));
Когда общий метод начинает принимать флаг, из-за которого ведёт себя по-разному, у вас не было дублирования. Были две похожие вещи, которые вы просто склеили. Неправильная абстракция обходится дороже, чем дублирование. Вызывающие методы зависят от неё и подгоняют её под себя, флаги накапливаются, и в итоге вы боитесь трогать метод, который больше не понимаете. Границы модулей Внутри одного класса плохой вспомогательный метод раздражает. Общий код, переходящий через границы модулей это уже структурный ущерб. Представьте модульный монолит с модулями счетов и доставки. В обоих есть класс заказа. Инженер с благими намерениями замечает, что классы имеют общие поля, и объединяет их в один, на который ссылаются оба модуля:
public class Order
{
  public Guid Id { get; set; }
  public string CustomerName { get; set; }
  public decimal Total { get; set; }
  // … другие поля из обоих модулей
}
Теперь модули не могут развиваться независимо. Два модуля, каждый из которых имеет собственный класс заказа, — суть хранения данных в их границах. Формы могут быть похожими, моделируя одну и ту же реальную вещь с двух точек зрения, которые меняются со временем. Правило: Дождитесь третьего раза Не рефакторьте первый повтор. Дождитесь третьего раза и спросите себя: если это правило изменится, должны ли все копии так же измениться? Да — это дублирование, примените DRY. Нет — это совпадение, оставьте как есть, объединение обойдётся вам дороже позже. Пусть код повторяется до тех пор, пока правильная абстракция не станет очевидной, потому что хорошие абстракции обнаруживаются на конкретных примерах, а не угадываются заранее. Некоторые называют это AHA (Avoid Hasty Abstractions - Избегайте поспешных абстракций). Извлекайте информацию, когда можете дать название концепции. Реальное доменное имя: Money, TaxRate или InvoiceNumber - вероятно, представляет собой общее знание. Если лучшее имя, которое вы можете найти, — Helper, Utils или ProcessData, вы абстрагируете форму, а не знания. Источник: https://www.milanjovanovic.tech/blog/dry-is-the-most-misunderstood-rule-in-programming

День 2690. #ЗаметкиНаПолях Темпоральные таблицы в EF Core. Часть 5 1. Что это? 2. Настройка 3. Запросы 4. Добавление версионирования и производительность   Распространенные ошибки и способы их избежать Ошибка 1: Скафолдинг темпоральных таблиц при обратном проектировании Если вы используете шаблон dotnet ef dbcontext для существующей темпоральной БД, EF Core может некорректно определить конфигурацию таблиц. Всегда проверяйте созданный шаблон DbContext и добавляйте конфигурацию IsTemporal() вручную, если это необходимо.   Ошибка 2: Мягкое удаление и темпоральные таблицы Если ваша сущность использует паттерн мягкого удаления (флаг IsDeleted), темпоральные таблицы будут отслеживать изменения этих флагов. Т.е. «удалённые» строки по-прежнему будут находиться в основной таблице, просто с IsDeleted = true — и каждый раз, когда кто-то запрашивает историю, ему нужно будет это учитывать. Подумайте, нужны ли вам оба механизма, или достаточно жёсткого удаления и темпоральных таблиц для соответствия вашим требованиям.   Ошибка 3: Массовые операции в обход EF Core Это позволяет обойти отслеживание изменений EF Core, но при этом работает с темпоральными таблицами, поскольку они задаются на уровне SQL Server:
await _db.Database.ExecuteSqlRawAsync(
 "UPDATE Orders SET Status = 'Archived' WHERE CreatedAt < {0}",
 DateTime.UtcNow.AddYears(-1));
Если вы используете стороннюю библиотеку для пакетной обработки запросов, которая напрямую вызывает SqlBulkCopy, имейте в виду, что она обходит триггеры, но НЕ темпоральные таблицы — они обрабатываются на уровне движка.   Ошибка 4: Миграции EF Core на темпоральные таблицы требуют осторожности При добавлении нового столбца в темпоральную таблицу EF Core также должен добавить его в таблицу истории. EF Core обрабатывает это автоматически в миграциях, но если вы когда-либо вручную изменяли таблицу истории, миграция завершится неудачей. Никогда не изменяйте схему таблицы истории напрямую.   Ошибка 5: Забывание о UTC Временные метки в таблицах всегда в формате UTC. Если ваше приложение использует местное время, преобразования могут вызвать трудно отслеживаемые ошибки в запросах к историческим данным:
// ❌ Используется местное время
var asOf = DateTime.Now.AddDays(-1);
 
// ✅ Всегда используйте UTC
var asOf = DateTime.UtcNow.AddDays(-1);
 
var snapshot = await _db.Orders
  .TemporalAsOf(asOf)
  .FirstOrDefaultAsync(o => o.Id == orderId);
  Когда НЕ следует использовать темпоральные таблицы - Таблицы с высокой частотой записи (например, телеметрия в реальном времени, состояние сессии) Накладные расходы на запись и рост истории будут проблемой. Вместо этого используйте выделенную БД временных рядов или хранилище событий.   - Конфиденциальность персональных данных/GDPR Темпоральные таблицы затрудняют удаление данных — таблица истории сохраняет старые версии. Если вам необходимо поддерживать запросы «права на забвение», вам потребуется реализовать процесс удаления вручную, который отключает версионирование, очищает историю и снова включает его.   - Таблицы с BLOB-объектами или большими столбцами NVARCHAR(MAX) Каждое обновление копирует всю строку в историю, включая большие поля. Это может привести к чрезвычайно быстрому росту таблицы истории. - Требования к согласованности между базами данных Временные метки являются индивидуальными для каждой БД. Если бизнес-транзакция охватывает несколько БД, временные записи будут иметь немного разные метки времени, что делает восстановление данных на определённый момент времени между базами данных ненадёжным.   Источник: https://thecodeman.net/posts/temporal-tables-efcore-auditing-history

День 2689. #ЗаметкиНаПолях Темпоральные таблицы в EF Core. Часть 4 1. Что это? 2. Настройка 3. Запросы   Безопасная миграция в производственной среде Изменение существующей таблицы на темпоральную — более деликатный процесс, чем создание её с нуля. Безопаснее всего создать ручную миграцию с чистым SQL:
public partial class TemporalOnOrders : Migration
{
  protected override void Up(MigrationBuilder mb)
  {
    // Добавляем столбцы периода
    mb.Sql(@"
  ALTER TABLE [Orders] ADD
  [ValidFrom] DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN
    CONSTRAINT DF_Orders_ValidFrom DEFAULT '2000-01-01 00:00:00.0000000',
  [ValidTo] DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN
    CONSTRAINT DF_Orders_ValidTo DEFAULT '9999-12-31 23:59:59.9999999',
  PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]);
    ");
 
     // Включаем версионирование
    mb.Sql(@"
  ALTER TABLE [Orders]
  SET (SYSTEM_VERSIONING = ON (
    HISTORY_TABLE = [audit].[OrdersHistory],
    DATA_CONSISTENCY_CHECK = ON));
    ");
  }
 
  protected override void Down(MigrationBuilder migrationBuilder)
  {
    mb.Sql(@"
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = OFF);
  ALTER TABLE [Orders] DROP PERIOD FOR SYSTEM_TIME;
  ALTER TABLE [Orders] DROP COLUMN [ValidFrom];
  ALTER TABLE [Orders] DROP COLUMN [ValidTo];
  DROP TABLE IF EXISTS [audit].[OrdersHistory];
   ");
  }
}
Замечание: В производственной среде вы, наверное, не захотите удалять историю, а просто отключите версионирование.   Производительность записи Темпоральные таблицы не бесплатны. Каждое обновление или удаление требует от SQL Server записи дополнительной строки в таблицу истории. Для таблиц с высокой нагрузкой на запись это может быть значительным. В типичных сценариях OLTP накладные расходы - 5–15%. Для сценариев с высокой нагрузкой следует оценить, оправдывает ли необходимая детализация истории эти затраты.   Рост таблиц истории Таблицы истории со временем могут значительно увеличиваться в размерах. Можно архивировать старые данные:
public async Task ArchiveOldHistoryAsync()
{
  var cutoff = DateTime.UtcNow.AddYears(-2);
 
  await _db.Database.ExecuteSqlRawAsync(@"
  -- Перемещаем в архив
  INSERT INTO [audit].[OrdersHistoryArchive]
  SELECT * FROM [audit].[OrdersHistory]
   WHERE [ValidTo] < {0};
 
  -- Удаляем историю (надо отключить версионирование)
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = OFF);
 
  DELETE FROM [audit].[OrdersHistory]
   WHERE [ValidTo] < {0};
 
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = ON (
   HISTORY_TABLE = [audit].[OrdersHistory]));
  ", cutoff);
}
  Производительность чтения Функции TemporalAll() и TemporalBetween() сканируют таблицу истории. Для панелей аудита с большими таблицами истории всегда используйте агрессивную фильтрацию и правильные индексы.   EF Core создаёт кластерный индекс по столбцам периода в таблице истории. Для запросов к конкретным сущностям на определённый момент времени добавьте некластерный индекс:
CREATE NONCLUSTERED INDEX IX_OrdersHistory_IdValidFrom
ON [audit].[OrdersHistory] ([Id], [ValidFrom] DESC);
  Окончание следует…   Источник: https://thecodeman.net/posts/temporal-tables-efcore-auditing-history

День 2688. #ЗаметкиНаПолях Темпоральные таблицы в EF Core. Часть 3 1. Что это? 2. Настройка   Запросы Стандартные операции EF Core (вставка, изменение, удаление) автоматически запускают операции аудита. Чтение текущих значений так же не отличается от обычного.   Расширенные шаблоны запросов EF Core 7+ предоставляет 5 операторов темпоральных запросов.   1. TemporalAll() — полная история для сущности
var orderHistory = _db.Orders
 .TemporalAll()
 .Where(o => o.Id == orderId)
 .OrderBy(o => EF.Property<DateTime>(o, "ValidFrom"))
 .Select(o => new OrderAuditEntry
 {
   ShippingAddress = o.ShippingAddress,
   Status = o.Status,
   TotalAmount = o.TotalAmount,
   ValidFrom = EF.Property<DateTime>(o, "ValidFrom"),
   ValidTo = EF.Property<DateTime>(o, "ValidTo")
 })
 .ToListAsync();
}
Это покажет нам всю историю заказа (и ответит на претензию клиента – мы будем знать, когда изменился адрес).   2. TemporalAsOf() – снепшот на точку во времени Воссоздаёт состояние сущности на определённый момент:
var asOf = DateTime.UtcNow.AddDays(-2);
var order = _db.Orders
  .TemporalAsOf(asOf)
  .Include(o => o.Items)
  .FirstOrDefaultAsync(o => o.Id == orderId);
Заметьте: при использовании TemporalAsOf() с Include() EF Core применяет временной фильтр и к связанной сущности. В результате вы получаете OrderItems, существовавшие на тот момент времени, а не текущие позиции.   3. TemporalBetween() — изменения в пределах временного окна Найти все заказы, которые были изменены в течение определённого временного окна:
var timeWindow = new
{
  Start = new DateTime(2026, 6, 11, 3, 0, 0, DateTimeKind.Utc),
  End   = new DateTime(2026, 6, 11, 4, 0, 0, DateTimeKind.Utc)
};
 
var changed = await _db.Orders
 .TemporalBetween(timeWindow.Start, timeWindow.End)
 .Select(o => new
 {
   o.Id,
   o.Status,
   ValidFrom = EF.Property<DateTime>(o, "ValidFrom")
 })
 .ToListAsync();
  4. TemporalFromTo() — перекрывающиеся временные диапазоны Cтроки, которые были активны в течение заданного диапазона, — даже если были созданы раньше:
var ordersActiveLastWeek = await _db.Orders
  .TemporalFromTo(
    DateTime.UtcNow.AddDays(-7),
    DateTime.UtcNow)
  .Where(o => o.Status == "Processing")
  .ToListAsync();
  5. TemporalContainedIn() — строки, полностью находящиеся в заданном диапазоне Только строки, которые были созданы и удалены в пределах указанного диапазона:
var contained = await _db.Orders
 .TemporalContainedIn(
   DateTime.UtcNow.AddHours(-1),
   DateTime.UtcNow)
 .ToListAsync();
  Продолжение следует…   Источник: https://thecodeman.net/posts/temporal-tables-efcore-auditing-history

День 2687. #ЗаметкиНаПолях Темпоральные таблицы в EF Core. Часть 2 Часть 1 Настройка темпоральных таблиц в EF Core Системные требования: - .NET 6+ - EF Core 6+ (Microsoft.EntityFrameworkCore.SqlServer) - SQL Server 2016+ (или Azure SQL Database) 1. Определяем сущности
// Models/Order.cs
public class Order
{
  public int Id { get; set; }
  public int CustomerId { get; set; }
  // …

  public Customer Customer { get; set; } = null!;
  public List<OrderItem> Items { get; set; } = [];
}

// Models/OrderItem.cs
public class OrderItem
{
  public int Id { get; set; }
  public int OrderId { get; set; }
  public string Product { get; set; } = "";
  public int Quantity { get; set; }
  public decimal Price { get; set; }
 
  public Order Order { get; set; } = null!;
}
Обратите внимание, что сами сущности не имеют столбцов аудита — это исключительно вопрос конфигурации уровня данных. 2. Настраиваем DbContext
// Data/AppDbContext.cs
public class AppDbContext : DbContext
{
  public DbSet<Order> Orders => Set<Order>();
  public DbSet<OrderItem> OrderItems => Set<OrderItem>();
  public DbSet<Customer> Customers => Set<Customer>();
 
  //…

  protected override void 
   OnModelCreating(ModelBuilder mb)
  {
    mb.Entity<Order>(e =>
    {
      e.ToTable("Orders", x => x.IsTemporal(t =>
      {
        t.HasPeriodStart("ValidFrom");
        t.HasPeriodEnd("ValidTo");
        t.UseHistoryTable("OrdersHistory", "audit");
      }));
 
      e.Property(o => o.TotalAmount)
       .HasColumnType("decimal(18,2)");
 
      e.Property(o => o.Status)
       .HasMaxLength(50);
    });
 
    mb.Entity<OrderItem>(e =>
    {
      e.ToTable("OrderItems", x => x.IsTemporal(t =>
      {
        t.HasPeriodStart("ValidFrom");
        t.HasPeriodEnd("ValidTo");
        t.UseHistoryTable("OrderItemsHistory", "audit");
      }));
 
      e.Property(i => i.UnitPrice)
       .HasColumnType("decimal(18,2)");
    });
  }
}
Несколько нюансов: - схема audit - таблицы истории размещены в отдельной схеме, что позволяет сохранить основную схему чистой и упрощает управление правами доступа (например, читать audit могут только администраторы); - явное указание имён столбцов периода - ValidFrom/ValidTo — общепринятые имена, но вы можете использовать PeriodStart/PeriodEnd, если хотите; - и Order и OrderItem, являются временными - нам нужна полная картина. 3. Создаём миграцию
dotnet ef migrations add AddTemporalTables
dotnet ef database update
SQL Server сгенерирует что-то вроде этого:
sql 
CREATE TABLE [Orders] (
[Id]         INT       NOT NULL IDENTITY,
[CustomerId] INT       NOT NULL,
…
[ValidFrom]  DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
[ValidTo]    DATETIME2 GENERATED ALWAYS AS ROW END   NOT NULL,
  PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),
  CONSTRAINT [PK_Orders] PRIMARY KEY ([Id])
)
WITH (SYSTEM_VERSIONING = ON (
 HISTORY_TABLE = [audit].[OrdersHistory]
));
Продолжение следует… Источник: https://thecodeman.net/posts/temporal-tables-efcore-auditing-history

День 2686. #ЗаметкиНаПолях Темпоральные таблицы в EF Core. Часть 1 Представьте, что вы создаёте платформу электронной коммерции. Клиент обращается в службу поддержки, утверждая, что его заказ был изменён без его согласия — адрес доставки изменился после оформления. Вашей операционной команде необходимо ответить на следующие вопросы: - Как выглядел этот заказ на момент его оформления? - Кто (или какой процесс) изменил адрес доставки и когда? - Можно ли восстановить предыдущее состояние, не нарушая ссылочную целостность? Наивный подход — добавить столбцы CreatedAt, UpdatedAt и ModifiedBy — показывает только дату последнего изменения. Вы не сохраняете историю. Самодельная таблица аудита изменений работает, но требует дисциплины: - каждый разработчик должен помнить о необходимости записи в неё, - каждый вызов SaveChanges() должен перехватываться. Темпоральные таблицы SQL Server решают эту проблему на уровне ядра БД, а EF Core 6+ предоставляет к ним доступ через чистый, первоклассный API. Что это? Темпоральные таблицы автоматически поддерживают полную историю изменений строк. Ядро БД отслеживает: - Текущее состояние каждой строки (основная таблица), - Каждое предыдущее состояние с точным временным диапазоном, в течение которого оно было актуальным (таблица истории). Два скрытых столбца периода datetime2 — обычно ValidFrom и ValidTo — определяют временной диапазон, в течение которого версия строки была актуальной. Они заполняются и управляются исключительно SQL Server, а не кодом приложения. При обновлении строки старая версия перемещается в таблицу истории, при этом значение ValidTo устанавливается в текущую метку времени UTC. При удалении строки происходит то же самое — запись остаётся только в таблице истории. Как это работает INSERT - В основную таблицу добавляется новая строка. - ValidFrom устанавливается в текущее время транзакции. - ValidTo устанавливается в 9999-12-31 23:59:59.9999999 («всё ещё актуально»). UPDATE SQL Server атомарно: - Копирует текущую строку в таблицу истории, устанавливая значение ValidTo равным времени транзакции. - Обновляет строку в основной таблице, устанавливая значение ValidFrom равным времени транзакции. DELETE - Текущая строка копируется в таблицу истории, устанавливая значение ValidTo равным текущему времени. - Строка в основной таблице удаляется. Все метки времени устанавливаются в UTC самим SQL Server. Ваше приложение не может их переопределить. Это фактически функция безопасности — она делает таблицу аудита изменений защищённой от несанкционированного изменения. Продолжение следует… Источник: https://thecodeman.net/posts/temporal-tables-efcore-auditing-history

День 2685. #ЗаметкиНаПолях #PowerShell Атрибуты Валидации Параметров в PowerShell. Окончание Начало ValidateNotNull Проверяет, что параметр не null.
function Process-Data {
  param(
    [ValidateNotNull()]
    [object]$Data
   )
   Write-Host "Обработка данных: $Data"
}

Process-Data -Data 'some data'  #✅
Process-Data -Data $null        #❌
ValidateNotNullOrEmpty Проверяет, что строка или коллекция не null и не пустая.
function Write-Log {
  param(
    [ValidateNotNullOrEmpty()]
    [string]$Message
  )
  Write-Host "Log: $Message"
}

Write-Log -Message 'Important event'  #✅
Write-Log -Message ''                 #❌
Write-Log -Message $null              #❌
ValidateNotNullOrWhiteSpace Аналогично предыдущему, проверяет, что строка не null, не пустая и не пробел. ValidateDrive Проверяет, что путь использует один из указанных дисков.
function Get-FileInfo {
  param(
    [ValidateDrive('C', 'D', 'Temp')]
    [string]$Path
  )
  Write-Host "Получаем данные для: $Path"
}

Get-FileInfo -Path 'C:\Windows\System32' #✅
# если диск Temp существует
Get-FileInfo -Path 'Temp:\file.txt'       #✅
Get-FileInfo -Path 'Z:\file.txt'          #❌
ValidateUserDrive Проверяет, что путь использует диск, созданный пользователем в PowerShell (а не встроенные C:, D:, и т.п.).
New-PSDrive -Name 'MyDrive' -PSProvider FileSystem -Root 'C:\MyFolder'

function Read-UserFile {
  param(
    [ValidateDrive()]
    [ValidateUserDrive()]
    [string]$Path
  )
  Get-Content -Path $Path
}

Read-UserFile -Path 'MyDrive:\file.txt'  #✅
Read-UserFile -Path 'C:\file.txt'        #❌
ValidateTrustedData С версии 6.1.1+ проверяет, что данные происходят из надёжного источника.
function Invoke-TrustedCommand {
  param(
    [ValidateTrustedData()]
    [string]$Command
  )
  Invoke-Expression $Command
}
Это используется в основном в сценариях, требующих повышенного внимания к безопасности, для обеспечения того, чтобы параметры поступали из доверенных источников, а не были введены пользователем. Комбинирование нескольких атрибутов К одному параметру можно применить несколько атрибутов проверки:

function Set-Configuration {
  param(
    [ValidateNotNullOrEmpty()]
    [ValidateLength(3, 50)]
    [ValidatePattern('^[a-zA-Z0-9_-]+$')]
    [string]$ConfigName
  )
  Write-Host "Установка конфигурации: $ConfigName"
}

Set-Configuration -ConfigName 'my-config_123' #✅
Set-Configuration -ConfigName 'ab'            #❌
Set-Configuration -ConfigName 'invalid@name'  #❌
Все атрибуты валидации проверяются в порядке появления. Если любая из валидаций завершается неудачей, ошибка выбрасывается немедленно. Источник: https://www.meziantou.net/powershell-parameter-validation-attributes.htm