Хватит дергать DateTime.UtcNow прямо в бизнес-логике.
Строчка выглядит безобидно, но именно из-за нее тесты потом превращаются в кошмар
Вот типичная ловушка:
* DateTime.UtcNow > user.SubscriptionEnd
Вроде ок, да?
Но как только пытаешься это протестировать, вылезает проблема:
- ты не контролируешь DateTime.UtcNow
- тесты становятся флаки или обрастают костылями
- бизнес-правила оказываются привязаны к инфраструктуре
Есть варианты лучше:
1. Передавать DateTime параметром. Самый простой путь, отлично заходит для маленьких методов.
2. Вынести время в интерфейс IClock. Масштабируется лучше и дружит с тестами.
Когда ты инвертируешь зависимость от времени, доменная логика становится:
✅ проще тестировать
✅ более детерминированной
✅ с нормальным разделением ответственности
В следующий раз, когда пишешь условие, завязанное на время, остановись и спроси:
“Мне нужно будет это тестировать?”
Если да, выноси часы наружу.
@KodBlog
Строчка выглядит безобидно, но именно из-за нее тесты потом превращаются в кошмар
Вот типичная ловушка:
* DateTime.UtcNow > user.SubscriptionEnd
Вроде ок, да?
Но как только пытаешься это протестировать, вылезает проблема:
- ты не контролируешь DateTime.UtcNow
- тесты становятся флаки или обрастают костылями
- бизнес-правила оказываются привязаны к инфраструктуре
Есть варианты лучше:
1. Передавать DateTime параметром. Самый простой путь, отлично заходит для маленьких методов.
2. Вынести время в интерфейс IClock. Масштабируется лучше и дружит с тестами.
Когда ты инвертируешь зависимость от времени, доменная логика становится:
В следующий раз, когда пишешь условие, завязанное на время, остановись и спроси:
“Мне нужно будет это тестировать?”
Если да, выноси часы наружу.
@KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🥴9🔥3❤1👍1
Не пиши в SQL = NULL или != NULL
Для NULL всегда используй IS NULL / IS NOT NULL
NULL это “нет значения / неизвестно”, и обычные операторы сравнения (=, !=, <, >) с ним не работают, потому что NULL не равен, не больше и не меньше вообще ничего.
= / != сравнивают значения. А NULL это не значение, а отсутствие значения. Поэтому IS / IS NOT проверяют именно наличие или отсутствие значения.
👉 @KodBlog
Для NULL всегда используй IS NULL / IS NOT NULL
NULL это “нет значения / неизвестно”, и обычные операторы сравнения (=, !=, <, >) с ним не работают, потому что NULL не равен, не больше и не меньше вообще ничего.
= / != сравнивают значения. А NULL это не значение, а отсутствие значения. Поэтому IS / IS NOT проверяют именно наличие или отсутствие значения.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23🔥3🥴3
Лучший конфиг для HttpClient это тот, который ты не копируешь в каждом сервисе.
В .NET ты настраиваешь все один раз через IHttpClientFactory (и Aspire, если используешь):
* service discovery для вызовов сервис-сервис
* дефолтную resiliency-настройку, чтобы кратковременные фейлы не ломали тебе жизнь
Дальше просто инжектишь клиент и пишешь бизнес-код.
- полный разбор
👉 @KodBlog
В .NET ты настраиваешь все один раз через IHttpClientFactory (и Aspire, если используешь):
* service discovery для вызовов сервис-сервис
* дефолтную resiliency-настройку, чтобы кратковременные фейлы не ломали тебе жизнь
Дальше просто инжектишь клиент и пишешь бизнес-код.
- полный разбор
Please open Telegram to view this post
VIEW IN TELEGRAM
👏6
25 Claude Code skills для .NET-разрабов
Покрывает штуки, которые реально важны и неочевидны (типа: как тестировать транзакционные письма в ASP.NET Core?) и вообще помогает улучшать вывод LLM (типы!).
github.com/Aaronontheweb/dotnet-skills?tab=readme-ov-file
Все это про то, чтобы Claude делал вещи нормально без того, чтобы ты на него орал.
👉 @KodBlog
Покрывает штуки, которые реально важны и неочевидны (типа: как тестировать транзакционные письма в ASP.NET Core?) и вообще помогает улучшать вывод LLM (типы!).
github.com/Aaronontheweb/dotnet-skills?tab=readme-ov-file
Все это про то, чтобы Claude делал вещи нормально без того, чтобы ты на него орал.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍2🥴2
БОЛЬШОЙ SQL грех
Никогда не пихай ORDER BY внутрь CTE (или подзапроса), если там же рядом нет LIMIT.
Если LIMIT забыть, база обычно будет сортировать всю таблицу вообще без смысла.
👉 @KodBlog
Никогда не пихай ORDER BY внутрь CTE (или подзапроса), если там же рядом нет LIMIT.
Если LIMIT забыть, база обычно будет сортировать всю таблицу вообще без смысла.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤3👏2👎1
Как пользоваться LINQ
Чтобы использовать LINQ, нужно подключить
➡️ Фильтрация через Where
Если использовать
Результат вывода ниже будет
➡️ Цикл через ForEach
Если использовать
Результат вывода ниже будет
➡️ Комбинации
LINQ при правильной комбинации может сильно сокращать код.
В примере ниже из
➡️ Пошаговая обработка через Select
В коде ниже каждый элемент массива строк по одному конвертируется в
(В других языках это что-то вроде
➡️ Any как OR-условие
В коде ниже проверяем, есть ли в списке имён хотя бы одно, которое начинается с
➡️ GetRange: взять диапазон элементов
В примере ниже при
➡️ Reverse: развернуть список
В коде ниже изначально список
➡️ Single: получить единственный элемент
В коде ниже пытаемся через
Для
➡️ Contains: проверить наличие элемента
В коде ниже проверяем, есть ли в списке
👉 @KodBlog
Чтобы использовать LINQ, нужно подключить
System.Linq через using.Если использовать
Where, можно коротко вытащить из массива/списка только элементы, которые подходят под условие.Результат вывода ниже будет
1.// список int
List<int> idList = new List<int>{1, 2, 3};
// фильтруем: берём только те, где 1
IEnumerable<int> selectList = idList.Where(id => id == 1);
Если использовать
ForEach, можно так же, как в обычном ForEach, сделать цикл короче, чем for.Результат вывода ниже будет
1,2,3.// список int
List<int> idList = new List<int>{1, 2, 3};
// проходимся по всему списку
idList.ForEach(id =>
{
print(id);
}
LINQ при правильной комбинации может сильно сокращать код.
В примере ниже из
attackList берутся элементы, где totalAttack != 0, затем они группируются по totalAttack, и группы сортируются по totalAttack.// группировка по totalAttack (одинаковый totalAttack, разные GridInfo)
IEnumerable<IGrouping<int, GridInfo>> total = attackList
.Where(info => info.totalAttack != 0) // фильтруем где есть totalAttack
.GroupBy(info => info.totalAttack) // группируем по totalAttack
.OrderBy(group => group.Key); // сортируем по totalAttack
Select позволяет легко обработать элементы массива по порядку.В коде ниже каждый элемент массива строк по одному конвертируется в
int, а затем весь результат превращается в List<int>.(В других языках это что-то вроде
Map: берём элементы массива и преобразуем)// массив String
string[] idString = {"1", "2", "3"};
// делаем int и превращаем в List
List<int> idList = idString.Select(id => int.Parse(id)).ToList();
Any возвращает true, если хотя бы один элемент удовлетворяет условию.В коде ниже проверяем, есть ли в списке имён хотя бы одно, которое начинается с
"A".// List строк
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
// Alice подходит, значит True
names.Any(name => name.StartsWith("A"));
GetRange позволяет взять из списка элементы по указанному диапазону.В примере ниже при
0〜5 можно получить "1, 2, 3, 4", но если указать диапазон за пределами размера списка, будет ошибка.// List int
List<int> idList = new List<int> { 1, 2, 3, 4, 5 };
// берём элементы 0〜4
idList.GetRange(0, 5);
// ошибка 1
idList.GetRange(0, 6);
// ошибка 2
idList.GetRange(1, 5);
Reverse разворачивает порядок элементов в списке.В коде ниже изначально список
"1, 2, 3, 4, 5", после Reverse станет "5, 4, 3, 2, 1".// List int
List<int> idList = new List<int> { 1, 2, 3, 4, 5 };
// выводим 1〜5
idList.ForEach(id =>
{
print(id);
});
// разворот
idList.Reverse();
// выводим 5〜1
idList.ForEach(id =>
{
print(id);
});
Single позволяет получить элемент, который встречается ровно один раз.В коде ниже пытаемся через
Where получить из списка "1, 1, 1, 1, 2" значения 2 и 1.Для
2 всё ок (он один), а для 1 будет ошибка, потому что 1 встречается несколько раз.// List int
List<int> idList = new List<int> { 1, 1, 1, 1, 2 };
// берём только 2
int select = idList.Where(id => 2).SingleOrDefault();
// ошибка
int select = idList.Where(id => 1).SingleOrDefault();
Contains позволяет проверить, есть ли элемент в списке.В коде ниже проверяем, есть ли в списке
"Alice, Bob, Charlie" значение "Alice".// List строк
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
// Alice есть в списке, значит True
names.Contains(name => name.StartsWith("Alice"));
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🥰3
Фанфакт: в C# можно руками создать окно через Win32 API, прям как в C++.
По ощущениям это реально то же самое, что и в C++: регаешь window class, делаешь
Что в итоге получаешь: очень лёгкую штуку без лишнего оверхеда. Если использовать
И да, туда же спокойно добавляются кнопки и прочие контролы через тот же Win32. Выглядит олдскульно, но зато железобетонно понимаешь, как GUI под капотом работает.
👉 @KodBlog
По ощущениям это реально то же самое, что и в C++: регаешь window class, делаешь
CreateWindowEx, заводишь WndProc, гоняешь GetMessage/DispatchMessage. Практической пользы почти ноль по сравнению с WinForms, но поиграться прикольно :DЧто в итоге получаешь: очень лёгкую штуку без лишнего оверхеда. Если использовать
LibraryImport + NativeAOT (это быстрее и приятнее, чем классический P/Invoke), можно собрать примерно 1MB exe без DLL, который моментально стартует и открывает окно. Делать так в проде обычно незачем, это скорее чисто эксперимент.И да, туда же спокойно добавляются кнопки и прочие контролы через тот же Win32. Выглядит олдскульно, но зато железобетонно понимаешь, как GUI под капотом работает.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔8👍4❤3👎1🤯1
Please open Telegram to view this post
VIEW IN TELEGRAM
😐32🥴5😁2🤯1🤨1
Два файла, которые я создаю еще до того, как напишу первую строку .NET-кода:
Directory.Packages.props и Directory.Build.props
Что это вообще такое?
Это файлы, которые ты добавляешь в solution, чтобы повысить сопровождаемость.
1/ Directory.Build.props
Этот файл централизует общие настройки проектов по всей solution.
✅ версия .NET
✅ nullable reference types
✅ предупреждения компилятора
✅ анализаторы
Вместо того чтобы копировать одну и ту же конфигурацию в каждый csproj, ты задаешь ее один раз.
Все проекты следуют одним правилам, поэтому со временем настройки не начинают “разъезжаться”.
2/ Directory.Packages.props
Этот файл управляет версиями NuGet-пакетов для всей solution.
Ты задаешь версии NuGet в одном месте, и все проекты используют именно их. В больших кодовых базах это убирает целый класс болезненных сюрпризов при сборках и деплоях.
Эти два файла особенно полезны при апгрейде версии .NET:
1. Меняешь в Directory.Build.props версию .NET.
2. Обновляешь версии пакетов в Directory.Packages.props.
3. Всё, готово.
Хочешь, чтобы .NET-решения нормально жили годами, начни с добавления этих двух простых файлов.
👉 @KodBlog
Directory.Packages.props и Directory.Build.props
Что это вообще такое?
Это файлы, которые ты добавляешь в solution, чтобы повысить сопровождаемость.
1/ Directory.Build.props
Этот файл централизует общие настройки проектов по всей solution.
Вместо того чтобы копировать одну и ту же конфигурацию в каждый csproj, ты задаешь ее один раз.
Все проекты следуют одним правилам, поэтому со временем настройки не начинают “разъезжаться”.
2/ Directory.Packages.props
Этот файл управляет версиями NuGet-пакетов для всей solution.
Ты задаешь версии NuGet в одном месте, и все проекты используют именно их. В больших кодовых базах это убирает целый класс болезненных сюрпризов при сборках и деплоях.
Эти два файла особенно полезны при апгрейде версии .NET:
1. Меняешь в Directory.Build.props версию .NET.
2. Обновляешь версии пакетов в Directory.Packages.props.
3. Всё, готово.
Хочешь, чтобы .NET-решения нормально жили годами, начни с добавления этих двух простых файлов.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤30👍5🍌2
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👌4
Все еще сидишь на .sln в 2026?
Давай покажу, с чем ты реально имеешь дело:
🤮 Везде GUID’ы, вечные merge-конфликты, читать невозможно.
А теперь то же самое в .slnx:
Вот и всё. Читаемо человеком, дружит с git, никаких GUID’ов.
Почему стоит мигрировать сейчас:
▪️ Поддерживается в Visual Studio 2022 (17.10+) и .NET 9+
▪️
▪️ Меньше merge-конфликтов, меньше боли в команде
▪️ В code review это наконец можно нормально читать
Как мигрировать:
Готово.
Старый формат делали под внутренности Visual Studio, не под людей. SLNX делают для разработчиков.
Все еще держишься за .sln? Почему?
👉 @KodBlog
Давай покажу, с чем ты реально имеешь дело:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp", "MyApp\MyApp.csproj", "{8A3B4E29-7F7D-4C51-9A18-5D8B9A3E1234}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
.... +200 more unreadable lines?А теперь то же самое в .slnx:
<Solution>
<Project Path="MyApp/MyApp.csproj" />
</Solution>
Вот и всё. Читаемо человеком, дружит с git, никаких GUID’ов.
Почему стоит мигрировать сейчас:
dotnet sln migrate делает это одной командойКак мигрировать:
dotnet sln migrate MyApp.sln
Готово.
.slnx создан.Старый формат делали под внутренности Visual Studio, не под людей. SLNX делают для разработчиков.
Все еще держишься за .sln? Почему?
Please open Telegram to view this post
VIEW IN TELEGRAM
👍28❤4🔥1
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13😐9👎1
Знаешь, что даже простая смена
В C# 11 поменялась генерация кода для конверсии method group так, чтобы в некоторых случаях избегать аллокаций.
Пример:
на net4.8 при LangVersion C# 10 и ниже вариант с method group может создавать делегат (то есть аллоцировать) и из-за этого быть заметно медленнее. А при LangVersion C# 11+ компилятор меняет генерацию кода так, чтобы делегат кэшировался, и аллокации пропадают.
В C# 11 поменяли codegen, чтобы кэшировать конверсию method group
C# 10 и ниже (делегат создается на месте):
C# 11 и выше (делегат кэшируется):
👉 @KodBlog
C# langversion может повлиять на производительность?В C# 11 поменялась генерация кода для конверсии method group так, чтобы в некоторых случаях избегать аллокаций.
Пример:
[Benchmark]
public int Lambda()
{
Func<int> func = () => Method();
return func();
}
[Benchmark]
public int MethodGroup()
{
Func<int> func = Method;
return func();
}
static int Method() => 42;
на net4.8 при LangVersion C# 10 и ниже вариант с method group может создавать делегат (то есть аллоцировать) и из-за этого быть заметно медленнее. А при LangVersion C# 11+ компилятор меняет генерацию кода так, чтобы делегат кэшировался, и аллокации пропадают.
В C# 11 поменяли codegen, чтобы кэшировать конверсию method group
C# 10 и ниже (делегат создается на месте):
return new Func<int>((object)null, __methodptr(Method))();
C# 11 и выше (делегат кэшируется):
return (MethodGroupBenchmarks.<>O.<0>__Method ??
(MethodGroupBenchmarks.<>O.<0>__Method = new Func<int>((object)null, __methodptr(Method))))();
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔8
В .NET 10 больше не нужно писать Dockerfile.
Кто уже пользуется новой фичей? Как ощущения в проде?
Типичный многостадийный Dockerfile для .NET-приложения выглядит примерно так:
Это работает, но есть кривая обучения и накладные расходы на поддержку:
Поддержка: нужно вручную обновлять теги базовых образов
Кэширование слоёв: неправильный порядок COPY убивает кэш сборки
Дублирование: почти каждому проекту нужен похожий Dockerfile
Переключение контекста: ты пишешь Docker DSL, а не .NET-код
Подход с .NET SDK устраняет все эти проблемы.
Если ты работаешь на .NET 10, ничего дополнительно включать не нужно. Публикация в контейнеры работает из коробки для ASP.NET Core-приложений, worker-сервисов и консольных приложений.
Можно сразу публиковать приложение в контейнерный образ:
И всё. .NET SDK сам:
соберёт приложение
подберёт подходящий базовый образ
создаст контейнерный образ с результатом публикации
загрузит его в локальный OCI-совместимый демон
Чаще всего это Docker, но Podman тоже поддерживается.
(На скрине показан вывод команды dotnet publish, создающей контейнерный образ.)
👉 @KodBlog
Кто уже пользуется новой фичей? Как ощущения в проде?
Типичный многостадийный Dockerfile для .NET-приложения выглядит примерно так:
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"]
RUN dotnet restore "src/MyApi/MyApi.csproj"
COPY . .
WORKDIR "/src/src/MyApi"
RUN dotnet build "MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
Это работает, но есть кривая обучения и накладные расходы на поддержку:
Поддержка: нужно вручную обновлять теги базовых образов
Кэширование слоёв: неправильный порядок COPY убивает кэш сборки
Дублирование: почти каждому проекту нужен похожий Dockerfile
Переключение контекста: ты пишешь Docker DSL, а не .NET-код
Подход с .NET SDK устраняет все эти проблемы.
Если ты работаешь на .NET 10, ничего дополнительно включать не нужно. Публикация в контейнеры работает из коробки для ASP.NET Core-приложений, worker-сервисов и консольных приложений.
Можно сразу публиковать приложение в контейнерный образ:
dotnet publish --os linux --arch x64 /t:PublishContainer
И всё. .NET SDK сам:
соберёт приложение
подберёт подходящий базовый образ
создаст контейнерный образ с результатом публикации
загрузит его в локальный OCI-совместимый демон
Чаще всего это Docker, но Podman тоже поддерживается.
(На скрине показан вывод команды dotnet publish, создающей контейнерный образ.)
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14❤6
This media is not supported in your browser
VIEW IN TELEGRAM
AviyalWM: портативный и лёгкий оконный менеджер, написанный на C#
Рад сообщить о релизе AviyalWM — простого, лёгкого и портативного динамического тайлингового оконного менеджера для Windows. Краткий список возможностей:
▪️ Рабочие пространства
▪️ Анимации рабочих пространств (горизонтальные и вертикальные)
▪️ Макеты: Dwindle, Stack, Master
▪️ Переключение в плавающий режим
▪️ Закрытие окна в фокусе
▪️ Переключение фокуса
▪️ Конфигурация через JSON
▪️ Горячая перезагрузка
▪️ Запрос состояния через WebSocket и выполнение команд
▪️ Запуск приложений с помощью горячих клавиш
Репозиторий: https://github.com/TheAjaykrishnanR/aviyal
👉 @KodBlog
Рад сообщить о релизе AviyalWM — простого, лёгкого и портативного динамического тайлингового оконного менеджера для Windows. Краткий список возможностей:
Репозиторий: https://github.com/TheAjaykrishnanR/aviyal
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥2
В .NET теперь добавили
Это изменение вчера влито в dotnet main и, судя по всему, будет доступно в .NET 11.
На втором фото есть таблица от Gemini с сравнением Gzip, Brotli и Zstandard по скорости и сжатию.
👉 @KodBlog
DecompressionMethods.Zstandard для автоматической распаковки HTTP-ответов Это изменение вчера влито в dotnet main и, судя по всему, будет доступно в .NET 11.
На втором фото есть таблица от Gemini с сравнением Gzip, Brotli и Zstandard по скорости и сжатию.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👏13🔥2❤1
Когда появился Docker, это была настоящая революция.
Теперь .NET Aspire меняет способ доставки ПО навсегда.
И многие разработчики до сих пор не осознают масштаб этого сдвига.
Docker решил хаос с окружениями
До Docker проблема «у меня работает» была ежедневной:
• Разные версии ОС
• Отсутствующие зависимости
• Сломанные окружения повсюду
Docker решил это, стандартизировав окружения через контейнеры.
Одна команда → одно и то же приложение, одинаковое поведение, везде.
Это действительно было революцией.
Но Docker не решил всё
Он пакует приложения, но связи между сервисами всё ещё настраиваются вручную:
• Всё ещё приходится управлять строками подключения
• Всё ещё самому настраивать наблюдаемость
И распределённые системы остаются сложными.
Здесь .NET Aspire меняет правила игры
Aspire — это слой оркестрации для современных приложений.
Что Aspire делает из коробки:
• Service discovery → никаких хардкодных URL или портов
• Injection конфигураций → базы данных и кеши подключаются автоматически
• Встроенная наблюдаемость → логи, метрики, трассировки, готово с первого дня
• Локальная оркестрация → запускаем всю систему одной командой
• Production-ready деплой → через Docker Compose
• Cloud-ready деплой → Azure или AWS
Вот сам гайд
👉 @KodBlog
Теперь .NET Aspire меняет способ доставки ПО навсегда.
И многие разработчики до сих пор не осознают масштаб этого сдвига.
Docker решил хаос с окружениями
До Docker проблема «у меня работает» была ежедневной:
• Разные версии ОС
• Отсутствующие зависимости
• Сломанные окружения повсюду
Docker решил это, стандартизировав окружения через контейнеры.
Одна команда → одно и то же приложение, одинаковое поведение, везде.
Это действительно было революцией.
Но Docker не решил всё
Он пакует приложения, но связи между сервисами всё ещё настраиваются вручную:
• Всё ещё приходится управлять строками подключения
• Всё ещё самому настраивать наблюдаемость
И распределённые системы остаются сложными.
Здесь .NET Aspire меняет правила игры
Aspire — это слой оркестрации для современных приложений.
Что Aspire делает из коробки:
• Service discovery → никаких хардкодных URL или портов
• Injection конфигураций → базы данных и кеши подключаются автоматически
• Встроенная наблюдаемость → логи, метрики, трассировки, готово с первого дня
• Локальная оркестрация → запускаем всю систему одной командой
• Production-ready деплой → через Docker Compose
• Cloud-ready деплой → Azure или AWS
Вот сам гайд
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍6