День 2280. #TipsAndTricks
Удаляем Пустые Папки в PowerShell
Вот простой скрипт в PowerShell, который рекурсивно удаляет пустые папки:
Логика следующая:
1. Получить все каталоги рекурсивно, использовать -Force для получения скрытых папок;
2. Сортировать их в порядке убывания, так как мы хотим сначала удалить самые глубокие папки;
3. Проверить, пуста ли папка;
4. Удалить папку.
Источник: https://www.meziantou.net/remove-empty-folders-using-powershell.htm
Удаляем Пустые Папки в PowerShell
Вот простой скрипт в PowerShell, который рекурсивно удаляет пустые папки:
$rootFolder = 'C:\Temp'
Get-ChildItem $rootFolder -Recurse -Directory -Force |
Sort-Object -Property FullName -Descending |
Where-Object { $($_ | Get-ChildItem -Force | Select-Object -First 1).Count -eq 0 } |
Remove-Item
Логика следующая:
1. Получить все каталоги рекурсивно, использовать -Force для получения скрытых папок;
2. Сортировать их в порядке убывания, так как мы хотим сначала удалить самые глубокие папки;
3. Проверить, пуста ли папка;
4. Удалить папку.
Источник: https://www.meziantou.net/remove-empty-folders-using-powershell.htm
👍14👎1
День 2291. #TipsAndTricks
Скрипт PowerShell для Переименования Проектов .NET
Переименовать проект .NET — утомительное занятие. Вам придётся переименовать файлы и папки, а также заменить содержимое в файлах, например пространство имён или путь в файлах .sln.
Следующий скрипт PowerShell, переименует файлы и папки и заменит содержимое в файлах:
Источник: https://www.meziantou.net/powershell-script-to-rename-dotnet-projects.htm
Скрипт PowerShell для Переименования Проектов .NET
Переименовать проект .NET — утомительное занятие. Вам придётся переименовать файлы и папки, а также заменить содержимое в файлах, например пространство имён или путь в файлах .sln.
Следующий скрипт PowerShell, переименует файлы и папки и заменит содержимое в файлах:
$ErrorActionPreference = "Stop"
$rootFolder = Resolve-Path -Path "."
$oldName = "SampleRazorPages"
$newName = "SampleWebApp"
# Переименовываем файлы и папки
foreach ($item in Get-ChildItem -LiteralPath $rootFolder -Recurse | Sort-Object -Property FullName -Descending) {
$itemNewName = $item.Name.Replace($oldName, $newName)
if ($item.Name -ne $itemNewName) {
Rename-Item -LiteralPath $item.FullName -NewName $itemNewName
}
}
# Заменяем содержимое в файлах
foreach ($item in Get-ChildItem $rootFolder -Recurse -Include "*.cmd", "*.cs", "*.csproj", "*.json", "*.md", "*.proj", "*.props", "*.ps1", "*.sln", "*.slnx", "*.targets", "*.txt", "*.vb", "*.vbproj", "*.xaml", "*.xml", "*.xproj", "*.yml", "*.yaml") {
$content = Get-Content -LiteralPath $item.FullName
if ($content) {
$newContent = $content.Replace($oldName, $newName)
Set-Content -LiteralPath $item.FullName -Value $newContent
}
}
Источник: https://www.meziantou.net/powershell-script-to-rename-dotnet-projects.htm
👍27
День 2298. #TipsAndTricks
Очистка Кэшей NuGet
NuGet может кэшировать множество пакетов и других вещей с течением времени. Сегодня рассмотрим, как очистить большинство из этого.
Вы можете посмотреть используемые кэши и их местонахождения, выполнив следующую команду:
Вывод будет примерно таким:
Со временем там накапливается гигантский объём данных. У меня http-cache больше 2ГБ, а global-packages больше 20ГБ. Если у вас достаточно места, можете оставить всё как есть. Это просто кэшированные данные, которые на самом деле безвредны (кроме занятого места).
Кэш NuGet-пакетов
Папка global-packages — это то место, куда dotnet restore помещает все пакеты пользователя. Поэтому неважно, в каком репозитории вы находитесь, он всегда загружает каждый пакет (конечно, включая все зависимости, которые требуются пакету) в эту папку. Преимущество в том, что, если только это не nodejs и npm, у вас будет супербыстрое восстановление для пакетов, которые уже загружены и не являются локальными для вашего репозитория.
Чтобы удалить кэш (что приведёт к повторной загрузке требуемых пакетов), вы можете либо очистить содержимое папки, либо просто вызвать:
Кэш Http
То же самое относится к http-cache. По сути, он хранит метаданные в пакетов (например, в каких версиях они существуют в NuGet), но также, похоже, содержит некоторые бинарные файлы. В любом случае, если вы хотите удалить это:
Временные данные
Папка temp хранит временные файлы. Очистить её можно так:
Удалить всё
Следующая команда удалит все кэшированные данные NuGet:
После этого, при создании нового приложения, все данные NuGet будут скачаны заново из интернета. Но это также уберёт всё ненужное!
Источник: https://steven-giesel.com/blogPost/ef7e9271-3b8d-4658-988f-b48bbd11e320/clearing-nuget-caches
Очистка Кэшей NuGet
NuGet может кэшировать множество пакетов и других вещей с течением времени. Сегодня рассмотрим, как очистить большинство из этого.
Вы можете посмотреть используемые кэши и их местонахождения, выполнив следующую команду:
dotnet nuget locals all --list
Вывод будет примерно таким:
http-cache: C:\Users\sbenz\AppData\Local\NuGet\v3-cache
global-packages: C:\Users\sbenz\.nuget\packages\
temp: C:\Users\sbenz\AppData\Local\Temp\NuGetScratch
plugins-cache: C:\Users\sbenz\AppData\Local\NuGet\plugins-cache
Со временем там накапливается гигантский объём данных. У меня http-cache больше 2ГБ, а global-packages больше 20ГБ. Если у вас достаточно места, можете оставить всё как есть. Это просто кэшированные данные, которые на самом деле безвредны (кроме занятого места).
Кэш NuGet-пакетов
Папка global-packages — это то место, куда dotnet restore помещает все пакеты пользователя. Поэтому неважно, в каком репозитории вы находитесь, он всегда загружает каждый пакет (конечно, включая все зависимости, которые требуются пакету) в эту папку. Преимущество в том, что, если только это не nodejs и npm, у вас будет супербыстрое восстановление для пакетов, которые уже загружены и не являются локальными для вашего репозитория.
Чтобы удалить кэш (что приведёт к повторной загрузке требуемых пакетов), вы можете либо очистить содержимое папки, либо просто вызвать:
dotnet nuget locals global-packages --clear
Кэш Http
То же самое относится к http-cache. По сути, он хранит метаданные в пакетов (например, в каких версиях они существуют в NuGet), но также, похоже, содержит некоторые бинарные файлы. В любом случае, если вы хотите удалить это:
dotnet nuget locals http-cache --clear
Временные данные
Папка temp хранит временные файлы. Очистить её можно так:
dotnet nuget locals temp --clear
Удалить всё
Следующая команда удалит все кэшированные данные NuGet:
dotnet nuget locals all --clear
После этого, при создании нового приложения, все данные NuGet будут скачаны заново из интернета. Но это также уберёт всё ненужное!
Источник: https://steven-giesel.com/blogPost/ef7e9271-3b8d-4658-988f-b48bbd11e320/clearing-nuget-caches
👍26
День 2302. #ЧтоНовенького #TipsAndTricks
Используем Расширения C# 14 для Парсинга Enum
Расширения ещё только в планах для C# 14, а умельцы уже предлагают интересные варианты их использования.
В .NET многие типы предоставляют статический метод Parse для преобразования строк в соответствующие им типы. Например:
В перечислениях используется обобщённый метод Enum.Parse:
А вот это не сработает:
Было бы более интуитивно понятно, если бы перечисления поддерживали метод Parse напрямую. С помощью C# 14 и его новой функции членов-расширений мы можем этого добиться.
Следующий код демонстрирует, как добавить методы Parse и TryParse к перечислениям с использованием расширений C# 14:
Теперь мы можем использовать методы Parse/TryParse для самого типа enum, так же как мы это делаем для других типов:
Источник: https://www.meziantou.net/use-csharp-14-extensions-to-simplify-enum-parsing.htm
Используем Расширения C# 14 для Парсинга Enum
Расширения ещё только в планах для C# 14, а умельцы уже предлагают интересные варианты их использования.
В .NET многие типы предоставляют статический метод Parse для преобразования строк в соответствующие им типы. Например:
int.Parse("123");
double.Parse("123.45");
DateTime.Parse("2023-01-01");
IPAddress.Parse("192.168.0.1");В перечислениях используется обобщённый метод Enum.Parse:
Enum.Parse<MyEnum>("Value1");А вот это не сработает:
MyEnum.Parse("Value1");Было бы более интуитивно понятно, если бы перечисления поддерживали метод Parse напрямую. С помощью C# 14 и его новой функции членов-расширений мы можем этого добиться.
Следующий код демонстрирует, как добавить методы Parse и TryParse к перечислениям с использованием расширений C# 14:
static class EnumExtensions
{
extension<T>(T _) where T : struct, Enum
{
public static T Parse(string value)
=> Enum.Parse<T>(value);
public static T Parse(string value, bool ignoreCase)
=> Enum.Parse<T>(value, ignoreCase);
public static T Parse(ReadOnlySpan<char> value)
=> Enum.Parse<T>(value);
public static T Parse(
ReadOnlySpan<char> value,
bool ignoreCase)
=> Enum.Parse<T>(value, ignoreCase);
public static bool TryParse(
[NotNullWhen(true)] string? value,
out T result)
=> Enum.TryParse(value, out result);
public static bool TryParse(
[NotNullWhen(true)] string? value,
bool ignoreCase,
out T result)
=> Enum.TryParse(value, ignoreCase, out result);
public static bool TryParse(
ReadOnlySpan<char> value,
out T result)
=> Enum.TryParse(value, out result);
public static bool TryParse(
ReadOnlySpan<char> value,
bool ignoreCase,
out T result)
=> Enum.TryParse(value, ignoreCase, out result);
}
}
Теперь мы можем использовать методы Parse/TryParse для самого типа enum, так же как мы это делаем для других типов:
MyEnum.Parse("Value1");
if (MyEnum.TryParse("Value1", out var result))
{
//…
}Источник: https://www.meziantou.net/use-csharp-14-extensions-to-simplify-enum-parsing.htm
👍35
День 2305. #ЧтоНовенького #TipsAndTricks
Используем Расширения C# 14 для Написания Защитных Конструкций
Продолжаем рассматривать примеры применения ещё не вышедших расширений в C# 14 (первая часть тут).
В C# есть много хороших защитных конструкций, расположенных поверх статических классов исключений, таких как ArgumentNullException, ArgumentOutOfRangeException и т.д. Например, ArgumentException.ThrowIfNullOrEmpty, ArgumentException.ThrowIfNullOrWhiteSpace. Теперь мы можем легко их расширить!
Расширения в C#14 позволяют добавлять новые защитные конструкции к существующим классам. Например, если мы хотим иметь такую «жутко полезную» семантику, как: «Выбрасывать исключение, если строка содержит ровно один символ», мы можем сделать что-то вроде этого:
Теперь мы можем использовать этот метод-расширение так:
Он прекрасно вливается в семейство существующих защитных конструкций:
Конечно, это слишком упрощённый пример. Но вы поняли идею. Мы получаем что-то похожее на существующие защитные конструкции.
Заметьте, что до C#14 этого сделать нельзя, т.к. здесь мы использовали статический метод-расширение, который можно вызвать так:
Существующие на данный момент методы-расширения позволяют делать только экземплярные методы, которые пришлось бы вызывать так:
Источник: https://steven-giesel.com/blogPost/e2552b7a-293a-4f46-892f-95a0cd677e4d/writing-new-guards-with-extensions-in-c-14
Используем Расширения C# 14 для Написания Защитных Конструкций
Продолжаем рассматривать примеры применения ещё не вышедших расширений в C# 14 (первая часть тут).
В C# есть много хороших защитных конструкций, расположенных поверх статических классов исключений, таких как ArgumentNullException, ArgumentOutOfRangeException и т.д. Например, ArgumentException.ThrowIfNullOrEmpty, ArgumentException.ThrowIfNullOrWhiteSpace. Теперь мы можем легко их расширить!
Расширения в C#14 позволяют добавлять новые защитные конструкции к существующим классам. Например, если мы хотим иметь такую «жутко полезную» семантику, как: «Выбрасывать исключение, если строка содержит ровно один символ», мы можем сделать что-то вроде этого:
static class EnumExtensions
{
extension(ArgumentException)
{
public static void
ThrowIfHasOneCharacter(
string arg,
[CallerArgumentExpression("arg")]
string? paramName = null)
{
if (arg.Length == 1)
throw new ArgumentException($"Аргумент '{paramName}' не может иметь только один символ.", paramName);
}
}
}
Теперь мы можем использовать этот метод-расширение так:
public void MyMethod(string arg)
{
ArgumentException.ThrowIfHasOneCharacter(arg);
…
}
Он прекрасно вливается в семейство существующих защитных конструкций:
public void MyMethod(string arg)
{
ArgumentException.ThrowIfNullOrEmpty(arg);
ArgumentException.ThrowIfHasOneCharacter(arg);
…
}
Конечно, это слишком упрощённый пример. Но вы поняли идею. Мы получаем что-то похожее на существующие защитные конструкции.
Заметьте, что до C#14 этого сделать нельзя, т.к. здесь мы использовали статический метод-расширение, который можно вызвать так:
ArgumentException.ThrowIfHasOneCharacter(…);
Существующие на данный момент методы-расширения позволяют делать только экземплярные методы, которые пришлось бы вызывать так:
var ex = new ArgumentException();
ex.ThrowIfHasOneCharacter(…);
Источник: https://steven-giesel.com/blogPost/e2552b7a-293a-4f46-892f-95a0cd677e4d/writing-new-guards-with-extensions-in-c-14
👍17
День 2324. #TipsAndTricks
Развенчиваем Миф Производительности SQL "Сначала Фильтр Потом JOIN"
В интернете часто можно встретить описание «трюка, повышающего производительность запросов в SQL», который звучит "Сначала Фильтр Потом JOIN". В нём утверждается, что вместо того, чтобы сначала объединять таблицы, а затем применять фильтр к результатам, нужно делать наоборот.
Например, вместо:
использовать:
Смысл в том, что БД сначала уберёт ненужные данные из одной таблицы, а потом выполнит соединение меньшего объёма, экономя время и память. Звучит логично. Но дело в том, что для современных БД этот совет не имеет смысла.
Вот пример плана выполнения (EXPLAIN ANALYZE) обоих запросов в PostgreSQL над таблицами с 10000 записями в users и 5000000 в orders.
«Неоптимальный» план запроса:
«Оптимальный» план запроса:
Как видите, планы запросов идентичны, и «оптимизация» ничего не добилась.
Основные операции:
- Последовательное сканирование (Seq Scan) таблицы orders с применением фильтра;
- Последовательное сканирование таблицы users;
- Операция хеширования (Hash) меньшей таблицы (users);
- Хеш-соединение по user_id.
Оптимизаторы запросов умнее вас
Современные БД используют стоимостную оптимизацию. Оптимизатор имеет статистику о таблицах: количество строк, распределение данных, наличие индекса, селективность столбцов и т.п. – и использует её для оценки стоимости различных стратегий выполнения. Современные БД, такие как PostgreSQL, MySQL и SQL Server, уже автоматически выполняют «выталкивание предикатов» и переупорядочивание соединений. Т.е. оба запроса переписываются по одному и тому же оптимальному плану. Поэтому ручная оптимизация в подзапрос не ускоряет работу, а просто затрудняет чтение SQL-кода.
Итого
Пишите понятный, читаемый SQL. Позвольте оптимизатору делать свою работу. В непонятных ситуациях используйте EXPLAIN ANALYZE, чтобы понять, что на самом деле делает БД и действительно ли один запрос быстрее другого.
Источник: https://www.milanjovanovic.tech/blog/debunking-the-filter-early-join-later-sql-performance-myth
Развенчиваем Миф Производительности SQL "Сначала Фильтр Потом JOIN"
В интернете часто можно встретить описание «трюка, повышающего производительность запросов в SQL», который звучит "Сначала Фильтр Потом JOIN". В нём утверждается, что вместо того, чтобы сначала объединять таблицы, а затем применять фильтр к результатам, нужно делать наоборот.
Например, вместо:
SELECT *
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.total > 500;
использовать:
SELECT *
FROM (
SELECT * FROM orders WHERE total > 500
) o
JOIN users u ON u.id = o.user_id;
Смысл в том, что БД сначала уберёт ненужные данные из одной таблицы, а потом выполнит соединение меньшего объёма, экономя время и память. Звучит логично. Но дело в том, что для современных БД этот совет не имеет смысла.
Вот пример плана выполнения (EXPLAIN ANALYZE) обоих запросов в PostgreSQL над таблицами с 10000 записями в users и 5000000 в orders.
«Неоптимальный» план запроса:
Hash Join (cost=280.00..96321.92 rows=2480444 width=27) (actual time=1.014..641.202 rows=2499245 loops=1)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..89528.00 rows=2480444 width=14) (actual time=0.006..368.857 rows=2499245 loops=1)
Filter: (total > '500'::numeric)
Rows Removed by Filter: 2500755
-> Hash (cost=155.00..155.00 rows=10000 width=13) (actual time=0.998..0.999 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 577kB
-> Seq Scan on users u (cost=0.00..155.00 rows=10000 width=13) (actual time=0.002..0.341 rows=10000 loops=1)
Planning Time: 0.121 ms
Execution Time: 685.818 ms
«Оптимальный» план запроса:
Hash Join (cost=280.00..96321.92 rows=2480444 width=27) (actual time=1.019..640.613 rows=2499245 loops=1)
Hash Cond: (orders.user_id = u.id)
-> Seq Scan on orders (cost=0.00..89528.00 rows=2480444 width=14) (actual time=0.005..368.260 rows=2499245 loops=1)
Filter: (total > '500'::numeric)
Rows Removed by Filter: 2500755
-> Hash (cost=155.00..155.00 rows=10000 width=13) (actual time=1.004..1.005 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 577kB
-> Seq Scan on users u (cost=0.00..155.00 rows=10000 width=13) (actual time=0.003..0.348 rows=10000 loops=1)
Planning Time: 0.118 ms
Execution Time: 685.275 ms
Как видите, планы запросов идентичны, и «оптимизация» ничего не добилась.
Основные операции:
- Последовательное сканирование (Seq Scan) таблицы orders с применением фильтра;
- Последовательное сканирование таблицы users;
- Операция хеширования (Hash) меньшей таблицы (users);
- Хеш-соединение по user_id.
Оптимизаторы запросов умнее вас
Современные БД используют стоимостную оптимизацию. Оптимизатор имеет статистику о таблицах: количество строк, распределение данных, наличие индекса, селективность столбцов и т.п. – и использует её для оценки стоимости различных стратегий выполнения. Современные БД, такие как PostgreSQL, MySQL и SQL Server, уже автоматически выполняют «выталкивание предикатов» и переупорядочивание соединений. Т.е. оба запроса переписываются по одному и тому же оптимальному плану. Поэтому ручная оптимизация в подзапрос не ускоряет работу, а просто затрудняет чтение SQL-кода.
Итого
Пишите понятный, читаемый SQL. Позвольте оптимизатору делать свою работу. В непонятных ситуациях используйте EXPLAIN ANALYZE, чтобы понять, что на самом деле делает БД и действительно ли один запрос быстрее другого.
Источник: https://www.milanjovanovic.tech/blog/debunking-the-filter-early-join-later-sql-performance-myth
👍9
День 2325. #TipsAndTricks
Не Изобретайте Велосипед — Конфигурация
Часто в различных решениях dotnet core, можно встретить код вроде следующего:
EnvironmentHelper выглядит так:
С этим кодом есть проблема. Каждый раз, когда вызывается EnvironmentHelper.IsLocal, он создаёт новый экземпляр ConfigurationBuilder и считывает
Примечание: Вообще, идея регистрировать различные реализации в зависимости от того, в какой среде исполняется код, тоже не очень, но это уже другая история.
При регистрации сервисов можно использовать перегрузку, которая даёт доступ к провайдеру сервисов:
Здесь мы извлекаем IHostEnvironment из провайдера сервисов. Но это требует регистрации как MockClient, так и ClientService, поскольку мы используем контейнер для разрешения экземпляров.
Лучшим подходом является использование построителя. Он содержит свойство Environment:
По умолчанию фреймворк устанавливает среду на основе значения переменной среды
Бонус
Если же вы не можете избавиться от текущей реализации EnvironmentHelper из-за объёма рефакторинга, можно использовать такой «костыль», чтобы хотя бы не создавать ConfigurationBuilder при каждом обращении:
Источник: https://josef.codes/dont-reinvent-the-wheel-configuration-dotnet-core/
Не Изобретайте Велосипед — Конфигурация
Часто в различных решениях dotnet core, можно встретить код вроде следующего:
// Program.cs
…
if(EnvironmentHelper.IsLocal)
services
.AddSingleton<IClient, MockClient>();
else
services
.AddSingleton<IClient, ClientService>();
…
EnvironmentHelper выглядит так:
public static class EnvironmentHelper
{
public static bool IsLocal =>
{
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
return config.GetValue<bool>("IsDevelopmentEnvironment");
}
}
С этим кодом есть проблема. Каждый раз, когда вызывается EnvironmentHelper.IsLocal, он создаёт новый экземпляр ConfigurationBuilder и считывает
appsettings.json с диска. Код используется по всей кодовой базе. Нехорошо. Мы можем избежать этого и использовать встроенные инструменты фреймворка вместо того, чтобы придумывать собственные решения.Примечание: Вообще, идея регистрировать различные реализации в зависимости от того, в какой среде исполняется код, тоже не очень, но это уже другая история.
При регистрации сервисов можно использовать перегрузку, которая даёт доступ к провайдеру сервисов:
builder.Services
.AddSingleton<IClient>(provider =>
{
var env = provider.GetRequiredService<IHostEnvironment>();
if(env.IsDevelopment())
return provider.GetRequiredService<MockClient>();
return provider.GetRequiredService<ClientService>();
});
Здесь мы извлекаем IHostEnvironment из провайдера сервисов. Но это требует регистрации как MockClient, так и ClientService, поскольку мы используем контейнер для разрешения экземпляров.
Лучшим подходом является использование построителя. Он содержит свойство Environment:
var builder = WebApplication.CreateBuilder(args);
if(builder.Environment.IsDevelopment())
builder.Services
.AddSingleton<IClient, MockClient>();
else
builder.Services
.AddSingleton<IClient, ClientService>();
По умолчанию фреймворк устанавливает среду на основе значения переменной среды
ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT, поэтому нет никакой необходимости задействовать appsettings.json вообще.Бонус
Если же вы не можете избавиться от текущей реализации EnvironmentHelper из-за объёма рефакторинга, можно использовать такой «костыль», чтобы хотя бы не создавать ConfigurationBuilder при каждом обращении:
public static class EnvironmentHelper
{
private static readonly Lazy<bool> _isLocal;
static EnvironmentHelper()
{
_isLocal = new Lazy<bool>(() => {
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
return config.GetValue<bool>("IsDevelopmentEnvironment");
},
LazyThreadSafetyMode.ExecutionAndPublication);
}
public static bool IsLocal => _isLocal.Value;
}
Источник: https://josef.codes/dont-reinvent-the-wheel-configuration-dotnet-core/
👍13
День 2340. #TipsAndTricks
Широко известный в узких кругах дотнетчиков блогер Ник Чапсас помимо основных видео на своём ютуб-канале также выпускает шортсы с короткими советами по написанию более лучшего кода. Так вот, таких советов накопилось уже более сотни, поэтому он собрал первую сотню в одно почти часовое видео.
Смотрим и мотаем на ус 😉
https://youtu.be/8F-Pb-SKO5g
Широко известный в узких кругах дотнетчиков блогер Ник Чапсас помимо основных видео на своём ютуб-канале также выпускает шортсы с короткими советами по написанию более лучшего кода. Так вот, таких советов накопилось уже более сотни, поэтому он собрал первую сотню в одно почти часовое видео.
Смотрим и мотаем на ус 😉
https://youtu.be/8F-Pb-SKO5g
YouTube
100 Must Know Tips to Write Better C#
Get 30% off everything on Dometrain: https://dometrain.com/courses/?ref=nick-chapsas&promo=youtube&coupon_code=SUMMER30
Subscribe to my weekly newsletter: https://nickchapsas.com
Hello, everybody. I'm Nick, and in this video, I will show you 100 .NET Tips…
Subscribe to my weekly newsletter: https://nickchapsas.com
Hello, everybody. I'm Nick, and in this video, I will show you 100 .NET Tips…
👍23
День 2345. #TipsAndTricks
Добавляем Описание в Параметризованные Тесты
Часто нам требуется протестировать несколько вариантов использования метода с разными данными, и для этого подойдут параметризованные тесты, например, Theory в xUnit.
См. подробнее про параметризованные тесты в xUnit.
При этом бывает полезно добавить не только тестовые данные, но и описание к каждому тестовому случаю. Рассмотрим, как это сделать.
Представим, что у нас есть такой тест:
Тест принимает следующую запись с тестовыми данными:
Если мы выполним тест, мы увидим в окне выполнения теста что-то вроде следующего:
Как видите, сложно понять, о чём каждый тестовый случай, особенно учитывая, что у нас есть коллекции в параметрах. Но обратите внимание, что из-за использования record, мы видим строковое представление записи, т.к. среда выполнения вызывает метод ToString() параметров. Мы можем использовать это.
Чтобы заставить среду выводить более осмысленное описание, мы можем добавить описание теста в LimitDesignerFilters и переопределить метод ToString():
Теперь мы можем задать свойству Description описание каждого тестового случая:
Тогда в окне выполнения теста мы увидим следующее:
Тут всё ещё присутствует название параметра (filters), но всё же, понять, что проверяет каждый тест, уже гораздо проще.
Источник: https://steven-giesel.com/blogPost/80a53df4-a867-4202-916c-08e980f02505/adding-test-description-for-datadriven-tests-in-xunit
Добавляем Описание в Параметризованные Тесты
Часто нам требуется протестировать несколько вариантов использования метода с разными данными, и для этого подойдут параметризованные тесты, например, Theory в xUnit.
См. подробнее про параметризованные тесты в xUnit.
При этом бывает полезно добавить не только тестовые данные, но и описание к каждому тестовому случаю. Рассмотрим, как это сделать.
Представим, что у нас есть такой тест:
[Theory]
[MemberData(nameof(InvalidFilters))]
public async Task ShouldNotAllowInvalidInvariants(
LimitFilters filters)
{
…
}
Тест принимает следующую запись с тестовыми данными:
public record LimitFilters(
Guid? WorkpieceNumber,
IEnumerable<int>? Ids,
IEnumerable<int>? Tools,
IEnumerable<int>? LimitIds);
}
Если мы выполним тест, мы увидим в окне выполнения теста что-то вроде следующего:
✅ ShouldNotAllowInvalidInvariants(filters: { WorkpieceNumber = … })
✅ ShouldNotAllowInvalidInvariants(filters: { WorkpieceNumber = … })Как видите, сложно понять, о чём каждый тестовый случай, особенно учитывая, что у нас есть коллекции в параметрах. Но обратите внимание, что из-за использования record, мы видим строковое представление записи, т.к. среда выполнения вызывает метод ToString() параметров. Мы можем использовать это.
Чтобы заставить среду выводить более осмысленное описание, мы можем добавить описание теста в LimitDesignerFilters и переопределить метод ToString():
public record LimitDesignerFilters(
string Description,
Guid? WorkpieceNumber,
IEnumerable<int>? Ids,
IEnumerable<int>? Tools,
IEnumerable<int>? LimitIds)
{
public override string ToString()
=> Description;
}
Теперь мы можем задать свойству Description описание каждого тестового случая:
public static TheoryData<LimitDesignerFilters>
InvalidFilters =>
[
new("Workpiece is null", null, [1], [1], [1]),
new("Param1 is null", Guid.NewGuid(), null, [1], [1]),
];
Тогда в окне выполнения теста мы увидим следующее:
✅ ShouldNotAllowInvalidInvariants(filters: Param1 is null)
✅ ShouldNotAllowInvalidInvariants(filters: Workpiece is null)
Тут всё ещё присутствует название параметра (filters), но всё же, понять, что проверяет каждый тест, уже гораздо проще.
Источник: https://steven-giesel.com/blogPost/80a53df4-a867-4202-916c-08e980f02505/adding-test-description-for-datadriven-tests-in-xunit
👍16
День 2352. #TipsAndTricks
Используем Roslyn Для Улучшения Кода. Начало
Следующий пример проверит все публичные типы решения на предмет, можно ли сделать их internal.
Создадим консольное приложение, и добавим следующие ссылки в .csproj:
Код перебирает типы в решении, проверяет, есть ли ссылки на тип за пределами проекта. Если нет, тип может быть internal:
Источник: https://www.meziantou.net/how-to-find-public-symbols-that-can-be-internal-using-roslyn.htm
Используем Roslyn Для Улучшения Кода. Начало
Следующий пример проверит все публичные типы решения на предмет, можно ли сделать их internal.
Создадим консольное приложение, и добавим следующие ссылки в .csproj:
<PackageReference Include="Microsoft.Build.Locator" Version="1.9.1" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.14.8" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
Код перебирает типы в решении, проверяет, есть ли ссылки на тип за пределами проекта. Если нет, тип может быть internal:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
var path = @"Sample.sln";
Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
var ws = MSBuildWorkspace.Create();
var sln = await
ws.OpenSolutionAsync(path);
// Перебираем проекты в решении
foreach (var proj in sln.Projects)
{
if (!proj.SupportsCompilation)
continue;
var comp = await proj.GetCompilationAsync();
if (comp is null)
continue;
// Проверяем, может ли тип быть internal
foreach (var symb in GetTypes(comp.Assembly))
{
// Вычисляем видимость
var visibility = GetVisibility(symb);
if (visibility is not Visibility.Public)
continue;
var canBeInternal = true;
// Проверяем внешние ссылки
var refs = await SymbolFinder
.FindReferencesAsync(symb, sln);
foreach (var rf in refs)
{
foreach (var loc in rf.Locations)
{
if (loc.Document.Project != proj)
canBeInternal = false;
}
}
if (canBeInternal)
Console.WriteLine(
$"{symb.ToDisplayString()} может быть internal");
}
}
static IEnumerable<ITypeSymbol> GetTypes(IAssemblySymbol assembly)
{
var result = new List<ITypeSymbol>();
foreach (var module in assembly.Modules)
DoNS(result, module.GlobalNamespace);
return result;
static void DoNS(List<ITypeSymbol> result, INamespaceSymbol ns)
{
foreach (var type in ns.GetTypeMembers())
DoType(result, type);
foreach (var nestedNs in ns.GetNamespaceMembers())
DoNS(result, nestedNs);
}
static void DoType(List<ITypeSymbol> result, ITypeSymbol s)
{
result.Add(s);
foreach (var type in s.GetTypeMembers())
DoType(result, type);
}
}
static Visibility GetVisibility(ISymbol s)
{
var vis = Visibility.Public;
switch (s.Kind)
{
case SymbolKind.Alias:
return Visibility.Private;
case SymbolKind.Parameter:
return GetVisibility(s.ContainingSymbol);
case SymbolKind.TypeParameter:
return Visibility.Private;
}
while (s is not null &&
s.Kind != SymbolKind.Namespace)
{
switch (s.DeclaredAccessibility)
{
case Accessibility.NotApplicable:
case Accessibility.Private:
return Visibility.Private;
case Accessibility.Internal:
case Accessibility.ProtectedAndInternal:
vis = Visibility.Internal;
break;
}
s = s.ContainingSymbol;
}
return vis;
}
enum Visibility
{
Public,
Internal,
Private,
}
Источник: https://www.meziantou.net/how-to-find-public-symbols-that-can-be-internal-using-roslyn.htm
👍12
День 2353. #TipsAndTricks
Используем Roslyn Для Улучшения Кода. Окончание
Начало
Вчера мы рассмотрели, как использовать Roslyn для поиска всех типов, которые могут быть обозначены внутренними, вместо публичных. Продолжим эту серию, и сегодня посмотрим, как найти все типы, которые могут быть отмечены как запечатанные (sealed). Обозначение типа как запечатанного может повысить производительность и безопасность, предотвращая дальнейшее наследование. Обратите внимание, что удаление модификатора sealed не является критическим изменением, поэтому вы можете спокойно помечать типы как запечатанные, не беспокоясь о проблемах совместимости, если позже решите удалить модификатор.
Создадим консольное приложение и добавим необходимые NuGet-пакеты в файл .csproj:
Используем следующий код для анализа решения и поиска всех типов, которые можно запечатать. Код переберёт все типы в решении и попросит Roslyn найти производные классы для каждого. Если их нет, тип можно запечатать:
Источник: https://www.meziantou.net/how-to-find-all-types-that-can-be-sealed-using-roslyn.htm
Используем Roslyn Для Улучшения Кода. Окончание
Начало
Вчера мы рассмотрели, как использовать Roslyn для поиска всех типов, которые могут быть обозначены внутренними, вместо публичных. Продолжим эту серию, и сегодня посмотрим, как найти все типы, которые могут быть отмечены как запечатанные (sealed). Обозначение типа как запечатанного может повысить производительность и безопасность, предотвращая дальнейшее наследование. Обратите внимание, что удаление модификатора sealed не является критическим изменением, поэтому вы можете спокойно помечать типы как запечатанные, не беспокоясь о проблемах совместимости, если позже решите удалить модификатор.
Создадим консольное приложение и добавим необходимые NuGet-пакеты в файл .csproj:
<PackageReference Include="Microsoft.Build.Locator" Version="1.9.1" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="17.14.8" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
Используем следующий код для анализа решения и поиска всех типов, которые можно запечатать. Код переберёт все типы в решении и попросит Roslyn найти производные классы для каждого. Если их нет, тип можно запечатать:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
var path = @"Sample.sln";
Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
var ws = MSBuildWorkspace.Create();
var sln = await ws.OpenSolutionAsync(path);
foreach (var proj in sln.Projects)
{
if (!proj.SupportsCompilation)
continue;
var comp = await proj.GetCompilationAsync();
if (comp is null)
continue;
foreach (var symb in GetTypes(comp.Assembly))
{
if (symb is not INamedTypeSymbol namedType)
continue;
if (namedType.TypeKind is not TypeKind.Class)
continue;
if (namedType.IsSealed)
continue;
var derivedClasses = await
SymbolFinder.FindDerivedClassesAsync(namedType, sln);
if (!derivedClasses.Any())
Console.WriteLine(
$"{symb.ToDisplayString()} может быть sealed");
}
}
static IEnumerable<ITypeSymbol>
GetTypes(IAssemblySymbol assembly)
{
var result = new List<ITypeSymbol>();
foreach (var module in assembly.Modules)
DoNS(result, module.GlobalNamespace);
return result;
static void DoNS(
List<ITypeSymbol> result,
INamespaceSymbol ns)
{
foreach (var t in ns.GetTypeMembers())
DoType(result, t);
foreach (var ns in ns.GetNamespaceMembers())
DoNS(result, ns);
}
static void DoType(
List<ITypeSymbol> result,
ITypeSymbol symb)
{
result.Add(symb);
foreach (var type in symb.GetTypeMembers())
DoType(result, type);
}
}
Источник: https://www.meziantou.net/how-to-find-all-types-that-can-be-sealed-using-roslyn.htm
👍9
День 2378. #TipsAndTricks
Используем COPY для Экспорта/Импорта Данных в Potgresql
В PostgreSQL есть функция, позволяющая эффективно выполнять массовый импорт и экспорт данных в таблицу и из неё. Обычно это гораздо более быстрый способ загрузки данных в таблицу и извлечения из неё, чем использование команд INSERT и SELECT.
В .NET провайдер Npgsql поддерживает три режима операции COPY: бинарный, текстовый и бинарный необработанный.
1. Бинарный
Пользователь использует API для чтения и записи строк и полей, которые Npgsql кодирует и декодирует. После завершения необходимо вызвать функцию Complete() для сохранения данных; в противном случае операция COPY будет откачена при освобождении объекта записи (это поведение важно в случае возникновения исключения).
2. Текстовый
В этом режиме данные в БД и из неё передаются в текстовом или CSV-формате PostgreSQL. Пользователь должен самостоятельно отформатировать текст или CSV-файл, Npgsql предоставляет только функции чтения или записи текста. Этот режим менее эффективен, чем бинарный, и подходит, если у вас уже есть данные в CSV, а производительность не критична.
3. Бинарный необработанный
Данные передаются в двоичном формате, но Npgsql не выполняет никакого кодирования или декодирования — данные предоставляются как необработанный поток .NET. Имеет смысл только для обработки больших объемов данных и восстановления таблицы: таблица сохраняется как BLOB-объект, который впоследствии можно восстановить. Если нужно разбирать данные, используйте обычный бинарный режим.
Отмена
Операции импорта можно отменить в любой момент, освободив (dispose) NpgsqlBinaryImporter до вызова метода Complete(). Операции экспорта можно отменить, вызвав метод Cancel().
Источник: https://www.npgsql.org/doc/copy.html
Используем COPY для Экспорта/Импорта Данных в Potgresql
В PostgreSQL есть функция, позволяющая эффективно выполнять массовый импорт и экспорт данных в таблицу и из неё. Обычно это гораздо более быстрый способ загрузки данных в таблицу и извлечения из неё, чем использование команд INSERT и SELECT.
В .NET провайдер Npgsql поддерживает три режима операции COPY: бинарный, текстовый и бинарный необработанный.
1. Бинарный
Пользователь использует API для чтения и записи строк и полей, которые Npgsql кодирует и декодирует. После завершения необходимо вызвать функцию Complete() для сохранения данных; в противном случае операция COPY будет откачена при освобождении объекта записи (это поведение важно в случае возникновения исключения).
// Импорт в таблицу с 2 полями (string, int)
using (var writer = conn.BeginBinaryImport(
"COPY my_table (field1, field2) FROM STDIN (FORMAT BINARY)"))
{
writer.WriteRow("Row1", 123);
writer.WriteRow("Row2", 123);
writer.Complete();
}
// Экспорт из таблицы с 2 полями
using (var rdr = conn.BeginBinaryExport(
"COPY my_table (field1, field2) TO STDOUT (FORMAT BINARY)"))
{
rdr.StartRow();
Console.WriteLine(rdr.Read<string>());
Console.WriteLine(rdr.Read<int>(NpgsqlDbType.Smallint));
rdr.StartRow();
// пропускает поле
rdr.Skip();
// проверяет на NULL (без перехода на следующее поле)
Console.WriteLine(rdr.IsNull);
Console.WriteLine(rdr.Read<int>());
rdr.StartRow();
// StartRow() вернёт -1 в конце данных
}
2. Текстовый
В этом режиме данные в БД и из неё передаются в текстовом или CSV-формате PostgreSQL. Пользователь должен самостоятельно отформатировать текст или CSV-файл, Npgsql предоставляет только функции чтения или записи текста. Этот режим менее эффективен, чем бинарный, и подходит, если у вас уже есть данные в CSV, а производительность не критична.
using (var writer = conn.BeginTextImport(
"COPY my_table (field1, field2) FROM STDIN"))
{
writer.Write("HELLO\t1\n");
writer.Write("GOODBYE\t2\n");
}
using (var reader = conn.BeginTextExport(
"COPY my_table (field1, field2) TO STDOUT"))
{
Console.WriteLine(reader.ReadLine());
Console.WriteLine(reader.ReadLine());
}
3. Бинарный необработанный
Данные передаются в двоичном формате, но Npgsql не выполняет никакого кодирования или декодирования — данные предоставляются как необработанный поток .NET. Имеет смысл только для обработки больших объемов данных и восстановления таблицы: таблица сохраняется как BLOB-объект, который впоследствии можно восстановить. Если нужно разбирать данные, используйте обычный бинарный режим.
int len;
var data = new byte[10000];
// Экспорт table1 в массив данных
using (var inStream = conn.BeginRawBinaryCopy(
"COPY table1 TO STDOUT (FORMAT BINARY)"))
{
// Предполагаем, что данные влезут в 10000 байт
// В реальности их нужно читать блоками
len = inStream.Read(data, 0, data.Length);
}
// Импорт данных в table2
using (var outStream = conn.BeginRawBinaryCopy(
"COPY table2 FROM STDIN (FORMAT BINARY)"))
{
outStream.Write(data, 0, len);
}
Отмена
Операции импорта можно отменить в любой момент, освободив (dispose) NpgsqlBinaryImporter до вызова метода Complete(). Операции экспорта можно отменить, вызвав метод Cancel().
Источник: https://www.npgsql.org/doc/copy.html
👍16
День 2384. #TipsAndTricks
Как Продолжить Выполнение Процесса После Завершения Задания GitHub Action
После завершения задания GitHub Actions обработчик завершает все запущенные им дочерние процессы. Он идентифицирует эти процессы, проверяя переменную окружения RUNNER_TRACKING_ID. Любой процесс, где эта переменная установлена переменной считается дочерним процессом обработчика и будет остановлен.
Чтобы процесс продолжил работу после завершения задания, запустите его без переменной окружения RUNNER_TRACKING_ID:
Либо:
Примечание: В обработчиках, размещенных на GitHub, это решение не поможет, поскольку вся виртуальная машина удаляется после завершения задания.
Источник: https://www.meziantou.net/how-to-keep-processes-running-after-a-github-action-job-ends.htm
Как Продолжить Выполнение Процесса После Завершения Задания GitHub Action
После завершения задания GitHub Actions обработчик завершает все запущенные им дочерние процессы. Он идентифицирует эти процессы, проверяя переменную окружения RUNNER_TRACKING_ID. Любой процесс, где эта переменная установлена переменной считается дочерним процессом обработчика и будет остановлен.
Чтобы процесс продолжил работу после завершения задания, запустите его без переменной окружения RUNNER_TRACKING_ID:
var psi = new ProcessStartInfo("sample_app");
psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID");
Process.Start(psi);Либо:
# Вариант 1
$env:RUNNER_TRACKING_ID = $null
# Вариант 2
$psi = New-Object System.Diagnostics.ProcessStartInfo "sample_app"
$psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID")
[System.Diagnostics.Process]::Start($psi)
Примечание: В обработчиках, размещенных на GitHub, это решение не поможет, поскольку вся виртуальная машина удаляется после завершения задания.
Источник: https://www.meziantou.net/how-to-keep-processes-running-after-a-github-action-job-ends.htm
👍2
День 2385. #TipsAndTricks
5 Современных Возможностей C#, Которые Улучшат Ваш Код
C# стал гораздо более выразительным и мощным языком, чем был ещё несколько лет назад. Вот 5 новинок языка, которые отличают инженеров, просто «знающих C#», от тех, кто использует его на полную.
1. Структуры только для чтения для критически важных для производительности типов-значений
Обычная структура копируется без необходимости, что приводит к скрытым потерям производительности. Отметив её как только для чтения, вы сообщаете компилятору, что её поля никогда не изменятся, что позволяет среде выполнения не создавать защитные копии.
Раньше:
Сейчас:
Для высокопроизводительного кода, такого как графика, игры или API с большим количеством математических вычислений, это небольшое ключевое слово может означать значительную экономию.
2. Ключевое слово scoped для предотвращения использования ref-переменных вне области видимости
C# позволяет использовать ref-переменные для повышения производительности, но распространённой ошибкой является случайное их использование вне области видимости (возвращение ссылки на что-то, что больше не является безопасным). scoped гарантирует во время компиляции, что ссылка не проживёт дольше положенного.
Без scoped:
scoped:
3. Обязательные свойства для принудительной инициализации объекта
Сколько раз вы создавали объект и забывали установить одно из его критически важных свойств? При использовании обязательных свойств компилятор принуждает инициализировать объект.
Раньше:
Сейчас:
4. Возврат типа ref readonly, чтобы избежать защитного копирования
При возврате больших структур возврат по значению часто приводит к ненужному копированию. Возврат типа ref readonly даёт вызывающим функциям ссылку на объект, который они могут читать, но не могут изменять.
Без ref readonly:
С ref readonly:
Это особенно актуально в библиотеках, критичных к производительности, где структуры неизменяемы, но копирование требует больших затрат.
5. Статические абстрактные члены в интерфейсах для обобщённой математики
Это кажется узкоспециализированной функцией, но открывает доступ к настоящей обобщённой математике без рефлексии и упаковки. До появления этой функции нельзя было писать обобщённые алгоритмы, работающие с числовыми типами:
Благодаря статическим абстрактным членам, INumber<T> определяет операторы и константы обобщённо, обеспечивая чистые, независимые от типов математические библиотеки.
Итого
Современный C# — это не просто новый синтаксис; это достижение ясности, безопасности и производительности, которых просто не могли предложить старые версии. Начните внедрять эти функции в свой код, и вы не только начнёте писать лучшее ПО, но и по-новому взглянете на возможности языка.
Источник: https://blog.stackademic.com/5-modern-c-features-that-will-make-your-code-feel-like-magic-d1ef6a374d13
5 Современных Возможностей C#, Которые Улучшат Ваш Код
C# стал гораздо более выразительным и мощным языком, чем был ещё несколько лет назад. Вот 5 новинок языка, которые отличают инженеров, просто «знающих C#», от тех, кто использует его на полную.
1. Структуры только для чтения для критически важных для производительности типов-значений
Обычная структура копируется без необходимости, что приводит к скрытым потерям производительности. Отметив её как только для чтения, вы сообщаете компилятору, что её поля никогда не изменятся, что позволяет среде выполнения не создавать защитные копии.
Раньше:
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
Сейчас:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
Для высокопроизводительного кода, такого как графика, игры или API с большим количеством математических вычислений, это небольшое ключевое слово может означать значительную экономию.
2. Ключевое слово scoped для предотвращения использования ref-переменных вне области видимости
C# позволяет использовать ref-переменные для повышения производительности, но распространённой ошибкой является случайное их использование вне области видимости (возвращение ссылки на что-то, что больше не является безопасным). scoped гарантирует во время компиляции, что ссылка не проживёт дольше положенного.
Без scoped:
ref int Dangerous(ref int number)
{
// Может быть использовано извне и привести к проблемам
return ref number;
}
scoped:
ref int Safe(scoped ref int number)
{
// Компилятор проверяет, что number может
// использоваться только внутри метода
// и выдаёт ошибку компиляции
return ref number;
}
3. Обязательные свойства для принудительной инициализации объекта
Сколько раз вы создавали объект и забывали установить одно из его критически важных свойств? При использовании обязательных свойств компилятор принуждает инициализировать объект.
Раньше:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
// Можно забыть инициализировать свойства
var user = new User { Name = "John" };
Сейчас:
public class User
{
public required string Name { get; init; }
public required string Email { get; init; }
}
// Компилятор проверяет полноту инициализации
var user = new User {
Name = "John", Email = "john@mail.com" };
4. Возврат типа ref readonly, чтобы избежать защитного копирования
При возврате больших структур возврат по значению часто приводит к ненужному копированию. Возврат типа ref readonly даёт вызывающим функциям ссылку на объект, который они могут читать, но не могут изменять.
Без ref readonly:
public BigStruct GetData() =>
_bigStruct; // Копирует структуру
С ref readonly:
public ref readonly BigStruct GetData() =>
ref _bigStruct; // Нет копирования
Это особенно актуально в библиотеках, критичных к производительности, где структуры неизменяемы, но копирование требует больших затрат.
5. Статические абстрактные члены в интерфейсах для обобщённой математики
Это кажется узкоспециализированной функцией, но открывает доступ к настоящей обобщённой математике без рефлексии и упаковки. До появления этой функции нельзя было писать обобщённые алгоритмы, работающие с числовыми типами:
T Add<T>(T a, T b) where T : INumber<T>
=> a + b;
Благодаря статическим абстрактным членам, INumber<T> определяет операторы и константы обобщённо, обеспечивая чистые, независимые от типов математические библиотеки.
Итого
Современный C# — это не просто новый синтаксис; это достижение ясности, безопасности и производительности, которых просто не могли предложить старые версии. Начните внедрять эти функции в свой код, и вы не только начнёте писать лучшее ПО, но и по-новому взглянете на возможности языка.
Источник: https://blog.stackademic.com/5-modern-c-features-that-will-make-your-code-feel-like-magic-d1ef6a374d13
👍30
День 2390. #TipsAndTricks
Как не Возвращать Все Свойства в SqlRaw
В Entity Framework SqlRaw есть небольшое, иногда раздражающее ограничение: SQL-запрос должен возвращать данные для всех свойств типа сущности.
Иногда этого не нужно, поэтому давайте посмотрим, как это очень просто обойти.
Представьте, что у нас есть такой объект:
По сути, мы указываем, что ни PropA, ни PropB не являются обязательными и, следовательно, если они отсутствуют в результирующем наборе, должны быть равны NULL. Но SqlRaw ожидает, что все свойства сущности присутствуют в предложении SELECT.
То есть:
Завершится с ошибкой, что EF не может сопоставить запрос с типом, поскольку отсутствует PropB.
Чтобы обойти эту проблему, просто явно возвращайте PropB как NULL:
Вот и всё, EF снова счастлив. Конечно, это упрощённый пример, но суть, надеюсь, вы поняли.
Источник: https://steven-giesel.com/blogPost/c6bea409-9e49-4915-8529-8a8a8574ba80/how-to-not-return-all-properties-in-sqlraw
Как не Возвращать Все Свойства в SqlRaw
В Entity Framework SqlRaw есть небольшое, иногда раздражающее ограничение: SQL-запрос должен возвращать данные для всех свойств типа сущности.
Иногда этого не нужно, поэтому давайте посмотрим, как это очень просто обойти.
Представьте, что у нас есть такой объект:
public sealed record MyEntity
{
public double? PropA { get; init; }
public double? PropB { get; init; }
}
По сути, мы указываем, что ни PropA, ни PropB не являются обязательными и, следовательно, если они отсутствуют в результирующем наборе, должны быть равны NULL. Но SqlRaw ожидает, что все свойства сущности присутствуют в предложении SELECT.
То есть:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA FROM MyTable")
.ToListAsync(token);
Завершится с ошибкой, что EF не может сопоставить запрос с типом, поскольку отсутствует PropB.
Чтобы обойти эту проблему, просто явно возвращайте PropB как NULL:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA, NULL AS PropB FROM MyTable")
.ToListAsync(token);
Вот и всё, EF снова счастлив. Конечно, это упрощённый пример, но суть, надеюсь, вы поняли.
Источник: https://steven-giesel.com/blogPost/c6bea409-9e49-4915-8529-8a8a8574ba80/how-to-not-return-all-properties-in-sqlraw
👍22
День 2403. #TipsAndTricks
Git Worktree: Управление Несколькими Рабочими Каталогами
Git Worktree — это мощный инструмент, позволяющий связать несколько рабочих каталогов с одним репозиторием. Вместо того, чтобы переключаться между ветками и потенциально терять незафиксированную работу, вы можете одновременно извлекать разные ветки в разных каталогах без повторного клонирования репозитория.
Git Worktree создаёт дополнительные рабочие папки (деревья), связанные с тем же репозиторием. Каждое рабочее дерево может иметь извлекаемую ветку, что позволяет работать над несколькими функциями, исправлением ошибок или экспериментами без необходимости многократного клонирования всего репозитория. Главное преимущество заключается в том, что у вас есть только один индекс Git (каталог .git), что снижает использование дискового пространства и повышает производительность.
В процессе написания кода переключение контекстов может мешать работе. Рабочее дерево Git помогает:
- Сохранять контекст. Сохраняйте текущую работу без изменений, быстро переключаясь между исправлением ошибки или проверкой кода.
- Работать параллельно. Работайте над несколькими функциями одновременно без накладных расходов на переключение веток.
- Использовать ИИ-агента. Вы можете поручить ИИ-агенту работать над отдельной веткой, пока вы продолжаете выполнять текущие задачи.
- Снижать когнитивную нагрузку. Не нужно откладывать или фиксировать незавершенную работу при переключении задач.
- Сравнивать файлы. Легко сравнивайте изменения между ветками без необходимости переключаться между ними. Вы можете открыть несколько версий одного и того же файла в разных рабочих деревьях, что упрощает отслеживание того, как изменения влияют на ваш код.
Чтобы создать новое рабочее дерево:
Чтобы удалить рабочее дерево, можно использовать следующие команды:
Также можно получить список рабочих деревьев:
Рекомендации
- Храните рабочие деревья в предсказуемом месте (например, в каталогах на одном уровне с репозиториями).
- Используйте описательные имена каталогов, соответствующие названиям веток.
- Регулярно очищайте неиспользуемые рабочие деревья, чтобы избежать беспорядка.
Итого
Рабочее дерево Git — это малоиспользуемая функция, которая может значительно улучшить процесс разработки, особенно когда вам нужно одновременно поддерживать несколько контекстов.
Источник: https://www.meziantou.net/git-worktree-managing-multiple-working-directories.htm
Git Worktree: Управление Несколькими Рабочими Каталогами
Git Worktree — это мощный инструмент, позволяющий связать несколько рабочих каталогов с одним репозиторием. Вместо того, чтобы переключаться между ветками и потенциально терять незафиксированную работу, вы можете одновременно извлекать разные ветки в разных каталогах без повторного клонирования репозитория.
Git Worktree создаёт дополнительные рабочие папки (деревья), связанные с тем же репозиторием. Каждое рабочее дерево может иметь извлекаемую ветку, что позволяет работать над несколькими функциями, исправлением ошибок или экспериментами без необходимости многократного клонирования всего репозитория. Главное преимущество заключается в том, что у вас есть только один индекс Git (каталог .git), что снижает использование дискового пространства и повышает производительность.
В процессе написания кода переключение контекстов может мешать работе. Рабочее дерево Git помогает:
- Сохранять контекст. Сохраняйте текущую работу без изменений, быстро переключаясь между исправлением ошибки или проверкой кода.
- Работать параллельно. Работайте над несколькими функциями одновременно без накладных расходов на переключение веток.
- Использовать ИИ-агента. Вы можете поручить ИИ-агенту работать над отдельной веткой, пока вы продолжаете выполнять текущие задачи.
- Снижать когнитивную нагрузку. Не нужно откладывать или фиксировать незавершенную работу при переключении задач.
- Сравнивать файлы. Легко сравнивайте изменения между ветками без необходимости переключаться между ними. Вы можете открыть несколько версий одного и того же файла в разных рабочих деревьях, что упрощает отслеживание того, как изменения влияют на ваш код.
Чтобы создать новое рабочее дерево:
# Рабочее дерево для существующей ветки
git worktree add ../feature-branch feature-branch
# Рабочее дерево для новой ветки из main
git worktree add -b new-feature ../new-feature origin/main
Чтобы удалить рабочее дерево, можно использовать следующие команды:
# Удалить рабочее дерево
git worktree remove ../feature-branch
# Удалить директорию и очистить метаданные
rm -rf ../feature-branch
git worktree prune
Также можно получить список рабочих деревьев:
git worktree list
Рекомендации
- Храните рабочие деревья в предсказуемом месте (например, в каталогах на одном уровне с репозиториями).
- Используйте описательные имена каталогов, соответствующие названиям веток.
- Регулярно очищайте неиспользуемые рабочие деревья, чтобы избежать беспорядка.
Итого
Рабочее дерево Git — это малоиспользуемая функция, которая может значительно улучшить процесс разработки, особенно когда вам нужно одновременно поддерживать несколько контекстов.
Источник: https://www.meziantou.net/git-worktree-managing-multiple-working-directories.htm
👍16
День 2410. #TipsAndTricks
Улучшаем Отладку EF Core с Помощью Тегов Запросов
Отладка запросов к БД в EF Core иногда напоминает поиск иголки в стоге сена. Когда ваше приложение генерирует десятки или сотни SQL-запросов, определить, какое LINQ-выражение сгенерировало тот или иной SQL-запрос, становится настоящей проблемой. К счастью, есть элегантное решение: теги запросов.
Теги запросов
Теги запросов позволяют добавлять пользовательские комментарии к SQL-запросам, сгенерированным LINQ-выражениями. Эти комментарии отображаются непосредственно в сгенерированном SQL-коде, что позволяет легко сопоставлять SQL-запрос с кодом, который его создал. Чтобы использовать эту функцию, необходимо применить метод TagWith к любому объекту IQueryable и передать описательный комментарий:
Это сгенерирует примерно такой SQL-запрос:
Вместо того, чтобы пытаться определить, какой код сгенерировал тот или иной SQL-запрос, вы можете сразу увидеть цель и источник каждого запроса в логах БД или профилировщике.
Вы можете объединить несколько вызовов TagWith для добавления дополнительного контекста, а также добавлять значения переменных во время выполнения:
Результат:
*CorrelationId помогает проследить весь путь пользовательского запроса (см. подробнее).
Примечания:
1. Хотя влияние TagWith на производительность минимально, избегайте чрезмерно длинных тегов или сложной интерполяции строк в горячих путях.
2. Теги запросов должны быть строковыми литералами, и не могут принимать параметры. Однако можно использовать интерполяцию строк (как видно выше), а также многострочные строковые литералы.
Источник: https://dev.to/shayy/postgres-is-too-good-and-why-thats-actually-a-problem-4imc
Улучшаем Отладку EF Core с Помощью Тегов Запросов
Отладка запросов к БД в EF Core иногда напоминает поиск иголки в стоге сена. Когда ваше приложение генерирует десятки или сотни SQL-запросов, определить, какое LINQ-выражение сгенерировало тот или иной SQL-запрос, становится настоящей проблемой. К счастью, есть элегантное решение: теги запросов.
Теги запросов
Теги запросов позволяют добавлять пользовательские комментарии к SQL-запросам, сгенерированным LINQ-выражениями. Эти комментарии отображаются непосредственно в сгенерированном SQL-коде, что позволяет легко сопоставлять SQL-запрос с кодом, который его создал. Чтобы использовать эту функцию, необходимо применить метод TagWith к любому объекту IQueryable и передать описательный комментарий:
var оrders = context.Orders
.TagWith("Заказы больше $1000")
.Where(o => o.Total > 1000)
.Include(o => o.Customer)
.ToList();
Это сгенерирует примерно такой SQL-запрос:
-- Заказы больше $1000
SELECT [o].[Id], [o].[CustomerId], [o].[Total], [c].[Id], [c].[Name]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[Total] > 1000.0
Вместо того, чтобы пытаться определить, какой код сгенерировал тот или иной SQL-запрос, вы можете сразу увидеть цель и источник каждого запроса в логах БД или профилировщике.
Вы можете объединить несколько вызовов TagWith для добавления дополнительного контекста, а также добавлять значения переменных во время выполнения:
var userOrders = context.Orders
.TagWith("Запрос с дэшборда")
.TagWith($"User ID: {userId}")
.TagWith($"CorrelationId: {correlationId}")
.Where(o => o.CustomerId == userId)
.OrderByDescending(o => o.OrderDate)
.Take(10)
.ToList();
Результат:
-- Запрос с дэшборда
-- User ID: 12345
-- CorrelationId: 987654321
SELECT TOP(10) [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Orders] AS [o]
WHERE [o].[CustomerId] = 12345
ORDER BY [o].[OrderDate] DESC
*CorrelationId помогает проследить весь путь пользовательского запроса (см. подробнее).
Примечания:
1. Хотя влияние TagWith на производительность минимально, избегайте чрезмерно длинных тегов или сложной интерполяции строк в горячих путях.
2. Теги запросов должны быть строковыми литералами, и не могут принимать параметры. Однако можно использовать интерполяцию строк (как видно выше), а также многострочные строковые литералы.
Источник: https://dev.to/shayy/postgres-is-too-good-and-why-thats-actually-a-problem-4imc
1👍34