.NET Разработчик
6.52K subscribers
441 photos
3 videos
14 files
2.12K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 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
👍9
День 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
👍23
День 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
👍16
День 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
👍12
День 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
👍9
День 2378. #TipsAndTricks
Используем COPY для Экспорта/Импорта Данных в Potgresql

В PostgreSQL есть функция, позволяющая эффективно выполнять массовый импорт и экспорт данных в таблицу и из неё. Обычно это гораздо более быстрый способ загрузки данных в таблицу и извлечения из неё, чем использование команд INSERT и SELECT.

В .NET провайдер Npgsql поддерживает три режима операции COPY: бинарный, текстовый и бинарный необработанный.

1. Бинарный
Пользователь использует API для чтения и записи строк и полей, которые Npgsql кодирует и декодирует. После завершения необходимо вызвать функцию Complete() для сохранения данных; в противном случае операция COPY будет откачена при освобождении объекта записи (это поведение важно в случае возникновения исключения).
// Импорт в таблицу с 2 полями (string, int)
using (var writer = conn.BeginBinaryImport(
"COPY my_table (field1, field2) FROM STDIN (FORMAT BINARY)"))
{
writer.WriteRow("Row1", 123);
writer.WriteRow("Row2", 123);

writer.Complete();
}

// Экспорт из таблицы с 2 полями
using (var rdr = conn.BeginBinaryExport(
"COPY my_table (field1, field2) TO STDOUT (FORMAT BINARY)"))
{
rdr.StartRow();
Console.WriteLine(rdr.Read<string>());
Console.WriteLine(rdr.Read<int>(NpgsqlDbType.Smallint));

rdr.StartRow();
// пропускает поле
rdr.Skip();
// проверяет на NULL (без перехода на следующее поле)
Console.WriteLine(rdr.IsNull);
Console.WriteLine(rdr.Read<int>());

rdr.StartRow();
// StartRow() вернёт -1 в конце данных
}


2. Текстовый
В этом режиме данные в БД и из неё передаются в текстовом или CSV-формате PostgreSQL. Пользователь должен самостоятельно отформатировать текст или CSV-файл, Npgsql предоставляет только функции чтения или записи текста. Этот режим менее эффективен, чем бинарный, и подходит, если у вас уже есть данные в CSV, а производительность не критична.
using (var writer = conn.BeginTextImport(
"COPY my_table (field1, field2) FROM STDIN"))
{
writer.Write("HELLO\t1\n");
writer.Write("GOODBYE\t2\n");
}

using (var reader = conn.BeginTextExport(
"COPY my_table (field1, field2) TO STDOUT"))
{
Console.WriteLine(reader.ReadLine());
Console.WriteLine(reader.ReadLine());
}


3. Бинарный необработанный
Данные передаются в двоичном формате, но Npgsql не выполняет никакого кодирования или декодирования — данные предоставляются как необработанный поток .NET. Имеет смысл только для обработки больших объемов данных и восстановления таблицы: таблица сохраняется как BLOB-объект, который впоследствии можно восстановить. Если нужно разбирать данные, используйте обычный бинарный режим.
int len;
var data = new byte[10000];
// Экспорт table1 в массив данных
using (var inStream = conn.BeginRawBinaryCopy(
"COPY table1 TO STDOUT (FORMAT BINARY)"))
{
// Предполагаем, что данные влезут в 10000 байт
// В реальности их нужно читать блоками
len = inStream.Read(data, 0, data.Length);
}

// Импорт данных в table2
using (var outStream = conn.BeginRawBinaryCopy(
"COPY table2 FROM STDIN (FORMAT BINARY)"))
{
outStream.Write(data, 0, len);
}


Отмена
Операции импорта можно отменить в любой момент, освободив (dispose) NpgsqlBinaryImporter до вызова метода Complete(). Операции экспорта можно отменить, вызвав метод Cancel().

