День 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