C# Portal | Программирование – Telegram
C# Portal | Программирование
14.9K subscribers
965 photos
117 videos
24 files
807 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для C#-разработчика

Связь: @devmangx

РКН: https://clck.ru/3FocB6
Download Telegram
Теперь вам не нужно использовать zig, чтобы получить comptime, теперь вы можете сделать это в C#! 😁

https://github.com/sebastienros/comptime

comptime — экспериментальный проект для C#, который позволяет выполнять код на этапе компиляции.

Реализован как генератор источников: методы, помеченные атрибутом [Comptime], исполняются во время сборки, а их результат встраивается в сгенерированный C#-код. В итоге тяжёлые вычисления уезжают из runtime в build time.

Работает на .NET 8 и C# 12, с ограничениями: методы static, классы partial, аргументы — компиляторные константы.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
8🔥3
DbContext не потокобезопасен. Как правильно распараллеливать запросы в EF Core.

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

Обычно код выглядит так:

var orders = await GetRecentOrdersAsync(id);
var logs = await GetSystemLogsAsync();
var stats = await GetUserStatsAsync(id);

return new DashboardDto(orders, logs, stats);


Код чистый, читаемый, всё работает. Но есть нюанс. Если GetRecentOrdersAsync выполняется 300 мс, GetSystemLogsAsync — 400 мс, а GetUserStatsAsync — 300 мс, то общее время загрузки будет около секунды (300 + 400 + 300).

В распределённых системах лишняя задержка напрямую бьёт по пользовательскому опыту. Эти наборы данных никак не зависят друг от друга, значит, логично выполнять их параллельно. Тогда итоговое время будет равно самому медленному запросу — 400 мс. Это примерно 60% прироста производительности просто за счёт способа выполнения кода. Но с EF Core наивный подход не сработает.

» Проблема с Task.WhenAll

Самая частая ошибка — обернуть вызовы репозитория в задачи и ждать их одновременно:

// ТАК ДЕЛАТЬ НЕЛЬЗЯ
public async Task<DashboardData> GetDashboardData(int id)
{
// Все методы используют один и тот же _dbContext
var orders = _repo.GetOrdersAsync(id);
var logs = _repo.GetLogsAsync();
var stats = _repo.GetStatsAsync(id);

await Task.WhenAll(orders, logs, stats);

return new DashboardData(
orders.Result,
logs.Result,
stats.Result);
}


При выполнении вы получите исключение:

A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext, however instance members are not guaranteed to be thread safe.


(«Вторая операция в этом контексте началась до завершения предыдущей. Обычно это происходит из-за использования одного экземпляра DbContext из разных потоков, при том что его члены не являются потокобезопасными.»)

Почему так происходит ??

DbContext в EF Core не потокобезопасен. Это stateful-объект, рассчитанный на одну единицу работы. Он:

1. ведёт change tracking для загруженных сущностей
2. инкапсулирует одно соединение с базой данных

Протоколы БД (будь то TCP-соединение с PostgreSQL или SQL Server) по сути синхронны на уровне соединения. Нельзя одновременно отправить два разных SQL-запроса по одному и тому же каналу в один и тот же момент времени.

Когда вы используете Task.WhenAll, несколько потоков пытаются параллельно использовать одно и то же соединение. EF Core это отслеживает и намеренно выбрасывает исключение, чтобы не допустить некорректного состояния и повреждения данных.

В итоге получается дилемма » параллелизм нужен для скорости, но DbContext заставляет нас выполнять всё последовательно.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🤨74👍3
C# Portal | Программирование
DbContext не потокобезопасен. Как правильно распараллеливать запросы в EF Core. Все мы писали эндпоинты вроде панели мониторинга или пользовательской сводки. Это такие точки, где нужно собрать несколько вообще не связанных между собой наборов данных, чтобы…
Как правильно распараллеливать запросы в EF Core.