Источник: https://www.npgsql.org/doc/copy.html
👍16
День 2384. #TipsAndTricks
Как Продолжить Выполнение Процесса После Завершения Задания GitHub Action

После завершения задания GitHub Actions обработчик завершает все запущенные им дочерние процессы. Он идентифицирует эти процессы, проверяя переменную окружения RUNNER_TRACKING_ID. Любой процесс, где эта переменная установлена переменной считается дочерним процессом обработчика и будет остановлен.

Чтобы процесс продолжил работу после завершения задания, запустите его без переменной окружения RUNNER_TRACKING_ID:
var psi = new ProcessStartInfo("sample_app");
psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID");
Process.Start(psi);


Либо:
# Вариант 1
$env:RUNNER_TRACKING_ID = $null

# Вариант 2
$psi = New-Object System.Diagnostics.ProcessStartInfo "sample_app"
$psi.EnvironmentVariables.Remove("RUNNER_TRACKING_ID")
[System.Diagnostics.Process]::Start($psi)


Примечание: В обработчиках, размещенных на GitHub, это решение не поможет, поскольку вся виртуальная машина удаляется после завершения задания.

Источник: https://www.meziantou.net/how-to-keep-processes-running-after-a-github-action-job-ends.htm
👍2
День 2385. #TipsAndTricks
5 Современных Возможностей C#, Которые Улучшат Ваш Код

C# стал гораздо более выразительным и мощным языком, чем был ещё несколько лет назад. Вот 5 новинок языка, которые отличают инженеров, просто «знающих C#», от тех, кто использует его на полную.

1. Структуры только для чтения для критически важных для производительности типов-значений
Обычная структура копируется без необходимости, что приводит к скрытым потерям производительности. Отметив её как только для чтения, вы сообщаете компилятору, что её поля никогда не изменятся, что позволяет среде выполнения не создавать защитные копии.
Раньше:
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}

Сейчас:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}

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

2. Ключевое слово scoped для предотвращения использования ref-переменных вне области видимости
C# позволяет использовать ref-переменные для повышения производительности, но распространённой ошибкой является случайное их использование вне области видимости (возвращение ссылки на что-то, что больше не является безопасным). scoped гарантирует во время компиляции, что ссылка не проживёт дольше положенного.
Без scoped:
ref int Dangerous(ref int number)
{
// Может быть использовано извне и привести к проблемам
return ref number;
}

scoped:
ref int Safe(scoped ref int number)
{
// Компилятор проверяет, что number может
// использоваться только внутри метода
// и выдаёт ошибку компиляции
return ref number;
}


3. Обязательные свойства для принудительной инициализации объекта
Сколько раз вы создавали объект и забывали установить одно из его критически важных свойств? При использовании обязательных свойств компилятор принуждает инициализировать объект.
Раньше:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
}
// Можно забыть инициализировать свойства
var user = new User { Name = "John" };

Сейчас:
public class User
{
public required string Name { get; init; }
public required string Email { get; init; }
}
// Компилятор проверяет полноту инициализации
var user = new User {
Name = "John", Email = "john@mail.com" };


4. Возврат типа ref readonly, чтобы избежать защитного копирования
При возврате больших структур возврат по значению часто приводит к ненужному копированию. Возврат типа ref readonly даёт вызывающим функциям ссылку на объект, который они могут читать, но не могут изменять.
Без ref readonly:
public BigStruct GetData() => 
_bigStruct; // Копирует структуру

С ref readonly:
public ref readonly BigStruct GetData() => 
ref _bigStruct; // Нет копирования

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

5. Статические абстрактные члены в интерфейсах для обобщённой математики
Это кажется узкоспециализированной функцией, но открывает доступ к настоящей обобщённой математике без рефлексии и упаковки. До появления этой функции нельзя было писать обобщённые алгоритмы, работающие с числовыми типами:
T Add<T>(T a, T b) where T : INumber<T> 
=> a + b;

