.NET Разработчик
6.48K subscribers
421 photos
2 videos
14 files
2.02K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 1806. #TipsAndTricks
10 Крутых Трюков в C#. Продолжение

1-2
3-4
5
6
7
8

9. Преобразование анонимных типов в dynamic
Преобразование анонимных типов в объекты dynamic может обеспечить большую гибкость при манипулировании данными. Анонимные типы доступны только для чтения и строго типизированы, что может ограничивать возможности изменения или расширения данных. Преобразуя анонимный тип в динамический
ExpandoObject вы получаете возможность добавлять, удалять или изменять свойства во время выполнения.
dynamic ToDynamic(object anon)
{
var dyn = new ExpandoObject()
as IDictionary<string, object>;
var props = TypeDescriptor.GetProperties(anon);
foreach (PropertyDescriptor p in props)
dyn.Add(p.Name, p.GetValue(anon));
return dyn;
}

// использование
var anon = new { Name = "John", Age = 30 };
dynamic dyn = ToDynamic(anon);
Console.WriteLine(
$"Name: {dyn.Name}, Age: {dyn.Age}");
dyn.Age = 35;
dyn.City = "New York";
Console.WriteLine(
@$"Name: {dyn.Name},
Age: {dyn.Age},
City:{dyn.City}");


Как это работает и почему это полезно:
В примере выше метод ToDynamic принимает анонимный объект в качестве входных данных и преобразует его в динамический ExpandoObject. Это делается путем перебора свойств анонимного объекта с помощью TypeDescriptor.GetProperties и добавления их в ExpandoObject с помощью интерфейса IDictionary<string, object>.

Далее демонстрируется, как использовать метод ToDynamic для преобразования анонимного объекта в динамический объект. Переменная anon содержит анонимный объект со свойствами Name и Age. После преобразования его в динамический объект с помощью ToDynamic вы можете напрямую получать доступ к его свойствам и изменять их, а также добавлять новые свойства, такие как City.

Преобразование анонимных типов в динамические объекты может быть полезно, когда вам нужна большая гибкость со структурами данных, особенно при работе с динамически генерируемыми данными или когда схема неизвестна во время компиляции. Однако имейте в виду, что использование динамических объектов может привести к потере проверки типов во время компиляции и потенциальным проблемам производительности.

Окончание следует...

Источник:
https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
👍7👎1
День 1807. #TipsAndTricks
10 Крутых Трюков в C#. Окончание

1-2
3-4
5
6
7
8
9

10. Простой пул объектов для повторно используемых ресурсов
Пул объектов — это шаблон проектирования, который помогает повторно использовать объекты, создание которых требует больших затрат, например соединения с базой данных или большие буферы памяти. Создав пул предварительно выделенных объектов и повторно используя их при необходимости, вы можете повысить производительность приложения и снизить накладные расходы, связанные с созданием и уничтожением объектов.
class ObjectPool<T> where T : new()
{
private readonly ConcurrentBag<T> _objects;
private readonly Func<T> _generator;

public ObjectPool(Func<T>? generator = null)
{
_generator = generator ??
(() => new T());
_objects = [];
}

public T Get()
{
return _objects.TryTake(out T item)
? item
: _generator();
}

public void Return(T item)
{
_objects.Add(item);
}
}

class Expensive
{
public int Value { get; set; }
}

// использование
var pool = new ObjectPool<Expensive>();
var resource = pool.Get();
resource.Value = 42;

Console.WriteLine(
$"Значение ресурса: {resource.Value}");
pool.Return(resource);


Как это работает и почему это полезно:
В примере выше класс ObjectPool<T> является обобщённой реализацией пула объектов. Он использует ConcurrentBag<T> для хранения объектов и делегат Func<T> для создания новых объектов при необходимости. Метод Get извлекает объект из пула, если он доступен, или создает новый, если пул пуст. Метод Return возвращает объект в пул, когда он больше не нужен. Класс Expensive представляет гипотетический ресурс, создание которого требует больших затрат.

В примере использования создается экземпляр ObjectPool<Expensive>, а экземпляр Expensive извлекается из пула с помощью метода Get. После манипуляции со свойствами объекта вызывается метод Return, который возвращает объект в пул для будущего повторного использования.

Использование пула объектов может повысить производительность приложения и сократить использование памяти за счет минимизации количества созданий и удалений объектов. Это особенно полезно в высокопроизводительных средах или средах с ограниченными ресурсами, где создание объектов и сборка мусора могут стать серьёзными узкими местами. Однако имейте в виду, что объединение объектов в пул усложняет код и может подходить не для всех сценариев.

Источник: https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
👍9👎1
День 1812. #TipsAndTricks
Используем Оператор Неявного Преобразования для Улучшения Читаемости
Оператор неявного преобразования — одна из самых крутых скрытых функций C#. И причина, по которой он не так популярен, заключается в том, что трудно придумать, как его использовать. Многие называют его неявным конструктором, понятно, откуда это взялось. Но давайте попробовать использовать его так, как Microsoft его задумали.

Оператор неявного преобразования позволяет нам без лишних усилий преобразовывать один тип в другой. Например:
class Color
{
public string HexCode { get; set; }

public Color(string hexCode)
{
HexCode = hexCode;
}

public static implicit operator
Color(string hexCode) => new(hexCode);
}

class Wall
{
public void Paint(Color color) =>
Console.WriteLine($"Цвет: {color.HexCode}");
}

У нас есть класс Color, который просто принимает в конструкторе шестнадцатеричный код цвета. Также есть класс Wall с методом Paint, который получает цвет.

Использование:
var wall = new Wall();
wall.Paint("#FF0000");


Мы создаём экземпляр класса Wall и вызываем метод Paint. Однако мы передаём строку, а не экземпляр Color. Как это возможно? Всё благодаря магии неявного оператора приведения. Если вы посмотрите на определение оператора в классе Color, вы увидите, что он вызывает конструктор, используя предоставленный шестнадцатеричный код.

Это очень простой сценарий, но его потенциал огромен. Например, вы можете использовать его для номеров паспортов, координат, URI, IP-адресов и многих других объектов-значений, элегантно решая проблему одержимости примитавами. Только не переусердствуйте и не используйте сверхсложные операторы.

Источник: https://intodot.net/using-implicit-conversion-operators-in-c-to-improve-readability/
👍20
День 1931. #TipsAndTricks
Обновляем Сертификат для localhost в .NET Core

Многие команды не используют https на машинах разработчиков, поскольку не хотят возиться с сертификатами. Хорошей новостью является то, что добавить или обновить сертификат локального хоста в случае истечения срока при использовании Kestrel очень просто. Для управления самоподписанным сертификатом можно использовать встроенную команду dotnet dev-certs.

При необходимости сначала можно удалить существующий сертификат:
dotnet dev-certs https --clean

Вывод:
Cleaning HTTPS development certificates from the machine. A prompt might get displayed to confirm the removal of some of the certificates.
HTTPS development certificates successfully removed from the machine.

(Очистка сертификатов разработки HTTPS с компьютера. Может появиться запрос на подтверждение удаления некоторых сертификатов.
Сертификаты разработки HTTPS успешно удалены с компьютера.)


Теперь сгенерируем новый самоподписанный сертификат, и сразу добавим его в доверенные:
dotnet dev-certs https --trust

Вывод:
Trusting the HTTPS development certificate was requested. A confirmation prompt will be displayed if the certificate was not previously trusted. Click yes on the prompt to trust the certificate.
Successfully created and trusted a new HTTPS certificate.

(Было запрошено доверие к сертификату разработки HTTPS. Если сертификат ранее не был доверенным, отобразится запрос на подтверждение. Нажмите «Да» в запросе, чтобы доверять сертификату.
Успешно создан новый доверенный сертификат HTTPS.)

Наконец, следующая команда проверяет существующие сертификаты:
dotnet dev-certs https --check

Output:
A valid certificate was found: 189E61FFAD59C21110E9AD13A009B984EE5E8D5D - CN=localhost - Valid from 2024-04-22 13:11:50Z to 2025-04-22 13:11:50Z - IsHttpsDevelopmentCertificate: true - IsExportable: true

Run the command with both --check and --trust options to ensure that the certificate is not only valid but also trusted.

(Был найден действительный сертификат: 189E61FFAD59C21110E9AD13A009B984EE5E8D5D - CN=localhost - действителен с 22 апреля 2024 г., 13:11:50Z по 22 апреля 2025 г., 13:11:50Z - IsHttpsDevelopmentCertificate: true - IsExportable: true

Запустите команду с параметрами --check и --trust, чтобы убедиться, что сертификат не только действителен, но и добавлен в доверенные.)


Источник: https://bartwullems.blogspot.com/2024/05/net-core-renew-localhost-certificate.html
👍51
День 2053. #TipsAndTricks
Объединения по Нескольким Столбцам в Entity Framework

Я столкнулся с очень раздражающей «проблемой» с объединениями LINQ (left join) в Entity Framework.

Исходный запрос
var query = dbContext.Entity.Where(...);

var result = from e in query
join j in dbContext.JoinEntity
on new { e.Id, e.OtherId }
equals new { j.WId, j.OtherId } into group
from g in group.DefaultIfEmpty()
select new { e, g };


Этот запрос приводит к неприятной ошибке компилятора:
The type arguments cannot be inferred from the query. Candidates are: …
(Типы аргументов не могут быть выведены из запроса. Кандидаты: …) далее очень длинный текст.

Решение
Проблема в том, что я использовал несколько столбцов для объединения, но ВСЕ они должны иметь одинаковое имя, иначе компилятор не сможет вывести типы аргументов. Поэтому простое решение — что-то вроде:
var result = from e in query
join j in dbContext.JoinEntity
on new { WId = e.Id, OId = e.OtherId }
equals new { WId = j.WId, OId = j.OtherId } into group
from g in group.DefaultIfEmpty()
select new { e, g };

То есть оба столбца должны иметь одинаковое имя в анонимных типах. И да, можно было просто написать чистый SQL, но это отдельный вопрос.

Источник: https://steven-giesel.com/blogPost/78753461-d80f-4f81-9b4d-6484066aa43e/linq-joins-on-multiple-columns-in-entity-framework
Автор оригинала: Steven Giesel
👍15
День 2098. #Git #TipsAndTricks
Советы по Git: Предыдущие Ветки

Сегодня рассмотрим два совета, как легко переключаться между ветками вперед-назад и как получить последние использованные ветки.

Переключение между ветками
Представьте, что вы находитесь на ветке feature-1 и хотите переключиться на ветку main, чтобы выполнить там какую-то работу. После того, как вы закончите работу на main, вы хотите переключиться обратно на feature-1.

Не зная точного имени ветки, вы можете использовать:
git checkout -

или (начиная с версии Git 2.23):
git switch -


На самом деле, это сокращённая версия следующей команды:
git checkout @{-1}

Эта команда позволяет вернуться к любой предыдущей ветке.

Получение последних 5 использованных веток
Если вы хотите увидеть последние 5 веток, которые вы использовали, вы можете выполнить следующую команду:
git reflog | grep -o 'checkout: moving from [^ ]* to [^ ]*' | awk '{print $NF}' | awk '!seen[$0]++' | head -n 5 

Это работает с git bash и такими ОС, как Linux и MacOS. Идея заключается в использовании reflog для получения истории веток, которые вы извлекали. Команда grep фильтрует вывод, чтобы показывать только ветки, которые вы извлекли. Первая команда awk извлекает имена веток из вывода. Вторая команда awk удаляет дубликаты. Команда head ограничивает вывод последними 5 ветками. Поэтому, если вам нужны только последние 3 ветки, которые вы извлекли, вы можете изменить head -n 5 на head -n 3.

Источник: https://steven-giesel.com/blogPost/bbfb8333-e05a-4de7-88b9-17ac2248d77f/git-tricks-get-the-last-checked-out-branch
👍19
День 2183. #TipsAndTricks
Упрощаем Отладку Сторонних Типов

При отладке часто бывает полезно иметь возможность проверять состояние объектов в отладчике. Однако, когда вы работаете со сторонними типами, которые не имеют форматирования при отладке, вы не можете легко посмотреть их внутреннее состояние. Сегодня посмотрим, как добавить форматирование при отладке к сторонним типам.

DebuggerDisplayAttribute — очень полезный атрибут, позволяющий указать, как объект должен отображаться в отладчике. Ваши типы можно пометить этим атрибутом так:
[DebuggerDisplay("Name = {Name}, Age = {Age}")]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

var person = new Person { Name = "John", Age = 42 };


Это заставит отладчик отображать экземпляр объекта Person как Name = John, Age = 42.

Но что, если вы работаете со сторонним типом, в который вы не можете добавить атрибут DebuggerDisplay?

Существует менее известное свойство Target, которое определяет, к какому типу следует применять атрибут DebuggerDisplay. Мы можем использовать его в сочетании с атрибутом assembly, чтобы добавить любому типу форматирование при отладке. Для простоты добавим его для типа Task, хотя он уже отображается отформатированным при отладке.
[assembly: DebuggerDisplay("Is Task finished: {IsCompleted}", Target = typeof(Task))]

var task = Task.Delay(100_000);
await task;

Теперь при наведении мыши на переменную task вы увидите результат, похожий на тот, что на картинке ниже.

У этого подхода есть некоторые «естественные» недостатки:
1. Вы можете помечать только публичные типы или интерфейсы по очевидным причинам.
2. Может быть неочевидно, откуда берётся отладочная информация. Особенно, если у вас большая кодовая база, и кто-то в её «потайном уголке» помечает тип атрибутом DebuggerDisplay для форматирования при отладке.

Источник: https://www.meziantou.net/hsts-for-httpclient-in-dotnet.htm
👍35
День 2262. #TipsAndTricks
Применяем Естественную Сортировку в PowerShell

PowerShell не предоставляет встроенного способа использовать естественную сортировку. Рассмотрим, как это можно сделать при помощи простого скрипта.

Что такое естественная сортировка?
Естественная сортировка упорядочивает строки таким образом, чтобы это было более удобно для человека. Например, естественная сортировка следующих строк:
file1.txt
file10.txt
file2.txt

выдаст:
file1.txt
file2.txt
file10.txt

Как видите, естественная сортировка упорядочивает строки на основе чисел в строке, а не лексикографического порядка символов.

Простой способ получить естественную сортировку в PowerShell — использовать командлет Sort-Object с пользовательским блоком скрипта. Блок скрипта должен возвращать значение, по которому вы хотите выполнить сортировку. В этом случае вы можете использовать регулярное выражение для извлечения числового значения из строки и дополнить его нулями, чтобы гарантировать правильную сортировку чисел. Например, строка file1.txt будет преобразована в file00001.txt. Вы можете использовать столько нулей, сколько вам нужно, чтобы гарантировать правильную сортировку чисел.
Get-ChildItem | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(100) }) }


Кстати, возможность естественной сортировки строк появится в .NET 10 с помощью нового компаратора строк.

Источник: https://www.meziantou.net/how-to-use-a-natural-sort-in-powershell.htm
👍9👎4
День 2271. #TipsAndTricks #Blazor
Пользовательская Страница 404 в Blazor
Иногда нужно иметь пользовательскую (дружелюбную к посетителю) страницу 404. Начиная с .NET 8 в Blazor Web App тег <NotFound> маршрутизатора (Router) больше не работает, поэтому создадим собственную страницу.

До Blazor Web App
Раньше можно было использовать следующий код внутри компонента маршрутизатора:
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Здесь идёт код, отображающийся когда страница не найдена.</p>
</LayoutView>
</NotFound>
</Router>

Это всё ещё будет работать со «старым» подходом, где вы выбираете либо Blazor WebAssembly, либо Blazor Server (с использованием файла <script src="_framework/blazor.server.js"></script>). Но в новых шаблонах веб-приложения Blazor это больше не работает. Вы всё ещё можете добавить дочерний элемент NotFound в разметку маршрутизатора, но он не будет использоваться. Всё потому, что сам маршрутизатор теперь другой. Новый шаблон проекта использует файл <script src="_framework/blazor.web.js"></script> в App.razor и имеет AddInteractiveServerComponents в контейнере сервисов.