Начиная с .NET 5, в EF Core есть нормальное решение для этого сценария — IDbContextFactory<T>.

Вместо того чтобы внедрять DbContext со scoped-временем жизни (который живёт весь HTTP-запрос), вы внедряете фабрику. Она позволяет создавать лёгкие, независимые экземпляры DbContext ровно там, где они нужны.

Примечание: фабрика самый чистый вариант с точки зрения DI, но при желании можно и вручную создавать контекст (new AppDbContext(options)), если у вас есть доступ к DbContextOptions.

Сначала регистрируем фабрику в Program.cs:

// Регистрирует фабрику как Singleton
// Также регистрирует AppDbContext как Scoped
builder.Services.AddDbContextFactory<AppDbContext>(
opts =>
{
opts.UseNpgsql(
builder.Configuration.GetConnectionString("db"));
});


Теперь перепишем конечную точку, используя фабрику контекстов.
Внутри метода мы запускаем отдельную задачу на каждый запрос.
В каждой задаче создаётся свой DbContext, выполняется запрос и сразу же освобождается.

using Microsoft.EntityFrameworkCore;

public class DashboardService(
IDbContextFactory<AppDbContext> ctxFactory)
{
public async Task<DashboardDto> GetDashboardAsync(int id)
{
var orders = GetOrdersAsync(id);
var logs = GetSystemLogsAsync();
var stats = GetUserStatsAsync(id);

await Task.WhenAll(orders, logs, stats);

return new DashboardDto(
await orders,
await logs,
await stats
);
}

private async Task<List<Order>> GetOrdersAsync(int id)
{
// Создаём отдельный контекст для операции
await using var ctx =
await ctxFactory.CreateDbContextAsync();

return await ctx.Orders
.AsNoTracking()
.Where(o => o.UserId == id)
.ToListAsync();
}

// остальные методы аналогичны
}


Ключевые моменты

1. Изоляция

Каждая задача получает свой DbContext.
Соответственно, у каждой задачи своё соединение с БД.
Никаких конфликтов.

2. Освобождение ресурсов

await using здесь критически важен.
Как только запрос завершился, контекст сразу освобождается, а соединение возвращается в пул.

При последовательном выполнении каждая операция ждёт завершения предыдущей.
При параллельном подходе все три запроса к БД стартуют и завершаются одновременно, что уменьшает общее время ответа.

Компромиссы и выводы

IDbContextFactory хорошо закрывает разрыв между unit-of-work-архитектурой EF Core и реальными требованиями к параллельной обработке.
Он позволяет выйти за рамки принципа «один запрос — один поток» без нарушения безопасности.

Но использовать этот подход нужно с головой:

1. Истощение пула соединений

Один HTTP-запрос теперь может одновременно занять 3 соединения с БД вместо одного.
При высокой нагрузке пул соединений можно выбить довольно быстро.

2. Накладные расходы

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

Итог:

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

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍167😁1
Шокирующие бенчмарки производительности итераторов в C# / .NET.

Что лучше использовать в API для получения данных — итератор или материализованную коллекцию? В этом видео разбирают их реальные характеристики производительности, сравнивая итераторы в C# / .NET с обычными коллекциями на бенчмарках.

После этого видео стоит посмотреть продолжение:
https://youtube.com/watch?v=0s_VMhZSOwQ

Также можно идти параллельно по статье в блоге:
https://devleader.ca/2023/03/17/shocking-results-from-collection-and-iterator-benchmarks/

Исходники из видео здесь:
https://github.com/ncosentino/DevLeader/tree/master/AllAboutEnumerables/AllAboutEnumerables.BasicIteratorBenchmarks

Если хочется глубже разобраться в enumerable, итераторах и коллекциях, смотри плейлист:
https://youtube.com/watch?v=RR7Cq0iwNYo&list=PLzATctVhnsgjE3qOsbkPaC1NxXD605wOC