Благодаря статическим абстрактным членам, INumber<T> определяет операторы и константы обобщённо, обеспечивая чистые, независимые от типов математические библиотеки.

Итого
Современный C# — это не просто новый синтаксис; это достижение ясности, безопасности и производительности, которых просто не могли предложить старые версии. Начните внедрять эти функции в свой код, и вы не только начнёте писать лучшее ПО, но и по-новому взглянете на возможности языка.

Источник: https://blog.stackademic.com/5-modern-c-features-that-will-make-your-code-feel-like-magic-d1ef6a374d13
👍30
День 2390. #TipsAndTricks
Как не Возвращать Все Свойства в SqlRaw
В Entity Framework SqlRaw есть небольшое, иногда раздражающее ограничение: SQL-запрос должен возвращать данные для всех свойств типа сущности.
Иногда этого не нужно, поэтому давайте посмотрим, как это очень просто обойти.

Представьте, что у нас есть такой объект:
public sealed record MyEntity
{
public double? PropA { get; init; }
public double? PropB { get; init; }
}

По сути, мы указываем, что ни PropA, ни PropB не являются обязательными и, следовательно, если они отсутствуют в результирующем наборе, должны быть равны NULL. Но SqlRaw ожидает, что все свойства сущности присутствуют в предложении SELECT.

То есть:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA FROM MyTable")
.ToListAsync(token);

Завершится с ошибкой, что EF не может сопоставить запрос с типом, поскольку отсутствует PropB.

Чтобы обойти эту проблему, просто явно возвращайте PropB как NULL:
dbContext
.Database
.SqlQuery<MyEntity>(
"SELECT PropA, NULL AS PropB FROM MyTable")
.ToListAsync(token);


Вот и всё, EF снова счастлив. Конечно, это упрощённый пример, но суть, надеюсь, вы поняли.

Источник: https://steven-giesel.com/blogPost/c6bea409-9e49-4915-8529-8a8a8574ba80/how-to-not-return-all-properties-in-sqlraw
👍22
День 2403. #TipsAndTricks
Git Worktree: Управление Несколькими Рабочими Каталогами

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

Git Worktree создаёт дополнительные рабочие папки (деревья), связанные с тем же репозиторием. Каждое рабочее дерево может иметь извлекаемую ветку, что позволяет работать над несколькими функциями, исправлением ошибок или экспериментами без необходимости многократного клонирования всего репозитория. Главное преимущество заключается в том, что у вас есть только один индекс Git (каталог .git), что снижает использование дискового пространства и повышает производительность.

В процессе написания кода переключение контекстов может мешать работе. Рабочее дерево Git помогает:
- Сохранять контекст. Сохраняйте текущую работу без изменений, быстро переключаясь между исправлением ошибки или проверкой кода.
- Работать параллельно. Работайте над несколькими функциями одновременно без накладных расходов на переключение веток.
- Использовать ИИ-агента. Вы можете поручить ИИ-агенту работать над отдельной веткой, пока вы продолжаете выполнять текущие задачи.
- Снижать когнитивную нагрузку. Не нужно откладывать или фиксировать незавершенную работу при переключении задач.
- Сравнивать файлы. Легко сравнивайте изменения между ветками без необходимости переключаться между ними. Вы можете открыть несколько версий одного и того же файла в разных рабочих деревьях, что упрощает отслеживание того, как изменения влияют на ваш код.

Чтобы создать новое рабочее дерево:
# Рабочее дерево для существующей ветки
git worktree add ../feature-branch feature-branch

# Рабочее дерево для новой ветки из main
git worktree add -b new-feature ../new-feature origin/main

Чтобы удалить рабочее дерево, можно использовать следующие команды:
# Удалить рабочее дерево
git worktree remove ../feature-branch

# Удалить директорию и очистить метаданные
rm -rf ../feature-branch
git worktree prune


Также можно получить список рабочих деревьев:
git worktree list


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

