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

Для связи: @SBenzenko

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

1-2
3-4

5. Конвейеры для потоковой обработки
Pipelines — библиотека потоковой обработки в .NET для эффективной обработки больших потоков данных с низкими задержками. Пространство имён System.IO.Pipelines предоставляет абстракции для чтения и записи данных в потоковом режиме, минимизируя при этом выделение памяти и копирование.
async Task ProcessStream(
Stream stream)
{
var pipe = new Pipe();
var write = FillPipe(stream, pipe.Writer);
var read = ReadPipe(pipe.Reader);
await Task.WhenAll(write, read);
}

async Task FillPipe(
Stream stream,
PipeWriter writer
)
{
const int minSize = 512;
while (true)
{
var memory = writer.GetMemory(minSize);
int bytes = await stream.ReadAsync(memory);
if (bytes == 0)
break;
writer.Advance(bytes);
var result = await writer.FlushAsync();
if (result.IsCompleted)
break;
}
writer.Complete();
}

async Task ReadPipe(
PipeReader reader)
{
while (true)
{
var result = await reader.ReadAsync();
var buffer = result.Buffer;
foreach (var seg in buffer)
{
// Обработка данных частями
Console.WriteLine(
$"Прочитано {seg.Length} байт");
}
reader.AdvanceTo(buffer.End);
if (result.IsCompleted)
break;
}
reader.Complete();
}

// использование
using var stream =
new MemoryStream(
Encoding.UTF8.GetBytes("Hello, Pipelines!"));
await ProcessStream(stream);

Вывод:
Прочитано 17 байт


Как это работает и почему это полезно:
В примере выше метод ProcessStream создаёт новый экземпляр Pipe и запускает две задачи: для записи данных в канал (FillPipe) и для чтения данных из канала (ReadPipe). Task.WhenAll используется для ожидания завершения обеих задач. FillPipe считывает данные из входного потока и записывает их в PipeWriter. Он делает это в цикле, получая память от PipeWriter и считывая данные из потока в память. Метод PipeWriter.FlushAsync вызывается, чтобы сигнализировать о том, что данные доступны для чтения, и цикл продолжается до тех пор, пока поток не будет исчерпан или канал не будет закрыт. Метод ReadPipeAsync ожидает PipeReader.ReadAsync, который возвращает ReadResult, содержащий буфер ReadOnlySequence<byte>. Буфер обрабатывается частями, а метод PipeReader.AdvanceTo вызывается, чтобы сигнализировать о том, что данные были использованы. Цикл продолжается до тех пор, пока конвейер не будет завершён.

Использование конвейеров для потоковой обработки может обеспечить значительный выигрыш в производительности, особенно в сценариях, где требуется обработка с малой задержкой и минимальное выделение памяти. Абстракции и дизайн библиотеки упрощают обработку сложных сценариев потоковой передачи, таких как сетевое взаимодействие или файловый ввод-вывод, с эффективным управлением ресурсами и оптимальной производительностью.

Продолжение следует…

Источник:
https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
День 1802. #TipsAndTricks
10 Крутых Трюков в C#. Продолжение

1-2
3-4
5

6. Рефлексия и деревья выражений
Деревья выражений могут быть полезны для сложных сценариев рефлексии, таких как создание динамических методов или оптимизация путей кода, критичных к производительности.
Func<T, object> GeneratePropertyGetter<T>(
string property
)
{
var param =
Expression.Parameter(typeof(T), "obj");
var prop =
Expression.Property(param, property);
var conv =
Expression.Convert(prop, typeof(object));
var lambda =
Expression.Lambda<Func<T, object>>(conv, param);
return lambda.Compile();
}

// использование
var person = new Person("John Doe", 30);
var getName = GeneratePropertyGetter<Person>("Name");
var getAge = GeneratePropertyGetter<Person>("Age");
Console.WriteLine(
$"Name: {getName(person)}, Age: {getAge(person)}");

record Person(string Name, int Age);

Вывод:
Name: John Doe, Age: 30


Как это работает и почему это полезно:
В примере выше метод GeneratePropertyGetter<T> демонстрирует, как использовать деревья выражений для создания метода получения свойств для заданного класса и имени свойства. Метод принимает параметр типа T и строку, представляющую имя свойства, затем создаёт дерево выражений, которое предоставляет доступ к свойству в экземпляре T и возвращает его значение.

Дерево выражений создается с использованием методов класса Expression, таких как Expression.Parameter, Expression.Property и Expression.Lambda. После завершения создания дерева выражений вызывается метод Compile для создания делегата Func<T, object>, который можно использовать для вызова метода получения свойств во время выполнения. Метод GeneratePropertyGetter используется для создания методов получения свойств для свойства Name и Age записи Person. Эти методы получения свойств затем используются для получения значений свойств из экземпляра Person.

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

Продолжение следует…

Источник:
https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
День 1803. #TipsAndTricks
10 Крутых Трюков в C#. Продолжение

1-2
3-4
5
6