Само видео:
https://youtube.com/watch?v=G2-d7kZFlRA

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Давка кэша в C#: почему ConcurrentDictionary и MemoryCache не спасают

В статье разбирается проблема cache stampede и то, как она приводит к многократным тяжёлым запросам. Показаны бенчмарки для IO-bound и CPU-bound сценариев, есть сравнение с HybridCache и разбор подходов к защите кэша.

Показано, что GetOrAdd и GetOrCreateAsync не гарантируют единичный вызов тяжёлой операции. HybridCache умеет защищать только внутри одного процесса, но не между репликами. В конце — практические рекомендации, как выбирать решения и обходить проблему.

Предлагается добавлять случайный джиттер к TTL (в FusionCache это уже есть) и использовать паттерн Single Flight на уровне L2, чтобы обновление тяжёлых данных выполняла только одна реплика. Готовых библиотек под это автор не нашёл.

Читать подробнее: https://habr.com/ru/articles/977498/

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍1
ToArrayAsync или ToListAsync в Entity Framework?

Очевидно, каждый из этих методов стоит использовать в своей ситуации. Но если разницы нет, что выбрать? Короткий ответ: ToListAsync.

Посмотрим на реализацию. ToListAsync выглядит так:

public static async Task<List<TSource>>
ToListAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
{
var list = new List<TSource>();
await foreach (var element in
source
.AsAsyncEnumerable()
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
list.Add(element);
}

return list;
}


Источник

А вот как реализован ToArrayAsync:

public static async Task<TSource[]>
ToArrayAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
=> (
await source
.ToListAsync(cancellationToken)
.ConfigureAwait(false))
.ToArray();


Источник

Итого: ToArrayAsync сначала вызывает ToListAsync, а потом превращает полученный список в массив через ToArray(). В результате появляются лишние накладные расходы: создание списка, его наполнение, затем создание массива и копирование элементов.

Это не относится к ToHashSetAsync — он реализован по той же схеме, что и ToListAsync, без промежуточного списка.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144
Как включать фичи без редеплоя приложения?

Используй feature flags

Feature flags в dotnet позволяют на лету включать или выключать функциональность во время работы приложения. Так проще контролировать поведение без нового релиза.

Это удобно для постепенных раскаток, A/B тестов и безопасного выкатывания новых фич.

Ключевые идеи:

• Feature Flags: переключатели, которые определяют, включена фича или нет.
• Feature Management: встроенная поддержка в .NET 8 для управления флагами через API feature management.
• Gradual Rollout: можно открыть фичу части пользователей и только потом включить всем.
• Configuration Sources: флаги можно хранить в разных источниках, например appsettings.json, Azure App Configuration или во внешних сервисах.
• Feature Filters: условия и правила, когда фича должна включаться (роль пользователя, окружение, кастомная логика и т.д.).

Пример:

Есть новая фича, и ты хочешь проверить, как пользователи на неё отреагируют.
Feature flags позволяют показать её только определённому проценту аудитории, например 10%.

Допустим, за день на сайт заходит 1000 человек. Тогда 100 из них увидят новую фичу.
Так можно аккуратно посмотреть реакцию на изменения и не рисковать всем трафиком сразу.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍93
This media is not supported in your browser
VIEW IN TELEGRAM
Gemini Code Wiki

Бесплатный инструмент, который генерирует интерактивную документацию по любому GitHub-репозиторию 🤯

Что он умеет:
→ можно общаться с Gemini и разбираться в любом репо
→ визуализирует структуру кода
→ генерирует документацию из публичных репозиториев

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍64😁2
Используем выражения коллекций для пользовательских типов

В C# 12 появились выражения коллекций — новый, более лаконичный синтаксис инициализации коллекций:

int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];


Этот синтаксис отлично работает со встроенными коллекциями. Но что делать со своими типами коллекций? Тут на сцену выходит атрибут CollectionBuilderAttribute. Он позволяет распространить этот современный синтаксис на пользовательские типы.

Пользовательские коллекции

Представим, что у вас есть собственный тип коллекции:

public class MyCollection<T> 
{
private readonly List<T> _items;

public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];

public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();

// другие члены…
}


Раньше использовать выражения коллекций с таким типом было нельзя. Приходилось писать по-старому:

var myCol =
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });


Или так (что по сути не сильно лучше):

var myCol =
new MyCollection<int>([1, 2, 3, 4, 5]);


Выглядит не особо элегантно.

CollectionBuilderAttribute


CollectionBuilderAttribute закрывает этот пробел, подсказывая компилятору, как создавать вашу коллекцию из выражения коллекции:

[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
// …
}

public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}


После этого можно писать так:

MyCollection<int> myCol = [1, 2, 3, 4, 5];


Компилятор сам вызовет метод Create и передаст ему элементы.

Как это работает

Атрибут принимает два параметра:

1. Тип билдера — тип, в котором находится фабричный метод (typeof(MyCollectionBuilder)).

2. Имя метода — имя статического метода, создающего экземпляр ("Create").

Метод билдера должен:

- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типов, совпадающие с параметрами вашей коллекции.

Примечание: у коллекции также должен быть «итерационный контракт», то есть метод GetEnumerator(), возвращающий IEnumerator или IEnumerator<T>. Это можно сделать либо через реализацию IEnumerable / IEnumerable<T>, либо просто добавив метод GetEnumerator().

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

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍42🍌1
PostgreSQL позволяет клонировать базу на 6 ГБ за 212 миллисекунд вместо 67 секунд. Вот как это работает.

Клонирование баз данных полезно в нескольких сценариях:

* тестирование миграций без трогания продакшн-данных
* поднятие свежих копий под каждый прогон тестов
* сброс sandbox-окружения между сессиями
* воспроизводимые снапшоты для отладки

Когда база весит несколько мегабайт, pg_dump вполне справляется. Но когда речь идёт о сотнях гигабайт, «просто сделать копию» превращается в серьёзный узкий момент.

В PostgreSQL система шаблонов была всегда. Каждый CREATE DATABASE тихо клонирует template1 под капотом, и вместо template1 можно использовать любую базу.

В версии 15 появился параметр STRATEGY, и по умолчанию включили WAL_LOG — поблочное копирование через журнал предзаписи (WAL). I/O становится ровнее, но для больших баз это медленнее.

В PostgreSQL 18 появилась опция file_copy_method = clone. На современных файловых системах вроде XFS, ZFS или APFS она использует операцию FICLONE. Вместо копирования байтов файловая система создаёт новые метаданные, которые указывают на те же физические блоки. Обе базы используют одно и то же хранилище, пока вы ничего не меняете.

Всю магию здесь делает файловая система, создавая copy-on-write (CoW) клон файлов.

Когда вы обновляете строку, файловая система запускает copy-on-write только для затронутых страниц. Всё остальное остаётся общим. Клон на 6 ГБ изначально не занимает дополнительного места и растёт только по мере расхождения данных.

Важно помнить один момент: у исходной базы не должно быть активных подключений во время клонирования. Это ограничение PostgreSQL, а не файловой системы.

Довольно круто.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥7🎉3👍1🍌1
Пример кода с FormattableString в C#

- Создание из списка с цветом для вывода
- Достаём нужные свойства через extension-методы
- Фильтрация в простом варианте

Читать подробнее

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👎4👍3🍌1
Вы всё ещё вызываете new Random() по каждому поводу? Пора перестать.

В сети часто повторяют: new Random() — это плохо.
Мол, он берёт сид из системных часов, и если создать несколько экземпляров подряд, то значения будут одинаковыми. Типа такой код:

for (var i = 0; i < 5; i++)
{
var random = new Random();
Console.WriteLine(random.Next(1, 100));
}