Итого
Рабочее дерево Git — это малоиспользуемая функция, которая может значительно улучшить процесс разработки, особенно когда вам нужно одновременно поддерживать несколько контекстов.

Источник: https://www.meziantou.net/git-worktree-managing-multiple-working-directories.htm
👍16
День 2410. #TipsAndTricks
Улучшаем Отладку EF Core с Помощью Тегов Запросов
Отладка запросов к БД в EF Core иногда напоминает поиск иголки в стоге сена. Когда ваше приложение генерирует десятки или сотни SQL-запросов, определить, какое LINQ-выражение сгенерировало тот или иной SQL-запрос, становится настоящей проблемой. К счастью, есть элегантное решение: теги запросов.

Теги запросов
Теги запросов позволяют добавлять пользовательские комментарии к SQL-запросам, сгенерированным LINQ-выражениями. Эти комментарии отображаются непосредственно в сгенерированном SQL-коде, что позволяет легко сопоставлять SQL-запрос с кодом, который его создал. Чтобы использовать эту функцию, необходимо применить метод TagWith к любому объекту IQueryable и передать описательный комментарий:
var оrders = context.Orders
.TagWith("Заказы больше $1000")
.Where(o => o.Total > 1000)
.Include(o => o.Customer)
.ToList();


Это сгенерирует примерно такой SQL-запрос:
-- Заказы больше $1000
SELECT [o].[Id], [o].[CustomerId], [o].[Total], [c].[Id], [c].[Name]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[Total] > 1000.0

Вместо того, чтобы пытаться определить, какой код сгенерировал тот или иной SQL-запрос, вы можете сразу увидеть цель и источник каждого запроса в логах БД или профилировщике.

Вы можете объединить несколько вызовов TagWith для добавления дополнительного контекста, а также добавлять значения переменных во время выполнения:
var userOrders = context.Orders
.TagWith("Запрос с дэшборда")
.TagWith($"User ID: {userId}")
.TagWith($"CorrelationId: {correlationId}")
.Where(o => o.CustomerId == userId)
.OrderByDescending(o => o.OrderDate)
.Take(10)
.ToList();

Результат:
-- Запрос с дэшборда
-- User ID: 12345
-- CorrelationId: 987654321
SELECT TOP(10) [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Orders] AS [o]
WHERE [o].[CustomerId] = 12345
ORDER BY [o].[OrderDate] DESC

*CorrelationId помогает проследить весь путь пользовательского запроса (см. подробнее).

Примечания:
1. Хотя влияние TagWith на производительность минимально, избегайте чрезмерно длинных тегов или сложной интерполяции строк в горячих путях.
2. Теги запросов должны быть строковыми литералами, и не могут принимать параметры. Однако можно использовать интерполяцию строк (как видно выше), а также многострочные строковые литералы.

Источник: https://dev.to/shayy/postgres-is-too-good-and-why-thats-actually-a-problem-4imc
1👍34
День 2426. #TipsAndTricks
Перемещение Файлов и Папок в Корзину в .NET
При работе с файлами и папками в приложениях .NET в Windows может потребоваться перемещать элементы в корзину вместо их безвозвратного удаления (File.Delete, Directory.Delete). Это позволит пользователям восстановить случайно удалённые элементы. Вот как это можно сделать с помощью API Windows Shell:
static void MoveToRecycleBin(string path)
{
if (!OperatingSystem.IsWindows()) return;

var shellType = Type.GetTypeFromProgID(
"Shell.Application", throwOnError: true)!;
dynamic shellApp =
Activator.CreateInstance(shellType)!;

// https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants?WT.mc_id=DT-MVP-5003978
var recycleBin = shellApp.Namespace(0xa);

// https://learn.microsoft.com/en-us/windows/win32/shell/folder-movehere?WT.mc_id=DT-MVP-5003978
recycleBin.MoveHere(path);
}


Теперь можно использовать этот метод для перемещения файлов и папок в Корзину:
MoveToRecycleBin(@"C:\path\to\file.txt");
MoveToRecycleBin(@"C:\path\to\directory");


Источник: https://www.meziantou.net/moving-files-and-folders-to-recycle-bin-in-dotnet.htm
👍26
День 2444. #TipsAndTricks
Сеньорские Приёмы в C#

Некоторые функции C# настолько фундаментальны для чистой, современной разработки .NET, что их игнорирование выдаёт неопытность. Это не яркие новые игрушки, а практичные инструменты, которые мгновенно отличают код начинающего разработчика от отточенного кода опытного.

1. Раннее выявление ошибок внедрения зависимостей
Одна из самых серьёзных ошибок в приложениях .NET — это когда сервис успешно разрешается при запуске, но позже падает из-за недопустимых значений времени жизни или отсутствующих зависимостей. Этого можно избежать.
Код начинающего:
builder.Services.AddScoped<IMyService, MyService>();
// Нет настройки валидации

Вы не узнаете, что что-то неправильно настроено до того, как это не отвалится при обработке запроса.
Код опытного:
var builder = Program.CreateHostBuilder(args);
builder.Services.AddScoped<IMyService, MyService>();
builder.Host.UseDefaultServiceProvider(
(context, options) =>
{
options.ValidateOnBuild = true;
options.ValidateScopes = true;
});

Эти две строки гарантируют обнаружение захвата зависимостей при запуске приложения, а не при его работе. ValidateScopes обнаруживает недопустимые времена жизни (например, внедрение scoped-сервиса в синглтон), а ValidateOnBuild заставляет контейнер попытаться создать сервисы при построении контейнера. Если что-то сломается, вы узнаете об этом ещё до запуска приложения.

2. Понимание и создание областей действия сервисов
Многие разработчики помещают все сервисы в Transient, не понимая, что это значит. Время жизни — это не просто шаблонный код; это модель памяти вашего приложения.
Код начинающего:
builder.Services.AddTransient<UserSession>();

Ни контекста, ни объяснения, ни обдумывания, когда объект должен жить.
Ход мыслей опытного:
- Transient – новый экземпляр каждый раз,
- Scoped – один экземпляр на запрос/скоуп DI,
- Singleton – один экземпляр на всё время жизни приложения.
Создавая свою область действия, вы можете вручную изолировать контексты:
using var scope = serviceProvider.CreateScope();
var scopedService = scope
.ServiceProvider
.GetRequiredService<IScopedThing>();

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

3. Использование DateTimeOffset
Часовые пояса, летнее время и работа с UTC — вот где начинающие разработчики чаще всего терпят неудачу. Хуже всего то, что ваше приложение может работать… но потом перестаёт.
Ошибка начинающего:
var orderTime = DateTime.Now; // локальное время

Это сломается, как только код попадёт в другой часовой пояс (например, при размещении в облаке).
Подход опытного:
var orderTime = DateTimeOffset.Now;

DateTimeOffset включает в себя как временную метку, так и её смещение. Это идеально подходит для реальных сценариев, таких как журналы, транзакции и планирование. Храните в формате UTC, отображайте по местному времени.

4. Использование алиасов пространств имён и типов
Загромождённые пространства имён и глубоко вложенные типы затрудняют чтение и рефакторинг кода. Тем не менее, начинающие разработчики редко используют алиасы:
System.Collections.Generic.Dictionary<System.Tuple<string, int>, List<MyNamespace.Models.ComplexThing>> myMap;

Более чистый подход:
using ComplexMap = System.Collections.Generic.Dictionary<(string, int), List<ComplexThing>>;

Алиасы — это не просто про сокращение имён; они проясняют суть. Они особенно полезны при рефакторинге или замене внешних библиотек, поскольку вы меняете всего одну строку — алиас.

Источник: https://blog.stackademic.com/5-c-features-that-instantly-expose-junior-devs-b126030c132e
👍25👎5
День 2449. #TipsAndTricks #Blazor
Лучшие Практики по Созданию Веб-Приложений в Blazor. Начало

Рассмотрим 9 рекомендаций по созданию веб-приложений Blazor.

1. Понимание жизненного цикла компонента
Первый и самый важный шаг при изучении Blazor — это правильное понимание жизненного цикла компонента. Blazor использует компонентно-ориентированную систему рендеринга, похожую на другие современные фреймворки веб-приложений, такие как Angular или React. См. подробнее о создании Blazor-компонентов.
Помимо изучения реализации компонентов Blazor, важно понимать, когда компонент Blazor автоматически перерисовывается и как управлять этим поведением. Например, мы можем переопределить метод жизненного цикла ShouldRender для управления обновлением UI. Если метод возвращает true, компонент перерисовывается.

2. Выбор правильного размера компонента
Одна из самых сложных задач - решение о том, когда следует разбить код на несколько компонентов. Нужно начать с простого. Сначала создаём маршрутизируемый компонент страницы и пишем весь код в нём. В какой-то момент код разрастается, и приходит понимание, что часть кода не связана с другой частью. Например, обработка формы никак не связана с отображением таблицы. Это сигнал, чтобы разделить компоненты, извлечь дочерние компоненты и превратить компонент страницы в оркестратор нескольких дочерних компонентов.
Не существует правильного или неправильного подхода, и требуется опыт, чтобы понять, что работает лучше всего. Одно из полезных правил - выделять то, что связано друг с другом, и ценить связность больше, чем размер (количество строк кода).

3. Реализация независимого режима рендеринга для Blazor-компонентов
С появлением интерактивного режима рендеринга в .NET 8 мы получаем гораздо больше гибкости по сравнению с предыдущими версиями Blazor. Например, мы можем реализовать веб-приложение, полностью отрисовываемое на сервере, без какой-либо интерактивности. Или можем комбинировать интерактивность Blazor Server и Blazor WebAssembly в одном приложении.
Для обеспечения гибкой архитектуры рекомендуется настроить компоненты Blazor так, чтобы они не зависели от режима рендеринга. Т.е. не задавать тип интерактивности внутри каждого компонента, а задавать его только на верхнем уровне. Это позволяет использовать компонент как часть интерактивного приложения Blazor Server и Blazor WebAssembly.

4. Изучите правильную обработку событий
Узнайте, как привязывать методы C# к событиям, вызываемым HTML-элементами. Это фундаментальный механизм для реализации обработчиков onClick для кнопок или обработчиков отправки для HTML-форм. При регистрации событий .NET, таких как событие LocationChanged класса NavigationManager, обязательно отписывайтесь от события при удалении компонента. В противном случае компонент не будет уничтожен сборщиком мусора.
@implements IDisposable
@inject NavigationManager NavigationManager

protected override void OnInitialized()
{
NavigationManager.LocationChanged
+= LocationChanged;
}

void LocationChanged(
object sender,
LocationChangedEventArgs e)
{
System.WriteLine("Location changed");
}

void IDisposable.Dispose()
{
NavigationManager.LocationChanged
-= LocationChanged;
}

Что касается связи между компонентами, обратные вызовы событий (EventCallback) являются стандартным способом осуществления обратного вызова родительского компонента из дочернего компонента.

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

Источник:
https://www.telerik.com/blogs/blazor-basics-9-best-practices-building-blazor-web-applications
👍7
День 2450. #TipsAndTricks #Blazor
Лучшие Практики по Созданию Веб-Приложений в Blazor. Окончание

Начало

5. Выберите подходящий метод управления состоянием
Blazor предлагает различные варианты управления состоянием. Параметры компонентов — самый простой вариант, за которым следуют каскадные значения и извлечение состояния в специализированные реализации сервисов.
Для больших приложений может подойти библиотека управления состоянием, например, Fluxor, или другой контейнер глобального состояния. Однако имейте в виду, что обработка глобального состояния может привести к сложностям в приложении.

6. Правильная организация и структура кода
Используйте чёткую, понятную и организованную структуру кода. Например, группируйте связанные компоненты по папкам, а сервисы и страницы — в логические папки.
Также следуйте рекомендациям по именованию компонентов, таким как соглашения об именовании, и разделяйте задачи путём извлечения компонентов, чтобы повысить удобство поддержки всего приложения.
Новый шаблон веб-приложения Blazor в .NET 8 - хорошая отправная точка. Однако обязательно реорганизуйте код, когда приложение значительно разрастётся в той или иной области, чтобы не приходилось постоянно искать связанные части кода.

7. Защитите приложение
Ознакомьтесь с лучшими практиками веб-безопасности, такими как OSWASP Top 10, и примите меры, особенно при работе с конфиденциальными данными.
Используйте аутентификацию и авторизацию ASP.NET Core для защиты доступа к конечным точкам и страницам Blazor. Храните только ту информацию, которая необходима для выполнения ваших бизнес-задач. Всегда используйте HTTPS.
Не храните пароли пользователей самостоятельно. Используйте провайдер аутентификации. Если нет другого варианта и приходится хранить учётные записи пользователей самостоятельно, убедитесь, что пароли правильно хэшируются с помощью надежного алгоритма хэширования, например, BCrypt.

8. Используйте правильную обработку ошибок и ведение журнала
Реализуйте надёжное решение для обработки ошибок и исключений. Убедитесь, что логи содержат важную информацию для решения проблем в коде. В то же время избегайте регистрации конфиденциальной информации и заменяйте ее плейсхолдерами.
Вы можете использовать фреймворк логирования ASP.NET Core или добавить более гибкое и эффективное решение, например, Serilog.

9. Максимально простое решение
Один из самых недооценённых советов как в разработке ПО в целом, так и в разработке на Blazor — это простота. Существует множество сложных реализаций, которые можно заменить простыми решениями. Всегда стремитесь реализовать максимально простое решение любой задачи.
Например, когда нужно передать значение компоненту, начните с использования параметра компонента. Зачем реализовывать сложный сервис и внедрять его в дочерний компонент, если можно решить проблему с помощью простого параметра компонента?

Источник: https://www.telerik.com/blogs/blazor-basics-9-best-practices-building-blazor-web-applications
2👍5
День 2456. #TipsAndTricks
6 Шагов для Правильной Настройки Нового .NET-проекта. Начало

Начинать новый .NET-проект всегда волнительно. Но легко пропустить подготовительную работу, которая делает проект масштабируемым и поддерживаемым. Вот несколько ключевых шагов, которые значительно облегчат вам (и вашим коллегам) жизнь в дальнейшем.

1. Единый стиль кода
Файл .editorconfig гарантирует, что все участники команды будут использовать одинаковые соглашения по форматированию и именованию, что позволяет избежать несоответствий в отступах и случайных правил именования.
Можно создать его прямо в Visual Studio. Щелкните правой кнопкой на решении Add -> New Editorconfig (Добавить -> Новый Editorconfig). Конфигурация по умолчанию — отличное начало. Но вы можете настроить её дополнительно в соответствии с предпочтениями команды. Разместите файл в корне решения, чтобы все проекты следовали одним и тем же правилам. При необходимости можно переопределить определённые настройки во вложенных папках, поместив туда свой файл .editorconfig. Вот пара примеров:
- из репозитория среды исполнения .NET
- от Милана общий для проектов .NET

2. Централизованная конфигурации сборки
Файл Directory.Build.props позволяет определить параметры сборки, применяемые к каждому проекту в решении:
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

Это позволяет сохранить чистоту и единообразие ваших файлов .csproj, поскольку нет необходимости повторять эти свойства в каждом проекте. Если позже вы захотите включить статические анализаторы или настроить параметры сборки, вы можете сделать это один раз здесь. Преимущество в том, что файлы .csproj становятся практически пустыми, большую часть времени содержащими только ссылки на NuGet-пакеты.

3. Централизованное управление пакетами
По мере роста решения управление версиями NuGet-пакетов в нескольких проектах становится проблематичным. Именно здесь на помощь приходит централизованное управление пакетами. Создайте файл с именем Directory.Packages.props в корне:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.15.0.120848" />
</ItemGroup>
</Project>

Теперь, когда нужно сослаться на NuGet-пакет в проекте, не нужно указывать версию. Можно использовать только имя пакета:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />

Всё управление версиями осуществляется централизованно. Это упрощает обновление зависимостей и позволяет избежать дрейфа версий между проектами. При необходимости вы по-прежнему можете переопределять версии в отдельных проектах.

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

Источник:
https://www.milanjovanovic.tech/blog/6-steps-for-setting-up-a-new-dotnet-project-the-right-way
1👍36
День 2457. #TipsAndTricks
6 Шагов для Правильной Настройки Нового .NET-проекта. Окончание

Начало

4. Статический анализ кода
Помогает выявлять потенциальные ошибки и поддерживать качество кода. В .NET есть набор встроенных анализаторов, но есть отличный NuGet-пакет SonarAnalyzer.CSharp для более полной проверки.
Install-Package SonarAnalyzer.CSharp

Также его можно добавить как глобальную ссылку в Directory.Build.props:
<ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" />
</ItemGroup>

Это в сочетании с такими настройками:
<Project>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>

…и ваши сборки будут завершаться неудачей при серьёзных недостатках качества кода. Это может быть отличной подстраховкой. Но поначалу это может мешать. Если некоторые правила не соответствуют вашему контексту, вы можете изменить или отключить их в файле .editorconfig, установив для важности правила значение none.

5. Настройка локальной оркестровки
Для обеспечения согласованности локальной среды в команде вам понадобится оркестровка контейнеров. Есть два основных варианта.

1) Docker Compose
Добавьте поддержку Docker Compose в Visual Studio. Будет добавлен файл docker-compose.yml, в котором вы можете определить сервисы:
services:
webapi:
build: .
postgres:
image: postgres:18
environment:
POSTGRES_PASSWORD: password

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

2) .NET Aspire
.NET Aspire выводит оркестровку на новый уровень. Он обеспечивает обнаружение сервисов, телеметрию и оптимизированную настройку, интегрированные с вашими проектами .NET. Вы можете добавить проект .NET и ресурс Postgres всего несколькими строками кода:
var postgres = builder.AddPostgres("demo-db");

builder.AddProject<WebApi>("webapi")
.WithReference(postgres)
.WaitFor(postgres);

builder.Build().Run();

Aspire также использует Docker, но предоставляет более широкие возможности для разработки.
Не важно, Docker Compose или Aspire, цель одна: воспроизводимая, надёжная локальная конфигурация, которая работает одинаково на всех машинах.

6. Автоматизация сборки
Простой рабочий процесс GitHub Actions для проверки каждого коммита .github/workflows/build.yml:
name: Build

on:
push:
# Выполнение только на ветке main
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- run: dotnet restore
- run: dotnet build --no-restore --configuration Release
- run: dotnet test --no-build --configuration Release

Это гарантирует, что проект всегда будет собираться и проходить тесты, а проблемы будут выявляться до того, как они попадут в продакшн. Если сборка непрерывной интеграции (CI) завершится неудачей, вы сразу поймете, что что-то не так.

Что касается тестирования, изучите:
- Тестирование архитектуры,
- Интеграционное тестирование с Testcontainers.
Это даст уверенность в том, что код работает как ожидалось в среде, максимально приближенной к производственной.

Источник: https://www.milanjovanovic.tech/blog/6-steps-for-setting-up-a-new-dotnet-project-the-right-way
👍17