C# 1001 notes
6.61K subscribers
412 photos
14 videos
2 files
357 links
Регулярные короткие заметки по C# и .NET.

Просто о сложном для каждого.

admin - @haarrp
Download Telegram
Форма логина и JWT-токен — ещё не безопасность приложения. На практике ошибки в аутентификации и авторизации становятся причиной утечек данных, проблем с доступом и уязвимостей, которые сложно обнаружить до выхода системы в production.

26 мая в 20:00 МСК приглашаем вас на открытый урок курса «C# ASP.NET Core-разработчик». На занятии разберём, как в ASP.NET Core устроены pipeline, middleware и схемы аутентификации. Покажем, как правильно использовать JWT, cookies, claims, роли и policy-based авторизацию для гибкого и безопасного контроля доступа.

Отдельно обсудим типичные ошибки, которые встречаются в production: небезопасное хранение токенов, ошибки настройки схем и проблемы в логике авторизации. Урок будет полезен .NET-разработчикам, которые хотят систематизировать знания по безопасности веб-приложений и увереннее работать с ASP.NET Core в реальных проектах.

Регистрация уже открыта:
https://otus.pw/tDF66/?erid=2W5zFGbdS73


Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
⚡️ Machine Learning Roadmap 2025: большая карта входа в ML без сказок про “нейросети за месяц

Большой русскоязычный roadmap по машинному обучению: от первого import numpy до LLM, RAG, fine-tuning, AI-агентов и MLOps и даже вабкодинга.

Внутри нормальная структура: что учить, в каком порядке, зачем это нужно и что должно получиться на практике после каждого этапа.

Roadmap разбит на 7 треков:

1. Фундамент: Python, математика, статистика, инструменты
2. Классический ML: scikit-learn, табличные данные, метрики, валидация
3. Deep Learning: PyTorch, CNN, RNN, training loop
4. LLM и трансформеры: attention, KV-cache, RAG, LoRA, агенты
5. Generative AI: изображения, видео, аудио, мультимодальность
6. MLOps и прод: Docker, Kubernetes, CI/CD, monitoring, serving
7. Специализация: CV, NLP, RecSys, RL, Safety

Roadmap не продаёт иллюзию “обучил модель - стал ML-инженером”.

В реальной работе много времени уходит на данные, метрики, деплой, мониторинг, воспроизводимость и разбор ошибок. Модель - только часть системы.

Хорошая мысль из roadmap: LLM не делает джуна сеньором. Она ускоряет того, кто уже понимает базу. Без базы человек просто становится оператором Copilot, который не может объяснить, почему всё сломалось.

По времени тоже без сказок:

1. 0-3 месяца: математика, классический ML
2. 3-6 месяцев: Deep Learning и PyTorch
3. 6-12 месяцев: LLM, RAG, fine-tuning, AI-агенты
4. 12+ месяцев: MLOps, прод, масштабирование, специализация

Тут же собрано 7 болших бесплатных курсов по машинному обучению, математике и вайбкодингу!

Если давно хотели зайти в ML системно, а не прыгать между роликами про ChatGPT, Stable Diffusion и “топ-10 библиотек”, это хороший ориентир.

https://github.com/justxor/MachineLearningRoadmap
Please open Telegram to view this post
VIEW IN TELEGRAM
✔️ Одна строчка .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/
Please open Telegram to view this post
VIEW IN TELEGRAM
🐳 «Используй 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

Если в приложении нужно обрабатывать поток данных от продюсеров к потребителям, первым делом вспоминают про очереди или 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
Please open Telegram to view this post
VIEW IN TELEGRAM
Задача по .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.
Аллокации, которых нет в коде: охота на скрытый боксинг в .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/
Что выведет код?


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.
Продвинутый 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’и.
Методы, их перегрузка и расширения. Бесплатный урок специализации «C#-разработчик»

Методы — одна из базовых вещей в C#, без которой невозможно нормально писать, читать и поддерживать код. Но у начинающих разработчиков часто всё смешивается: где обычный метод, где перегрузка, как работает сигнатура, зачем нужны параметры по умолчанию и в каких случаях использовать params.

На открытом уроке 2 июля в 20:00 разберём, что такое метод в C#, как писать собственные методы и как использовать перегрузку без хаоса в коде. Поговорим о сигнатуре метода, параметрах по умолчанию, ключевом слове params и методах-расширениях. На примерах покажем, как эти механики помогают делать код понятнее, гибче и удобнее для повторного использования.

Урок не для тех, кто хочет просто «выучить синтаксис» без понимания, как методы влияют на структуру программы.

👉 Записаться: https://otus.pw/wkKs/?erid=2W5zFG7dRZ3

Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
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 убирает шум и делает намерение в коде очевидным.
⚡️ 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, где зависимости обычно весят больше, чем сам код.
🔥 Хочешь быстрее расти в 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://xn--r1a.website/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://xn--r1a.website/addlist/2Ls-snqEeytkMDgy
Папка Frontend: https://xn--r1a.website/addlist/mzMMG3RPZhY2M2Iy

Полезное сверху:

ИТ-мемы: t.me/memes_prog
Английский для программистов: t.me/english_forprogrammers
ИИ и технологии: t.me/vistehno
954 ГБ open-source курсов: @courses
ИТ-книги бесплатно: https://xn--r1a.website/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

Подписывайся на нужные направления и собирай себе ленту, которая реально двигает вперёд.

Пока кто-то листает шум, ты будешь видеть инструменты, задачи и идеи, которые помогают расти в профессии.
Каждый год в середине лета мы с друзьями собираемся на E-CODE. Ещё относительно молодая конфа Ozon Tech стала уже своего рода легендой. Не удивительно: хардовость официальной части здесь не уступает громкости афтерпати.

На E-CODE 2026 нас по части бэкэнда ждут:

• Runtime Async с его + и –
• трассировка с eBPF
• ускорение на SIMD
• построение кастомной real-time системы видеоаналитики
• сравнительный анализ алгоритмов сборки мусора

Фишка этого года — брейнринг разработчиков и ИИ. Эксперты разберут сложные кейсы и сравнят свои подходы с тем, что предложит машина. Победителя выберет зал.

Регистрируйтесь сейчас, чтобы услышать это всё вживую: https://ecode.ozon.tech/

12 и 13 сентября, Москва. До встречи на E-CODE!
.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/
This media is not supported in your browser
VIEW IN TELEGRAM
🚀 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/
🔥 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