выведет:

42 42 42 42 42


А вот этот вариант:

for (var i = 0; i < 5; i++)
Console.WriteLine(Random.Shared.Next(1, 100));


даст что-то вроде:

17 82 19 23 5


Откуда ноги растут?
В старом .NET Framework new Random() реально сидировался по DateTime.Now.Ticks, которые обновлялись примерно раз в 16 мс. Если создавать экземпляры чаще, сид совпадал, и результаты были одинаковыми. В .NET Core и дальше это уже не так. Можно прогнать код и убедиться.

А как насчёт потокобезопасности?

Наивный вариант, когда делают один общий генератор:

Random rng = new();
Parallel.For(0, 10, x =>
{
var value = rng.Next();
Console.WriteLine(value);
});


Выглядит норм, но потокобезопасным это не назовёшь. И проблемы могут всплыть не сразу.

Random при гонках в потоках часто возвращает 0, так что можно посмотреть масштаб проблемы:

Random rng = new();
Parallel.For(0, 10, x =>
{
var nums = new int[10_000];
for (int i = 0; i < nums.Length; ++i)
nums[i] = rng.Next();

Console.WriteLine($"{nums.Count(x => x == 0)} нулей");
});


В .NET 5 этот пример легко даёт 9000+ нулей, то есть почти всё упирается в проблемы потоков.
В .NET 8+ ситуация сильно лучше, но если задать сид вручную:

Random rng = new(123);


то проблемы возвращаются.

Что использовать?

Random.Shared создаёт свой экземпляр на поток (через [ThreadStatic]), поэтому для большинства кейсов на современных .NET это нормальный выбор. Если не нужен строго заданный сид — берите Random.Shared.
new Random() на новых версиях тоже обычно ок, но Shared безопаснее в многопоточке.

Про .NET Framework чуть позже.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
16
Проблема с Random в .NET Framework

У Random в .NET Framework по сути две основные болячки:

При new Random() в .NET Framework сид берется от системных часов.

У системных часов ограниченная точность (разрешение).

Из-за этого при быстром, плотном создании new Random() можно легко получить два (или больше) инстанса Random с одинаковым seed. А значит, они выдадут одну и ту же последовательность чисел:

// <TargetFramework>net48</TargetFramework>
Parallel.For(0, 10, x =>
{
Random rnd = new();
var value = rnd.Next();
Console.WriteLine(value);
});


В результате часть чисел (а иногда и вообще все) могут совпасть, что нам обычно вообще не нужно.

Решение

Идея простая: держим один общий Random, который используется только чтобы раздавать seed’ы для остальных Random. Раз сиды разные, то и последовательности у локальных Random не будут коррелировать, и проблема с одинаковыми значениями уйдет.

В .NET Core / .NET это как раз поведение “из коробки”, поэтому баг проявляется именно в .NET Framework.

Ниже пример обертки ThreadSafeRandom. Она использует [ThreadStatic] и аккуратно обходит проблему инициализации в .NET Framework:

internal static class ThreadSafeRandom
{
[ThreadStatic]
private static Random? _local;
private static readonly
Random Global = new Random();

private static Random Instance
{
get
{
if (_local is null)
{
int seed;
// избегаем конкурентного доступа
lock (Global)
{
seed = Global.Next();
}

_local = new Random(seed);
}
return _local;
}
}

public static int Next() => Instance.Next();
}


Эту потокобезопасную реализацию можно юзать на любых версиях фреймворка:

Parallel.For(0, 10, x =>
{
var value = ThreadSafeRandom.Next();
Console.WriteLine(value);
});


Если вы на .NET 6+, лучше использовать встроенный Random.Shared. Для более ранних версий подойдет ThreadSafeRandom выше. А если таргетитесь и в .NET 6+, и в .NET Framework, можно развести реализации через директивы #if под разные target framework.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍2
Оптимизация памяти в C# (и немного в Unity): эффективные методы и стратегии