Добавление страницы «не найдено»
Мы можем определить веб-страницу, которая будет отображаться с более низкой специфичностью, т.е. иметь наименьший приоритет. Назовём её NotFoundPage.razor:
@page "/{*route:nonfile}"

<p>Здесь идёт код, отображающийся когда страница не найдена.</p>

@code {
[Parameter]
public string? Route { get; set; }
}

Параметр Route не используется, но он обязателен. В противном случае Blazor выбросит исключение, что он не может связать маршрут со свойством.

Источник: https://steven-giesel.com/blogPost/38a4f1dc-420f-4489-9179-77371a79b9a9/a-custom-404-page-in-blazor-web-apps
👍3
День 2280. #TipsAndTricks
Удаляем Пустые Папки в 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, переименует файлы и папки и заменит содержимое в файлах:
$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
👍26
День 2298. #TipsAndTricks
Очистка Кэшей 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
👍25
День 2302. #ЧтоНовенького #TipsAndTricks
Используем Расширения 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 позволяют добавлять новые защитные конструкции к существующим классам. Например, если мы хотим иметь такую «жутко полезную» семантику, как: «Выбрасывать исключение, если строка содержит ровно один символ», мы можем сделать что-то вроде этого:
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". В нём утверждается, что вместо того, чтобы сначала объединять таблицы, а затем применять фильтр к результатам, нужно делать наоборот.

Например, вместо:
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
👍8
День 2325. #TipsAndTricks
Не Изобретайте Велосипед — Конфигурация
Часто в различных решениях 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
👍22
День 2345. #TipsAndTricks
Добавляем Описание в Параметризованные Тесты

Часто нам требуется протестировать несколько вариантов использования метода с разными данными, и для этого подойдут параметризованные тесты, например, 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
👍15
День 2352. #TipsAndTricks
Используем 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
👍11
День 2353. #TipsAndTricks
Используем 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
👍8