C# 1001 notes
описание
Регулярные короткие заметки по C# и .NET. Просто о сложном для каждого. admin - @haarrp
Лучшие посты
за три месяца⚡️ GPT-5.6 РЕЛИЗ OpenAI выкатили сразу три новые модели. • Sol - заявлено, что модель мощнее Mythos. Доступ для платных пользователей обещают в течение 24 часов. На Terminal Bench 2.1 с настройкой Ultra модель выбивает рекордные 91,9%. Первые тестеры отдельно отмечают сильную работу с интерфейсами: она уверенно собирает UI для приложений и сайтов, а не просто генерирует сырой код. • Terra - уровень Fable 5. Будет доступна бесплатно. • Luna - еще одна бесплатная модель для всех. Помимо самой модели, показали 3 крупных продуктовых обновления: 1. ChatGPT Work 2. новое desktop-приложение ChatGPT 3. hosted sites, то есть размещение сайтов прямо через Chatgpt https://openai.com/ru-RU/live/
Полезный Docker-трюк для .NET: кэшируйте NuGet-пакеты через BuildKit, а не скачивайте их заново при каждой сборке. # syntax=docker/dockerfile:1.7 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY *.csproj . RUN --mount=type=cache,target=/root/.nuget/packages \ dotnet restore COPY . . RUN --mount=type=cache,target=/root/.nuget/packages \ dotnet publish -c Release -o /app NuGet-кэш не попадает в финальный image, но между сборками сохраняется. В итоге restore работает быстрее, особенно в CI, где зависимости обычно весят больше, чем сам код.
✔️ Одна строчка .Result роняет ваш ASP.NET Core при CPU 8 %: разбор hill-climbing в .NET 9 TL;DR. Один foo.GetAsync().Result внутри middleware превращает ASP.NET Core, державший 50k RPS на p99 = 40 мс, в сервис на 12k RPS с p99 = 4 с при CPU 8 %. Виноват не блокирующий вызов сам по себе. Виноват hill-climbing: фидбэк-луп в ThreadPool, внутри которого живёт дискретное преобразование Фурье. Разбираемся по исходникам CoreCLR, как это работает, воспроизводим эффект на ~80 строках кода и показываем, почему SetMinThreads это не лечение, а анестезия. https://habr.com/ru/articles/1040804/
⚡️Про Каналы в .NET Если в приложении нужно обрабатывать поток данных от продюсеров к потребителям, первым делом вспоминают про очереди или ConcurrentQueue. Но в асинхронном мире этого мало. Поэтому в .NET есть каналы. Канал устроен как пара из продюсера и потребителя. Когда продюсер пишет элемент в канал, он либо сразу уходит к потребителю, либо ждёт, если очередь переполнена. Потребитель, в свою очередь, может ждать новые элементы без блокировки потоков — всё работает на async/await. Главная сила каналов — балансировка нагрузки. Ограниченные каналы позволяют держать под контролем количество элементов. Это защищает приложение от перегрузки: если продюсер работает быстрее, чем потребитель, система сама притормозит поток данных. Неограниченные каналы — вариант попроще, они всегда принимают новые элементы, но это может обернуться непредсказуемым ростом памяти. Мини-пример: var channel = Channel.CreateBounded<int>(5); // producer _ = Task.Run(async () => { for (int i = 0; i < 10; i++) { await channel.Writer.WriteAsync(i); Console.WriteLine($"Produced {i}"); } channel.Writer.Complete(); }); // consumer await foreach (var item in channel.Reader.ReadAllAsync()) { Console.WriteLine($"Consumed {item}"); } Почему это лучше, чем ConcurrentQueue? Потому что Channel создан сразу с учётом асинхронности. Вам не нужно городить блокировки и таймеры ожидания — достаточно написать await reader.ReadAsync(), и код будет сам по себе масштабироваться без блокировки потоков. #sharp_view
Продвинутый C#-трюк: обновляй `Dictionary` без двойного поиска Многие пишут так: if (dict.TryGetValue(key, out var value)) { dict[key] = value + 1; } else { dict[key] = 1; } Проблема: ты сначала ищешь ключ через TryGetValue, а потом снова лезешь в словарь через dict[key]. В hot path это лишняя работа. Есть более взрослый вариант: using System.Runtime.InteropServices; ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault( dict, key, out var exists ); if (!exists) { count = 0; } count++; Что происходит: 1. C# получает ссылку прямо на значение внутри Dictionary 2. ключ ищется один раз 3. значение можно менять без повторного обращения 4. меньше лишних операций в tight loop Где это полезно: 1. счётчики событий 2. парсеры 3. агрегации 4. обработка логов 5. high-performance backend code Но есть важный нюанс: не меняй структуру словаря, пока держишь ref. То есть не делай Add, Remove, Clear рядом с этой ссылкой. Это не трюк для каждого CRUD-сервиса. Это инструмент для мест, где C# уже упёрся в производительность, и ты начинаешь выжимать лишние аллокации и лишние lookup’и.
🐳 «Используй Testcontainers вместо in-memory» - это только половина правды Все уже выучили: EF Core InMemory provider - не интеграционный тест. Он не ловит: - баги в LINQ-трансляции - ограничения БД - коллации - реальные типы колонок - поведение конкретного SQL-провайдера Окей, заменили на реальный PostgreSQL через Testcontainers. Победа? Не совсем. Вот что начинается дальше. 1. Вы получили «медленное враньё» вместо «быстрого» Поднимать контейнер на каждый тест-класс - быстрый способ превратить CI из 30 секунд в 8 минут. Нормальный вариант: - один контейнер на всю тестовую сессию - изоляция данных между тестами через Respawn - без пересоздания базы и контейнера каждый раз Respawn чистит таблицы с учётом графа foreign keys за миллисекунды. 2. Транзакционный откат ≠ реальный сценарий Трюк «обернули тест в транзакцию и откатили» красиво выглядит, но ломается, когда в коде есть: - свои транзакции - несколько SaveChanges - фоновые операции - поведение, завязанное на commit В итоге тестируется сценарий, которого в проде нет. 3. Самая коварная ловушка - общий DbContext Если тест и код используют один экземпляр DbContext, EF может вернуть данные из change tracker, а не из базы. Тест зелёный, но он врёт: реальный SQL-запрос мог вообще не выполниться. Между Act и Assert стоит чистить трекер: Db.ChangeTracker.Clear(); 4. Бонус, который теряют 90% команд - тест миграций Реальная БД позволяет прогнать EF-миграции на чистой схеме. Если миграция падает или схема разъехалась с моделью, вы узнаёте об этом в CI, а не в проде в пятницу вечером. Пример базового подхода: public class IntegrationTestBase : IAsyncLifetime { private static readonly PostgreSqlContainer _db = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .Build(); private Respawner _respawner = null!; protected AppDbContext Db = null!; public async Task InitializeAsync() { await _db.StartAsync(); var options = new DbContextOptionsBuilder<AppDbContext>() .UseNpgsql(_db.GetConnectionString()) .Options; Db = new AppDbContext(options); // Реальные миграции - заодно проверяем, что они накатываются await Db.Database.MigrateAsync(); await using var conn = new NpgsqlConnection(_db.GetConnectionString()); await conn.OpenAsync(); _respawner = await Respawner.CreateAsync(conn, new RespawnerOptions { DbAdapter = DbAdapter.Postgres, SchemasToInclude = ["public"] }); } // Сброс данных перед каждым тестом - без пересоздания контейнера protected async Task ResetAsync() { await using var conn = new NpgsqlConnection(_db.GetConnectionString()); await conn.OpenAsync(); await _respawner.ResetAsync(conn); // Иначе тест может читать из кеша, а не из БД Db.ChangeTracker.Clear(); } public Task DisposeAsync() => Task.CompletedTask; } Testcontainers - это не галочка «best practice», а смена философии. Без нормальной изоляции данных вы просто пересели с быстрого вранья на медленное. А как вы изолируете состояние БД между интеграционными тестами - Respawn, транзакции или пересоздание контейнера? #dotnet #csharp #testing #efcore
Аллокации, которых нет в коде: охота на скрытый боксинг в .NET 10 Самая дорогая аллокация в вашем сервисе та, которой нет в исходниках. Вы написали struct ради zero-allocation, прошли code review, а в проде Gen0-коллекции все равно идут косяком. Потому что между вашим кодом и машинным кодом стоит компилятор, и он молча упаковывает ваш value-тип в кучу там, где вы этого не просили — а на код-ревью этого не видно. TL;DR. Боксинг (boxing) в .NET - это не только object o = 42. Он прячется в вызовах интерфейсных методов на struct, в дефолтном ValueType.Equals, в params object[]-аргументах, в foreach по интерфейсу и в замыканиях. При этом часть “классических” примеров боксинга из старых гайдов на современном рантайме уже не аллоцирует — JIT научился их вырезать, и слепо копировать советы десятилетней давности вредно. Ниже — карта мест, где боксинг живёт и сейчас, отдельный разбор того, что рантайм уже оптимизировал, реальный мини-кейс, воспроизводимый бенчмарк на BenchmarkDotNet с MemoryDiagnoser, способ ловить упаковку через DOTNET_JitDisasm и dotnet-gcdump, и паттерны лечения без потери читаемости. О версиях и числах. Всё прверялось на .NET 10 (текущий LTS) и C# 13/14-уровне компилятора, Release, без отладчика, BenchmarkDotNet с MemoryDiagnoser. На .NET 8/9 поведение в основном такое же, но отдельные оптимизации JIT отличаются между мажорными версиями — поэтому главный принцип статьи: не верьте на слово (в том числе мне), гоняйте MemoryDiagnoser на своей версии рантайма. Числа в таблицах ниже - иллюстративные, порядок величины, а не точные замеры с вашего железа. Пролог: “у нас же всё на struct, откуда Gen0?” Сервис на горячем пути считает метрики: миллионы маленьких readonly struct-значений в секунду, никакого new, никаких классов в hot path. По задумке — ноль аллокаций. На дашборде — стабильный поток Gen0-коллекций раз в несколько секунд под нагрузкой. Профайлер показывает аллокации, но стек ведёт в метод, где в коде нет ни одного new. Там цикл по интерфейсу, пара вызовов .Equals(), передача значения в params-метод лога. Глазами — чисто. В машинном коде — box-инструкции на каждой итерации. Это и есть скрытый боксинг: компилятор C# и JIT упаковывают ваш struct в объект на куче, потому что в конкретной точке кода value-тип нужно представить как ссылочный. Симптом — Gen0-коллекции “из ниоткуда”, и его не видно ни в code review, ни в дампе, пока не посмотришь на IL или дизасм. Если тема близка - я регулярно разбираю такие штуки по C# и .NET (внутренности рантайма, перформанс, неочевидные грабли с замерами и дизасмом) в своём Telegram-канале: t.me/csharp_ci. Заходите, если интересно копаться глубже. Что такое боксинг и почему он стоит дорого Боксинг — это упаковка value-типа (struct, enum, примитив) в объект на управляемой куче. Рантайму нужно выделить заголовок объекта, скопировать туда значение и вернуть ссылку. Анбоксинг - обратная операция с проверкой типа. Цена не в самой инструкции, а в последствиях: каждая упаковка - это аллокация в Gen0. Много мелких аллокаций на горячем пути означают частые Gen0-коллекции, паузы (пусть и короткие), вытеснение полезных данных из кэша и общий рост CPU на ровном месте. На сервисе с SLA по p99 это бьёт по хвосту латентности так же, как и любая другая лишняя аллокация. В IL боксинг виден явно - инструкция box. Именно её мы и будем искать. Читать дальше: https://habr.com/ru/articles/1049236/
🔥 Хочешь быстрее расти в IT? Хватит учиться в одиночку В IT прокачивается тот, кто каждый день видит сильные идеи, новые инструменты, реальные задачи, вакансии и разборы. Окружение решает больше, чем кажется. Собрал папки и каналы, где можно быстрее влиться в нужное направление, следить за трендами и не вариться в своём пузыре. AI: t.me/ai_machinelearning_big_data Python: t.me/pythonl Linux: t.me/linuxacademiya Хакинг: t.me/linuxkalii DevOps: t.me/DevOPSitsec Docker: t.me/DevopsDocker Golang: t.me/Golang_google Rust: t.me/rust_code C++: t.me/cpluspluc C#: t.me/csharp_1001_notes Java: t.me/java_library JavaScript: t.me/javascriptv React: t.me/react_tg Frontend: t.me/front PHP: t.me/phpshka Android: t.me/android_its Мобильная разработка: t.me/mobdevelop Базы данных: t.me/sqlhub Data Science: t.me/data_analysis_ml Big Data: t.me/bigdatai Математика: t.me/data_math Физика: t.me/fizmat Kubernetes: t.me/kubernetc GameDev: https://t.me/gamedev Haskell: t.me/haskell_tg Собеседования и карьера: DS собеседования: t.me/machinelearning_interview Python собеседования: t.me/python_job_interview Папка с вакансиями: t.me/addlist/_zyy_jQ_QUsyM2Vi Папка Go разработчика: t.me/addlist/MUtJEeJSxeY2YTFi Папка Python разработчика: t.me/addlist/eEPya-HF6mkxMGIy Папка ML: https://t.me/addlist/2Ls-snqEeytkMDgy Папка Frontend: https://t.me/addlist/mzMMG3RPZhY2M2Iy Полезное сверху: ИТ-мемы: t.me/memes_prog Английский для программистов: t.me/english_forprogrammers ИИ и технологии: t.me/vistehno 954 ГБ open-source курсов: @courses ИТ-книги бесплатно: https://t.me/addlist/BkskQciUW_FhNjEy Max Ai: https://max.ru/ai_machinelearning_big_data Max python: https://max.ru/pythonl ТЕХНО: https://max.ru/vistehno Max Go: https://max.ru/Golang_google Max Linux: https://max.ru/linuxkalii Devops: https://max.ru/DevOPSitsec C#: https://max.ru/csharp_ci C++: https://max.ru/cpluspluc SQL: https://max.ru/sqlhub Java: https://max.ru/javatg Подписывайся на нужные направления и собирай себе ленту, которая реально двигает вперёд. Пока кто-то листает шум, ты будешь видеть инструменты, задачи и идеи, которые помогают расти в профессии.
Record-типы в C#: когда модель данных не должна быть обычным class record в C# удобен там, где объект описывает данные, а не поведение и идентичность. Типичный пример: public record User(string Name, int Age); Компилятор сам сгенерирует конструктор, Equals, GetHashCode, ToString и деконструкцию. Но главное отличие не в сокращении кода, а в семантике. Обычный class сравнивается по ссылке: var a = new UserClass("Alice", 25); var b = new UserClass("Alice", 25); Console.WriteLine(a == b); // false record сравнивается по значению: var a = new User("Alice", 25); var b = new User("Alice", 25); Console.WriteLine(a == b); // true Это делает record хорошим выбором для DTO, read models, value objects, событий, результатов запросов и моделей, где важны значения полей. Ещё одна удобная вещь - with. Можно создать копию объекта, изменив только нужные поля: var user = new User("Alice", 25); var updated = user with { Age = 26 }; Console.WriteLine(updated); // User { Name = Alice, Age = 26 } Но есть важный нюанс: with делает поверхностную копию. Если внутри есть изменяемая коллекция, она не станет автоматически immutable. public record Team(string Name, List<string> Members); Такой record всё ещё может меняться через Members.Add(...). Поэтому для реально неизменяемых моделей лучше использовать immutable-коллекции или аккуратно закрывать доступ к изменяемому состоянию. record не заменяет class везде. Если у объекта есть жизненный цикл, identity, состояние и бизнес-поведение, обычный класс часто будет честнее. Но когда тип нужен как чистая модель данных, record убирает шум и делает намерение в коде очевидным.
AI-инструменты стали стандартной частью стека C#/.NET-разработчиков: ассистенты в IDE, генерация кода и тестов, помощь с документацией и ревью. По данным отраслевых отчётов, 80–90% инженеров уже используют AI-сервисы, а 97% компаний встроили AI в SDLC. Однако разрыв сегодня проходит не по доступу к моделям. У одних команд AI встроен в мультиагентные пайплайны, SDD подход и метрики качества. У других остаётся локальным помощником без влияния на метрики, скорость релизов и риск-профиль. Именно для интеграции AI в процессы команда Naition запускает 12‑недельную программу по AI-driven разработке. Программу собрали инженеры и техлиды из Яндекс Cloud, Сбера, Google и других продуктовых команд с опытом более 15 лет. Что внутри: • создание AI-окружения под ваш стек • контекст-инжиниринг - от создания и подключения кастомных MCP до RAG под вашу кодовую базу • решение продуктовых задач, spec-driven разработка и контроль качества с evals • разработка агентов, сабагентов с выходом в практики оркестрации • практики работы с легаси-проектами и крупными кодовыми базами при ограниченном бюджете Форматы участия: Командный - для C#/.NET-команд от 3–5 человек. Заходите в общий поток и получаете отдельный трек: разбор текущих процессов, выделение узких мест, план внедрения AI-практик и фиксация метрик до и после изменений. Индивидуальный формат - для архитекторов, техлидов, middle/senior‑разработчиков и руководителей, которые хотят собрать язык и паттерны AI‑ориентированной разработки, чтобы затем использовать их как основу изменений внутри своих команд. 95% участников прошлого потока дошли до конца. Первые изменения в процессах команды вносили уже после второго модуля. Новый поток AI‑DRIVEN стартует 21 июля. Если вы отвечаете за архитектуру и метрики разработки в C#/.NET‑проектах, вы можете оставить заявку на сайте и до 13 июля получить индивидуальные условия участия для команды или для себя как индивидуального участника. Подробности на сайте: naition.ai Реклама: ИП Крутов Дмитрий Валерьевич ИНН: 772973192199 Erid: 2VtzqxRqZmC
🚀 Claude Opus 5 за 24 часа собрал собственный космический симулятор Разработчик запустил модель на одну непрерывную сессию и получил The Long Silence - полноценную браузерную игру об исследовании космоса. По словам автора, весь код, визуал и звук создал Claude. Внутри: • процедурные звёзды, планеты, кольца и туманности • посадка на поверхность планет • перелёты между системами и fold drive • сканирование объектов, станции и заброшенные корабли • карта галактики, сюжет и архив найденных данных • динамическое масштабирование графики для стабильных 60 FPS Игра работает на WebGL2 без готовых ассетов: миры генерируются из seed, графика написана на GLSL, а музыка и звук создаются процедурно. Это ещё не Starfield по масштабу, но для проекта, собранного моделью за сутки, результат выглядит безумно. 🎮 Код и запуск: https://github.com/achimala/TheLongSilence 🌐 Запустить в браузере: https://longsilence.anshu.dev/
.NET 11 готовят под эпоху AI Microsoft собрала ключевые .NET-сессии с Build 2026. Фокус - AI, агенты и развитие C#. Что показали: - union types в C#; - улучшения runtime, SDK и производительности в .NET 11; - инструменты для добавления AI в C#-приложения; - agentic web в ASP.NET Core и Blazor; - локальные AI-модели и on-device inference через .NET MAUI; - новый dotnetup для установки и обновления SDK/runtime. Важно: .NET 11 пока находится в preview, а часть возможностей ещё развивается. https://devblogs.microsoft.com/dotnet/dotnet-at-microsoft-build-2026/
Что выведет код? using System; using System.Collections.Generic; using System.Linq; static IEnumerable<int> GetNumbers() { Console.Write("A"); yield return 1; Console.Write("B"); yield return 2; } var query = GetNumbers().Where(x => x > 0); Console.Write(query.First()); Console.Write(query.Count()); А — A12 Б — A1B2 В — A1AB2 Г — Error Правильный ответ: В — A1AB2. Почему: IEnumerable и LINQ выполняются лениво. First() запускает перебор один раз и доходит только до первого элемента: печатает A, потом 1. Count() запускает перебор заново: снова A, потом B, и в конце печатает 2.
Задача по .NET: зачем нужен IHttpClientFactory? Есть сервис, который ходит во внешний API: public sealed class PaymentApiClient { private readonly HttpClient _http; public PaymentApiClient(HttpClient http) { _http = http; } public async Task<string> GetStatusAsync(string id, CancellationToken ct) { using var response = await _http.GetAsync($"/payments/{id}", ct); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(ct); } } Регистрация: builder.Services.AddHttpClient<PaymentApiClient>(client => { client.BaseAddress = new Uri("https://payments.example.com"); client.Timeout = TimeSpan.FromSeconds(10); }) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5), MaxConnectionsPerServer = 50 }); Вопрос: почему это лучше, чем создавать new HttpClient() на каждый запрос? Потому что IHttpClientFactory переиспользует HttpMessageHandler и соединения под капотом. Это снижает риск socket exhaustion, даёт централизованную настройку таймаутов, ретраев, логирования и авторизации. А PooledConnectionLifetime нужен, чтобы соединения не жили вечно и приложение могло корректно подхватывать изменения DNS.
Каждый год в середине лета мы с друзьями собираемся на E-CODE. Ещё относительно молодая конфа Ozon Tech стала уже своего рода легендой. Не удивительно: хардовость официальной части здесь не уступает громкости афтерпати. На E-CODE 2026 нас по части бэкэнда ждут: • Runtime Async с его + и – • трассировка с eBPF • ускорение на SIMD • построение кастомной real-time системы видеоаналитики • сравнительный анализ алгоритмов сборки мусора Фишка этого года — брейнринг разработчиков и ИИ. Эксперты разберут сложные кейсы и сравнят свои подходы с тем, что предложит машина. Победителя выберет зал. Регистрируйтесь сейчас, чтобы услышать это всё вживую: https://ecode.ozon.tech/ 12 и 13 сентября, Москва. До встречи на E-CODE!
без подписи
Методы, их перегрузка и расширения. Бесплатный урок специализации «C#-разработчик» Методы — одна из базовых вещей в C#, без которой невозможно нормально писать, читать и поддерживать код. Но у начинающих разработчиков часто всё смешивается: где обычный метод, где перегрузка, как работает сигнатура, зачем нужны параметры по умолчанию и в каких случаях использовать params. На открытом уроке 2 июля в 20:00 разберём, что такое метод в C#, как писать собственные методы и как использовать перегрузку без хаоса в коде. Поговорим о сигнатуре метода, параметрах по умолчанию, ключевом слове params и методах-расширениях. На примерах покажем, как эти механики помогают делать код понятнее, гибче и удобнее для повторного использования. Урок не для тех, кто хочет просто «выучить синтаксис» без понимания, как методы влияют на структуру программы. 👉 Записаться: https://otus.pw/wkKs/?erid=2W5zFG7dRZ3 Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
🔥 EF Core migrations - это компилятор схемы базы, а не кнопка «создать таблицу» Опытный C#-разработчик не оставляет бизнес-инварианты только внутри SaveChangesAsync(). Проверку в приложении можно обойти прямым SQL, импортом данных, фоновым процессом или второй версией сервиса. Поэтому правила, нарушение которых делает данные некорректными, стоит закреплять в самой базе. public sealed class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.ToTable("Products", table => { table.HasCheckConstraint( "CK_Products_Price_Range", "[Price] > 0 AND [Price] <= 10000000"); }); builder.Property(x => x.Name) .HasMaxLength(200) .IsRequired(); builder.Property(x => x.NormalizedName) .HasMaxLength(200) .IsRequired(); builder.Property(x => x.Price) .HasPrecision(18, 2); builder.HasIndex(x => x.NormalizedName) .IsUnique() .HasDatabaseName("UX_Products_NormalizedName"); } } HasPrecision(18, 2) управляет физическим представлением decimal, но не проверяет допустимый диапазон цены. За это отвечает CHECK. Уникальный индекс решает другую важную проблему - гонку между запросами. if (!await db.Products.AnyAsync(x => x.Name == name)) { db.Products.Add(product); await db.SaveChangesAsync(); } Два параллельных запроса могут одновременно пройти AnyAsync() и попытаться создать одинаковые записи. Только уникальное ограничение базы гарантированно остановит второй запрос. Использовать для уникальности исходный Name тоже опасно. Результат будет зависеть от collation базы: Laptop, LAPTOP и Laptop могут считаться одинаковыми или разными. Поэтому часто сохраняют нормализованное значение: product.NormalizedName = product.Name .Trim() .ToUpperInvariant(); После изменения модели создаём миграцию: dotnet ef migrations add HardenProductSchema Но миграцию нельзя принимать вслепую. EF Core сравнивает текущую модель с ModelSnapshot и генерирует предполагаемый переход между двумя состояниями. Например, обычное переименование свойства может быть распознано как удаление старого столбца и создание нового: migrationBuilder.DropColumn( name: "Name", table: "Products"); migrationBuilder.AddColumn<string>( name: "DisplayName", table: "Products", nullable: false); В production это означает потерю данных. Правильную операцию нужно указать вручную: migrationBuilder.RenameColumn( name: "Name", table: "Products", newName: "DisplayName"); Для крупных таблиц полезен подход expand -> backfill -> contract. Сначала добавляется nullable-столбец: migrationBuilder.AddColumn<string>( name: "NormalizedName", table: "Products", type: "nvarchar(200)", nullable: true); Затем данные заполняются порциями отдельной задачей. После обновления всех строк новая версия приложения начинает записывать оба значения. И только в следующем релизе столбец переводится в NOT NULL, добавляется индекс и удаляется устаревшая логика. Так миграция не держит долгую блокировку и остаётся совместимой сразу с несколькими версиями приложения. В CI стоит проверять, не забыл ли разработчик создать миграцию: dotnet ef migrations has-pending-model-changes Для production лучше генерировать проверяемый SQL: dotnet ef migrations script \ --idempotent \ --output migrations.sql Либо собирать отдельный executable: dotnet ef migrations bundle \ --self-contained \ -r linux-x64 efbundle можно запускать в deployment job без исходников проекта и установленного EF CLI.
🚀 C# 15 меняет подход к `unsafe`: указатели больше не означают автоматически опасный код Раньше в C# сам факт использования указателя требовал: unsafe { int* ptr; } Теперь язык движется к более точной модели: опасным считается не наличие указателя, а операция, которая реально работает с unmanaged-памятью. В preview-версии C# 15 уже не требуют unsafe: ✅ объявление pointer-типа ✅ получение адреса через & ✅ fixed для закрепления памяти ✅ преобразование stackalloc в указатель ✅ sizeof для unmanaged-типов Пример: int number = 42; int* pointer = &number; int[] numbers = [10, 20, 30]; fixed (int* first = numbers) { // само закрепление памяти безопасно } Но вот это всё ещё требует unsafe: *pointer; // dereference pointer->Value; // доступ через указатель pointer[0]; // работа с элементами delegate*<> // вызов function pointer Главная идея: **создать инструмент можно безопасно. Опасно — когда начинаешь напрямую менять память.** Это первый этап большой переработки модели безопасности C#. Дальше ожидаются: - requires-unsafe для отдельных членов; - opt-in на уровне assembly; - новый safe keyword. C# постепенно делает unsafe-код более точечным: меньше блоков вокруг всего файла, больше внимания к реально опасным операциям. #csharp #dotnet #programming
⚡️ C#-приём: не привязывай метаданные к объектам через обычный `Dictionary` — иногда нужен `ConditionalWeakTable`. Допустим, библиотека кеширует данные о типах: private static readonly Dictionary<Type, Metadata> Cache = new(); В обычном приложении это может годами работать без проблем. Но если используются plugins, dynamic assemblies или unloadable AssemblyLoadContext, такой словарь способен удерживать Type, а вместе с ним — всю загруженную сборку. И получить memory leak. Для таких случаев в .NET есть малоизвестный: ConditionalWeakTable<TKey, TValue> Пример: private static readonly ConditionalWeakTable<Type, Metadata> Cache = new(); static Metadata GetMetadata(Type type) { return Cache.GetValue(type, BuildMetadata); } Ключ здесь хранится слабо. Если Type больше нигде не нужен, GC может его собрать, а связанная запись автоматически исчезнет из таблицы. Это особенно полезно для: * reflection-кешей; * proxy/framework-кода; * source/runtime metadata; * plugin-систем; * приложений с AssemblyLoadContext. 🔥 Если видишь: static Dictionary<Type, ...> или static ConcurrentDictionary<Type, ...> в коде, который работает с динамически загружаемыми сборками, стоит проверить: не удерживает ли этот кеш сборки в памяти навсегда? Иногда правильный ответ — ConditionalWeakTable. #CSharp #DotNet #Backend #Programming #CLR