Язык программирования C#, несмотря на то, что обеспечивает автоматическое управление памятью с помощью механизма сборки мусора (GC), требует от разработчиков специальных знаний и навыков для оптимизации работы с памятью

Итак, давайте рассмотрим различные стратегии и методы оптимизации памяти в C#, которые помогают создавать эффективные и быстрые приложения.

Читать статью...

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8👎7
Библиотеки, которые реально используют в Microsoft.

Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются до уровня Bing и Azure.

1. Microsoft.IO.RecyclableMemoryStream

Проблема:

Когда в API гоняется большой трафик или крутятся экспортные задания, обычный new MemoryStream() вызывает фрагментацию LOH (Large Object Heap) и частые остановки GC. В проде типа Bing это легко выливается в
OutOfMemoryException.

Решение:

RecyclableMemoryStream:

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

Где уместно:

ASP.NET API, которые возвращают PDF, изображения, Excel,

- фоновые задачи, работающие с файлами,
- бэкенды под gRPC и SignalR.

До:

using var ms = new MemoryStream();


После:

var mgr = new RecyclableMemoryStreamManager();
using var ms = mgr.GetStream();


Бенч:

Method           | Memory | Gen 0 | Gen 2
---------------- | -------| ----- | -----
MemoryStream | 85 KB | 1 | 1
RecyclableMemory | 2 KB | 0 | 0


2. LoggerMessage (генератор кода)

Проблема:
В сервисах с высоким трафиком даже такие безобидные логи:

_logger.LogInformation($"Обработка заказа {orderId}");


создают проблемы:

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

На масштабе Azure это превращается в задержки и прожорливость по памяти.

Решение:
LoggerMessage из .NET 8 генерирует методы на этапе компиляции:

- без интерполяции и упаковки типов,
- со структурированным форматированием через Span<T>,
- с IL без лишних аллокаций,

в 5–10 раз быстрее обычного ILogger.LogInformation(...).

Где уместно:

- нагруженные веб-API,
- воркфлоу/ивент-хендлеры, где логов много,
- телеметрия и критичные hot-path’ы.

До (аллокации):

_logger.LogInformation($"Обработка запроса {orderId}");


После (генерация):

[LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "Обработка запроса {OrderId}")]
partial void LogProcessingOrder(int orderId);

// вызов
LogProcessingOrder(orderId);


Можно вынести такие методы в статические хелперы и шарить между сервисами.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥226👍3👎1🤣1
C# Portal | Программирование
Библиотеки, которые реально используют в Microsoft. Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются…
Ещё стоит упомянуть:

3. Microsoft.SemanticKernel

Проблема: Обычных запросов и ответов уже не хватает для продакшн-приложений на ИИ. Нужны память между диалогами, разбиение целей на шаги и доступ к API вроде Microsoft 365 Copilot. Реализовать всё это вручную в .NET — тяжело и долго.

Решение:

SemanticKernel — официальный SDK от Microsoft для сборки похожих на агентов ИИ-процессов. Он даёт:

* Вызов инструментов и плагинов (C# или OpenAPI);
* Оркестрацию цепочек запросов;
* Долговременную память между вызовами;
* Планировщик для разбиения задач на подзадачи.

Когда стоит брать:

* Корпоративные ИИ-ассистенты (CRM, генерация отчётов и т.п.);
* ИИ-процессы, которые ходят во внутренние API/функции;
* Многошаговые чат-боты с планированием и памятью.

До (одиночный запрос):

var result = await openAiClient
.GetCompletionAsync("Какая погода в Москве?");


После (оркестрация через SemanticKernel):

var kernel = Kernel.CreateBuilder().Build();
var prompt = kernel
.CreateFunctionFromPrompt("Какая погода в Москве?");
var result = await prompt.InvokeAsync();


Можно регистрировать плагины, которые вызывают API, делают авторизацию, исполняют SQL или дергают другие сервисы — и всё из .NET. Документация

4. System.Threading.Channels + IAsyncEnumerable

Проблема: BlockingCollection, ConcurrentQueue и кастомные очереди в паттерне «производитель/потребитель» часто упираются в блокировки, нестабильность и проблемы под нагрузкой. Особенно в телеметрии и потоковых пайплайнах.

Решение:

System.Threading.Channels даёт:

* Быстрый обмен сообщениями внутри процесса;
* Контроль пропускной способности через ограниченные каналы;
* Простую связку с IAsyncEnumerable<T>;
* Асинхронный стриминг без лишних выделений памяти.

Изначально жило внутри SignalR и gRPC, сейчас это нормальный способ собирать отказоустойчивые асинхронные конвейеры в .NET.

Когда использовать:

* Сбор телеметрии и обработка метрик;
* Асинхронные пайплайны логов/событий;
* Очередеподобное поведение без внешней брокерской инфраструктуры;
* Изоляция перегородками (Bulkhead) в фоновых сервисах.

До (BlockingCollection или Queue<T>):

var queue = new Queue<int>();
lock (queue) { queue.Enqueue(42); }
int item;
lock (queue) { item = queue.Dequeue(); }


После (каналы и async-потоки):

var ch = Channel.CreateUnbounded<int>();
await ch.Writer.WriteAsync(42);

await foreach (var item in ch.Reader.ReadAllAsync())
Console.WriteLine(item);


Отлично работает из коробки с await foreach и подходит для фоновых задач с I/O, стриминговых API и повторяющихся пайплайнов.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍3
Может кому понадобится: проект генерирует архитектурные диаграммы из кода.

https://github.com/likec4/likec4/ 🤨

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10🤣21
Как компилятор выводит тип из default?

default в C# штука мощная. Для ссылочных типов он даёт null, для значимых — нулевое значение. Забавно, что default(T) и new T() для структур могут вести себя по-разному. Раньше в C# нужно было писать default(T), а сейчас, если тип очевиден для компилятора, можно оставить просто default. Но как именно компилятор понимает, что за тип там должен быть? И всегда ли можно доверять этому выводу?

Разберём простые варианты:

// Тип тянется из левой части
int foo = default;

// Тип тянется из параметра
Foo(default);
void Foo(int bar) => throw null;


Тут всё понятно. Но что происходит в switch-выражении?

var sample = new byte[0];
ReadOnlyMemory<byte> foo = sample switch
{
byte[] value => value,
_ => default
};


Кажется логичным ожидать, что default станет default(ReadOnlyMemory<byte>), ведь слева стоит такой тип. Но нет — фактически это будет default(byte[]). Компилятор берёт тип не из левой части, а из веток switch и пытается вывести общий тип. В данном случае общий тип — byte[], значит и default станет byte[].

Для этого конкретного примера разницы нет — и default(byte[]), и default(ReadOnlyMemory<byte>) в итоге дадут совместимый результат. Но последствия могут всплыть позже. Например, если заменить тип на nullable:

var sample = new object();
ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default
};


Тип default снова выводится как default(byte[]), то есть null. А дальше начинает работать неявное преобразование:

// из исходников .NET 9
public static implicit operator ReadOnlyMemory<T>(T[]? array)
=> new ReadOnlyMemory<T>(array);


Конструктор ReadOnlyMemory<T>, который принимает T[]?, при null выдаёт пустое значение:

// из исходников .NET 9
public ReadOnlyMemory(T[]? array)
{
if (array == null)
{
this = default;
return;
}

}


В итоге foo окажется пустым ReadOnlyMemory<byte> — не null. То есть foo.HasValue будет true.

Если же явно указать тип в default:

object sample = new();

ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default(ReadOnlyMemory<byte>?)
};


То результатом switch будет именно null, и foo.HasValue станет false.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
62