7. Упрощение многопоточности с помощью каналов
Каналы — это примитив синхронизации, представленный в .NET Core 3.0, который упрощает многопоточность, предоставляя потокам возможность взаимодействовать и обмениваться данными в потокобезопасном режиме. Их можно использовать для реализации шаблона производитель-потребитель, позволяющего разделить производство и потребление данных.
async Task ProcessData()
{
var channel = Channel.CreateUnbounded<int>();
var producer = Task.Run(async () =>
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"Произведено: {i}");
await channel.Writer.WriteAsync(i);
await Task.Delay(1000);
}
channel.Writer.Complete();
});

var consumer = Task.Run(async () =>
{
await foreach (
var i in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Обработано: {i}");
}
});
await Task.WhenAll(producer, consumer);
}


Как это работает и почему это полезно:
В примере выше метод ProcessData демонстрирует простой сценарий производитель-потребитель с использованием канала. Переменная канала инициализируется как неограниченный канал, то есть может хранить неограниченное количество элементов.

Задача производителя генерирует данные (целые числа от 1 до 10) и записывает их в канал с помощью метода WriteAsync. Задача потребителя считывает данные из канала с помощью метода ReadAllAsync и обрабатывает их. В этом случае она просто выводит на консоль полученные данные.

Задачи производителя и потребителя выполняются одновременно, что позволяет потребителю обрабатывать данные, как только они становятся доступными. Класс Channel гарантирует, что обмен данными является потокобезопасным, что упрощает написание многопоточного кода, не беспокоясь о блокировках или других механизмах синхронизации.
Каналы можно использовать в различных сценариях, таких как конвейеры обработки данных, распараллеливание рабочих нагрузок или реализация связи между компонентами в многопоточном приложении. Они предоставляют простой в использовании и эффективный способ управления параллелизмом и обменом данными между потоками.

Продолжение следует…

Источник:
https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
День 1804. #TipsAndTricks
10 Крутых Трюков в C#. Продолжение

1-2
3-4
5
6
7

8. Динамическая компиляция кода с Roslyn
Динамическая компиляция кода с помощью Roslyn позволяет компилировать и выполнять код C# во время выполнения. Это может быть полезно для сценариев, плагинов или ситуаций, когда код необходимо генерировать или изменять «на лету».
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

async Task ExecuteDynamicCodeAsync(
string code)
{
string sourceCode = $@"
using System;
namespace DynamicCode;
public class Runner
{{
public static void Run()
{{
{code}
}}
}}
";

var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(
Path.Combine(assemblyPath, "System.Private.CoreLib.dll")),
MetadataReference.CreateFromFile(
Path.Combine(assemblyPath, "System.Console.dll")),
MetadataReference.CreateFromFile(
Path.Combine(assemblyPath, "System.Runtime.dll"))
};

var compilation = CSharpCompilation.Create("DynamicCode")
.WithOptions(new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary))
.AddReferences(references)
.AddSyntaxTrees(syntaxTree);

using var ms = new MemoryStream();
var result = compilation.Emit(ms);
if (!result.Success)
{
Console.WriteLine("Компиляция не удалась");
return;
}
ms.Seek(0, SeekOrigin.Begin);

var assembly = Assembly.Load(ms.ToArray());
var type = assembly.GetType("DynamicCode.Runner");
var method = type.GetMethod("Run",
BindingFlags.Static | BindingFlags.Public);
method.Invoke(null, null);
}

// использование
await ExecuteDynamicCodeAsync(
"Console.WriteLine(\"Привет, динамический код!\");"
);

Вывод:
Привет, динамический код!


Как это работает и почему это полезно:
В примере выше метод ExecuteDynamicCodeAsync демонстрирует, как скомпилировать и выполнить фрагмент кода C# во время выполнения с помощью компилятора Roslyn. Метод Roslyn CSharpSyntaxTree.ParseText используется для анализа исходного кода в синтаксическое дерево, которое затем добавляется в новый объект CSharpCompilation. К объекту компиляции также добавляются необходимые ссылки на сборки: библиотеку CoreLib, System.Runtime и System.Console для работы класса Console. Заметьте, что в последних версиях .NET они находятся в разных файлах.

Метод Emit компилирует код в динамически подключаемую библиотеку (DLL) и записывает выходные данные в MemoryStream. Если компиляция прошла успешно, полученная сборка загружается в текущий домен приложения с помощью метода Assembly.Load. Затем класс Runner и его метод Run получаются через рефлексию, и этот метод вызывается, выполняя динамический код.

Этот метод позволяет создавать гибкие и расширяемые приложения, которые могут динамически компилировать и выполнять код C# во время выполнения. Однако будьте осторожны с последствиями для безопасности, поскольку выполнение произвольного кода может создать угрозу безопасности, если с ним не обращаться должным образом.

Продолжение следует…

Источник:
https://maherz.medium.com/10-mind-blowing-c-hacks-95fa629cfcef
День 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
День 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
День 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/
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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
День 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/