🆚 EF Core vs Dapper — новые фичи EF закрывают разрыв
EF Core и Dapper решают пересекающиеся, но не одинаковые задачи. С каждым релизом EF Core сокращает отставание по производительности, но у Dapper по-прежнему есть своя ниша.
Что нового в EF Core
Compiled queries убирают overhead на трансляцию повторяющихся запросов.
Улучшенная поддержка raw SQL с проекцией в DTO, JSON-колонки, батчинг. В итоге для типичных API-ридов с небольшими DTO разница с Dapper составляет около 10–30%, а при доминировании latency на стороне БД она вообще не заметна.
Где Dapper всё ещё выигрывает
Кастомные джоины, CTE, оконные функции, db-specific хинты. Read-mostly микросервисы без change tracking. Хранимые процедуры. Минимальный jitter на p95–p99.
Прагматичный подход
Большинство команд используют гибрид. EF Core по умолчанию для всего с
Типичные ошибки
Возврат сущностей вместо DTO из API. Lazy loading без контроля. N+1 из-за навигационных свойств. Отсутствие
Compiled EF vs non-compiled экономит 0.5–2.5 мс на вызов. Если compiled query укладывается в SLO в пределах 15% от Dapper и проще в поддержке, оставляйте EF.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
EF Core и Dapper решают пересекающиеся, но не одинаковые задачи. С каждым релизом EF Core сокращает отставание по производительности, но у Dapper по-прежнему есть своя ниша.
Что нового в EF Core
Compiled queries убирают overhead на трансляцию повторяющихся запросов.
private static readonly Func<AppDb, Guid, Task<UserDto?>> GetById =
EF.CompileAsyncQuery((AppDb db, Guid id) =>
db.Users.AsNoTracking()
.Where(u => u.Id == id)
.Select(u => new UserDto(u.Id, u.Email))
.FirstOrDefault());
ExecuteUpdateAsync / ExecuteDeleteAsync позволяют делать set-based операции без материализации сущностей.await db.Orders
.Where(o => o.Status == "Pending" && o.CreatedAtUtc < cutoff)
.ExecuteUpdateAsync(s =>
s.SetProperty(o => o.Status, _ => "Expired"));
Улучшенная поддержка raw SQL с проекцией в DTO, JSON-колонки, батчинг. В итоге для типичных API-ридов с небольшими DTO разница с Dapper составляет около 10–30%, а при доминировании latency на стороне БД она вообще не заметна.
Где Dapper всё ещё выигрывает
Кастомные джоины, CTE, оконные функции, db-specific хинты. Read-mostly микросервисы без change tracking. Хранимые процедуры. Минимальный jitter на p95–p99.
Прагматичный подход
Большинство команд используют гибрид. EF Core по умолчанию для всего с
Select + AsNoTracking. Для топ 2–5 горячих запросов пробуют compiled queries. Если мало, реализуют только их через Dapper за интерфейсом (IOrderQueries), а остальное на EF.Типичные ошибки
Возврат сущностей вместо DTO из API. Lazy loading без контроля. N+1 из-за навигационных свойств. Отсутствие
AsNoTracking на ридах. Generic repository, который прячет фичи EF.Compiled EF vs non-compiled экономит 0.5–2.5 мс на вызов. Если compiled query укладывается в SLO в пределах 15% от Dapper и проще в поддержке, оставляйте EF.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7
Открываете C# файл, а там десяток красных подчёркиваний. Нажимаете
Ctrl+., выбираете нужный namespace, переходите к следующей ошибке, снова Ctrl+.. И так по кругу. Visual Studio умеет добавлять using только по одному. В Rider есть Import Missing References, но это другая IDE.OhUsings решает эту проблему внутри Visual Studio. Расширение сканирует файл, находит все неразрешённые типы через Roslyn и добавляет нужные
using директивы разом. Без ручного перебора.Как это работает
Под капотом OhUsings использует семантическую модель Roslyn, а не regex. Расширение читает диагностики компилятора (
CS0246, CS0103, CS0234 и ещё около десятка), извлекает имена типов и находит подходящие namespace через SymbolFinder. Если тип однозначный, using добавляется автоматически. Если тип встречается в нескольких namespace (например, Timer живёт и в System.Timers, и в System.Threading), расширение покажет диалог выбора.Добавленные директивы сортируются по алфавиту,
System.* ставятся первыми. Всё форматируется через Roslyn.Три области применения
Можно запустить на текущем файле, на активном проекте или на всём solution целиком.
Установка
Самый простой способ: в Visual Studio 2022 откройте Extensions → Manage Extensions, найдите OhUsings и нажмите Download. После перезапуска расширение готово к работе.
Или соберите из исходников:
git clone https://github.com/MabroukMahdhi/OhUsings.git
Откройте
OhUsings.sln в Visual Studio 2022, соберите проект и установите полученный OhUsings.vsix.Как использовать
Три варианта на выбор. Через меню: Tools → OhUsings: Import All Missing Usings. Через контекстное меню: правый клик в редакторе → OhUsings: Import All Missing Usings. Или через light bulb: ставите курсор на неразрешённый тип, нажимаете
Ctrl+. и выбираете действие из предложенных OhUsings.Результат отображается в статусной строке и Output Window.
Настройки
Расширение поддерживает несколько опций.
SortUsings (по умолчанию true) отвечает за сортировку директив. PlaceSystemFirst (по умолчанию true) ставит System.* выше остальных. MaxDiagnosticsPerDocument (по умолчанию 200) ограничивает число обрабатываемых диагностик на файл для защиты производительности.Ограничения
OhUsings работает только с типами из уже подключённых сборок. Добавлять NuGet пакеты или ссылки на проекты расширение пока не умеет, но это есть в планах. Если тип неоднозначный, его придётся выбрать вручную через диалог.
Итого
OhUsings экономит время на рутине. Вместо того чтобы обходить каждую ошибку по отдельности, вы запускаете одну команду и получаете все нужные
using сразу. Расширение бесплатное, с открытым исходным кодом под MIT лицензией, без телеметрии и сетевых запросов.📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11🥱3
Thread.Start существует с первой версии .NET, и большинство разработчиков знают, что он «запускает поток». Но что именно происходит в момент вызова, как передать данные в поток и почему Thread почти не используют в новом коде — стоит разобрать подробно.Как это устроено
Thread — это обёртка над потоком операционной системы. Каждый созданный объект System.Threading.Thread соответствует одному OS-потоку со своим стеком (по умолчанию 1 МБ на x64).Создать поток можно двумя способами — через
ThreadStart или ParameterizedThreadStart:// Без параметра
var t1 = new Thread(new ThreadStart(DoWork));
// С параметром типа object
var t2 = new Thread(new ParameterizedThreadStart(DoWorkWithParam));
После
new Thread(...) поток ещё не запущен. Запуск происходит при вызове Start:t1.Start(); // ThreadStart — без аргумента
t2.Start("hello"); // ParameterizedThreadStart — передаём object
Внутри метода поток получает управление не сразу: ОС ставит его в очередь планировщика. Поэтому код после
Start() в вызывающем потоке продолжает выполняться параллельно.Передача данных в поток
ParameterizedThreadStart принимает object — это неудобно, потому что нужно приводить тип внутри метода:void DoWorkWithParam(object? state)
{
var message = (string)state!;
Console.WriteLine(message);
}
Более чистый способ — захват переменной через лямбду:
string data = "hello from closure";
var t = new Thread(() => Console.WriteLine(data));
t.Start();
Тут важно понимать: лямбда захватывает ссылку на переменную, а не значение. Если
data изменится до того, как поток до неё доберётся, поток увидит новое значение.Фоновые и foreground-потоки
По умолчанию
Thread создаётся как foreground (IsBackground = false). Это значит, что процесс не завершится, пока поток не закончит работу. Если нужно обратное поведение:var t = new Thread(DoWork);
t.IsBackground = true;
t.Start();
Фоновый поток будет убит принудительно при выходе из основного потока.
Подводные камни
Thread.Abort() удалён начиная с .NET 5 — он бросал ThreadAbortException в произвольном месте и приводил к непредсказуемому состоянию программы. Для отмены нужно использовать CancellationToken.Совместный доступ к данным между потоками требует синхронизации. Без неё возникают гонки, которые воспроизводятся нестабильно и тяжело отлаживаются:
int counter = 0;
var t1 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
var t2 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
t1.Start(); t2.Start();
t1.Join(); t2.Join();
// counter будет меньше 20000 — это гонка
Console.WriteLine(counter);
Чтобы дождаться завершения потока, используют
Join. Без него нельзя гарантировать, что поток завершился до следующей строки кода.Пример кода
Корректный вариант с отменой через
CancellationToken:var cts = new CancellationTokenSource();
var t = new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
{
Console.WriteLine("Working...");
Thread.Sleep(500);
}
Console.WriteLine("Stopped.");
});
t.IsBackground = true;
t.Start();
Thread.Sleep(2000);
cts.Cancel();
t.Join();
Thread.Start запускает OS-поток, но не гарантирует немедленное выполнение. Данные лучше передавать через замыкания, а не через ParameterizedThreadStart. Для большинства задач сегодня используют Task, async/await или ThreadPool — они не блокируют поток в ожидании и экономят ресурсы. 📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤8❤🔥2👏1
🆚 Minimal APIs vs контроллер
ASP.NET Core предлагает два способа строить HTTP API. Minimal APIs и контроллеры. Microsoft в документации рекомендует Minimal APIs для новых проектов, потому что это меньше кода, меньше конфигурации и выше производительность. Но это не значит, что контроллеры устарели. Это значит, что нужен осознанный выбор.
Когда Minimal APIs подходят лучше
Minimal APIs хорошо работают, когда API небольшой и сфокусированный. Микросервисы, внутренние API, легковесные эндпоинты, vertical slice архитектура. Каждый эндпоинт явно определяет маршрут, нет лишней церемонии с наследованием и атрибутами.
Вот как выглядит чистый эндпоинт на Minimal API:
Читаемо, прямолинейно, бизнес-логика вынесена в
Когда контроллеры всё ещё уместны
Контроллеры полезны там, где уже есть устоявшаяся MVC-инфраструктура, стандартизированные фильтры, сложное версионирование или большая команда, привыкшая к этому паттерну. Если в проекте выстроены конвенции вокруг контроллеров и это работает, переписывать на Minimal APIs ради моды нет смысла.
Что изменилось в .NET 10
В .NET 10 Microsoft добавила поддержку валидации для Minimal APIs, включая кастомизацию ответов через
Главная ловушка обоих подходов
Плохо организованный Minimal API проект превращается в огромный
Что выбрать
Для новых микросервисов и сфокусированных API автор статьи рекомендует Minimal APIs с vertical slices. Но не сваливать всё в
Для систем с уже выстроенной контроллерной архитектурой или жёсткими корпоративными стандартами контроллеры остаются рабочим выбором.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
ASP.NET Core предлагает два способа строить HTTP API. Minimal APIs и контроллеры. Microsoft в документации рекомендует Minimal APIs для новых проектов, потому что это меньше кода, меньше конфигурации и выше производительность. Но это не значит, что контроллеры устарели. Это значит, что нужен осознанный выбор.
Когда Minimal APIs подходят лучше
Minimal APIs хорошо работают, когда API небольшой и сфокусированный. Микросервисы, внутренние API, легковесные эндпоинты, vertical slice архитектура. Каждый эндпоинт явно определяет маршрут, нет лишней церемонии с наследованием и атрибутами.
Вот как выглядит чистый эндпоинт на Minimal API:
public static class CreateOrderEndpoint
{
public static IEndpointRouteBuilder MapCreateOrder(this IEndpointRouteBuilder app)
{
app.MapPost("/orders", async (
CreateOrderRequest request,
ICreateOrderUseCase useCase,
CancellationToken cancellationToken) =>
{
var result = await useCase.ExecuteAsync(request, cancellationToken);
return Results.Created($"/orders/{result.OrderId}", result);
});
return app;
}
}
Читаемо, прямолинейно, бизнес-логика вынесена в
ICreateOrderUseCase.Когда контроллеры всё ещё уместны
Контроллеры полезны там, где уже есть устоявшаяся MVC-инфраструктура, стандартизированные фильтры, сложное версионирование или большая команда, привыкшая к этому паттерну. Если в проекте выстроены конвенции вокруг контроллеров и это работает, переписывать на Minimal APIs ради моды нет смысла.
Что изменилось в .NET 10
В .NET 10 Microsoft добавила поддержку валидации для Minimal APIs, включая кастомизацию ответов через
IProblemDetailsService. Раньше отсутствие встроенной валидации было одним из главных аргументов против Minimal APIs. Теперь этот аргумент слабее.Главная ловушка обоих подходов
Плохо организованный Minimal API проект превращается в огромный
Program.cs. Плохо организованный проект на контроллерах превращается в папку с пустыми pass-through экшенами. Ни один из подходов не спасёт от слабой архитектуры.Что выбрать
Для новых микросервисов и сфокусированных API автор статьи рекомендует Minimal APIs с vertical slices. Но не сваливать всё в
Program.cs, а организовывать эндпоинты по фичам, выносить бизнес-логику из обработчиков маршрутов, использовать явные контракты запросов и ответов.Для систем с уже выстроенной контроллерной архитектурой или жёсткими корпоративными стандартами контроллеры остаются рабочим выбором.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6🔥2
В C# переделывают модель работы с
unsafe. Ключевое слово больше не будет просто «включать указатели». Оно станет контрактом между вызываемым и вызывающим методом. Превью появится в .NET 11, продакшен-релиз в .NET 12.Проблема
Сейчас
unsafe на методе означает «здесь можно использовать указатели». Он не накладывает обязательств на вызывающую сторону. Метод вроде Marshal.ReadByte принимает IntPtr, разыменовывает указатель внутри, но вызывается из безопасного кода без ограничений. Конвенции не проверяет компилятор и не видит ревьюер с первого взгляда.Что меняется
unsafe в сигнатуре метода теперь означает контракт: вызывающая сторона обязана выполнить задокументированные условия. Каждая небезопасная операция должна быть обёрнута во внутренний блок unsafe { }. Каждый unsafe-метод должен содержать блок /// <safety> с описанием обязательств.Вот
Marshal.ReadByte в новой модели:/// <safety>
/// Сумма ptr и ofs должна указывать на байт,
/// доступный вызывающей стороне для чтения.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int ofs)
{
byte* addr = (byte*)ptr;
unsafe
{
// SAFETY: полагаемся на обязательство вызывающей стороны.
return addr[ofs];
}
}
Приведение
(byte*)ptr безопасно, это преобразование числа. А вот addr[ofs] опасно, потому что разыменовывает указатель. Именно эта строка обёрнута в unsafe { }.Другие изменения
unsafe на типе теперь вызывает ошибку компиляции. Область действия опускается на уровень методов и свойств. unsafe на методе больше не создаёт unsafe-контекст автоматически. Типы указателей в сигнатуре больше не распространяют unsafe, опасна только операция разыменования. Новое ключевое слово safe обязательно для extern-деклараций, пропустить оба модификатора нельзя.Включение
Модель включается через новое свойство проекта и работает независимо от
<AllowUnsafeBlocks>. Для миграции планируется dotnet format fixer, который механически обернёт вызовы в unsafe { } и перенесёт модификаторы с типов на методы. Писать <safety>-документацию придётся вручную.Нарушения становятся ошибками компиляции, не предупреждениями. Модель повторяет подход Rust и Swift, но с учётом экосистемы .NET. Для тех, кто использует unsafe, код станет прозрачнее для ревью.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍1
🧑💻 Генераторы в .NET замедляют билд
Генераторы кода работают внутри компилятора. Они запускаются при каждой сборке и при каждом нажатии клавиши в IDE. Если не следить за ними, билд на 10 секунд может превратиться в полторы минуты без видимых причин. Вот что можно сделать, чтобы этого не допустить.
Сделайте сгенерированный код видимым
По умолчанию сгенерированные
Добавьте в
Теперь все сгенерированные файлы попадают в папку
Замеряйте время через binary log
Ощущение «билд стал медленнее» ничего не даёт. Нужны конкретные числа по каждому генератору. Соберите проект с флагом:
Откройте полученный
Проверьте транзитивные зависимости
Генераторы кода приезжают вместе с NuGet-пакетами. Вы подключаете библиотеку логирования или маппер, а внутри пакета лежит генератор, который теперь запускается в каждом проекте. Вы его не выбирали, но он работает.
В том же binary log задача CSC показывает все загруженные сборки анализаторов и генераторов. Пройдитесь по списку. Если видите незнакомое имя, выясните, откуда оно пришло. Команда
Используйте только инкрементальные генераторы
Если вы пишете свои генераторы, реализуйте
Но сам по себе интерфейс ничего не гарантирует. Генератор, который реализует
Принимайте решения по числам
Если дорогой генератор пришёл из стороннего пакета, вы не можете его исправить. Но можете решить, стоит ли пакет своего времени сборки. Иногда стоит. Иногда дешевле отказаться от пакета и решить задачу иначе.
Главное тут не гадать, а смотреть в binary log. Там есть имя генератора и его стоимость в миллисекундах. Этого достаточно, чтобы принять осознанное решение.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
Генераторы кода работают внутри компилятора. Они запускаются при каждой сборке и при каждом нажатии клавиши в IDE. Если не следить за ними, билд на 10 секунд может превратиться в полторы минуты без видимых причин. Вот что можно сделать, чтобы этого не допустить.
Сделайте сгенерированный код видимым
По умолчанию сгенерированные
.g.cs файлы не записываются на диск. Они существуют только в памяти компилятора. Когда что-то ломается, вы видите ошибку в файле, который не можете открыть.Добавьте в
Directory.Build.props в корне решения:<Project>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)generated
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>
Теперь все сгенерированные файлы попадают в папку
obj/generated. Их можно читать, искать по ним и ставить точки останова в отладчике. Это настройка на один раз, и она стоит того.Замеряйте время через binary log
Ощущение «билд стал медленнее» ничего не даёт. Нужны конкретные числа по каждому генератору. Соберите проект с флагом:
dotnet build -bl
Откройте полученный
msbuild.binlog в MSBuild Structured Log Viewer. Найдите задачу CSC. Внутри будет список всех генераторов с временем выполнения в миллисекундах. Собирайте именно полное решение, а не отдельный проект. Стоимость генератора умножается на количество проектов, которые его подтягивают.Проверьте транзитивные зависимости
Генераторы кода приезжают вместе с NuGet-пакетами. Вы подключаете библиотеку логирования или маппер, а внутри пакета лежит генератор, который теперь запускается в каждом проекте. Вы его не выбирали, но он работает.
В том же binary log задача CSC показывает все загруженные сборки анализаторов и генераторов. Пройдитесь по списку. Если видите незнакомое имя, выясните, откуда оно пришло. Команда
dotnet nuget why поможет отследить цепочку зависимостей.Используйте только инкрементальные генераторы
Если вы пишете свои генераторы, реализуйте
IIncrementalGenerator, а не устаревший ISourceGenerator. Инкрементальный генератор кеширует промежуточные результаты. Если входные данные не изменились, он пропускает повторную обработку.Но сам по себе интерфейс ничего не гарантирует. Генератор, который реализует
IIncrementalGenerator и при этом пересобирает всё заново на каждый вызов, работает так же медленно, как устаревший. Кеширование должно быть реальным. Пайплайн нужно строить так, чтобы неизменённые входы давали кешированный выход.Принимайте решения по числам
Если дорогой генератор пришёл из стороннего пакета, вы не можете его исправить. Но можете решить, стоит ли пакет своего времени сборки. Иногда стоит. Иногда дешевле отказаться от пакета и решить задачу иначе.
Главное тут не гадать, а смотреть в binary log. Там есть имя генератора и его стоимость в миллисекундах. Этого достаточно, чтобы принять осознанное решение.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤🔥3❤1👾1
Если вы запускали NuGet Audit на .NET-проекте, то наверняка видели предупреждения о транзитивных пакетах, которые не устанавливали. Часто это
System.Text.Json, System.Memory и подобные, которые уже есть в рантайме в более свежей версии. Предупреждение есть, а реальной проблемы нет. В .NET 10 эту ситуацию исправили.
В чём была проблема
Многие библиотеки на nuget.org таргетят
netstandard2.0 и тянут за собой пакеты вроде System.Text.Json 8.0.0. Проект на .NET 10 уже содержит их в рантайме в более новой версии, но NuGet всё равно резолвит старый пакет в граф. Когда на него публикуется CVE, сканер помечает его как уязвимый. По факту приложение использует версию из рантайма. Это ложное срабатывание, но отличить его от настоящего без ручного разбора сложно.
Что такое package pruning
Package pruning убирает из графа зависимостей пакеты, которые уже поставляются .NET Runtime Libraries. NuGet при restore сверяется со списком пакетов, входящих в целевой фреймворк. Если транзитивная зависимость попадает в диапазон, она исключается. Пакет не скачивается и не фигурирует в аудите.
Для прямых
PackageReference логика другая: NuGet ставит PrivateAssets='all' и IncludeAssets='none'. Ссылка остаётся в csproj, пока вы сами её не удалите. Если пакет можно убрать полностью, NuGet выдаёт NU1510.Pruning работает только в пределах версии фреймворка. Проект на
net8.0 с транзитивной зависимостью System.Text.Json 9.0.0 не уберёт пакет, потому что платформа поставляет только 8.0.x.Что изменилось в .NET 10
Package pruning впервые появился как opt-in в SDK 9.0.200. В .NET 10 он включён по умолчанию для
net10.0 и выше. Одновременно NuGetAuditMode по умолчанию стал all, то есть NuGet проверяет и транзитивные зависимости.По телеметрии Microsoft, проекты с новыми дефолтами получают на 70% меньше транзитивных предупреждений об уязвимостях. Restore ускоряется до 50% на уровне проекта за счёт меньшего графа.
Дефолтные настройки .NET 10:
<PropertyGroup>
<NuGetAuditMode>all</NuGetAuditMode>
<RestoreEnablePackagePruning>true</RestoreEnablePackagePruning>
</PropertyGroup>
Граф зависимостей теперь точнее отражает то, что приложение реально использует. Меньше ложных срабатываний, быстрее restore, понятнее аудит. Для проектов на .NET 10 всё работает из коробки.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🎉4🔥2👏1
Model Context Protocol (MCP) упростил подключение инструментов к AI-агентам. Но чем больше инструментов доступно агенту, тем острее вопрос контроля. Какие tools можно вызывать? Что если в описании инструмента спрятана prompt-инъекция? Как не пропустить опасный ответ обратно в модель?
Microsoft предложила ответ в виде NuGet-пакета
Microsoft.AgentGovernance.Extensions.ModelContextProtocol. Это Public Preview расширение для официального MCP C# SDK, которое добавляет слой governance к вашему MCP-серверу одним вызовом.Что делает пакет
Пакет встраивается в стандартный
IMcpServerBuilder и добавляет четыре вещи. Сканирование инструментов при старте, проверку политик при каждом вызове, санитизацию ответов перед возвратом в модель и аудит с метриками.Установка стандартная:
dotnet add package Microsoft.AgentGovernance.Extensions.ModelContextProtocol
Подключение через один extension-метод:
using AgentGovernance.Extensions.ModelContextProtocol;
builder.Services
.AddMcpServer()
.WithGovernance(options =>
{
options.PolicyPaths.Add("policies/mcp.yaml");
options.DefaultAgentId = "did:mcp:server";
options.ServerName = "contoso-support";
});
Сканирование при запуске
До того как инструменты станут доступны клиентам, пакет проверяет их определения на угрозы. Среди детектируемых категорий: tool poisoning, тайпсквоттинг, скрытые инструкции в описаниях, schema abuse (например, поля вроде
password или system_prompt), cross-server атаки. Если инструмент не прошёл проверку, сервер по умолчанию не запустится. Это fail closed на этапе старта, а не рантайм-фильтр.Политики вызовов
Контроль доступа к инструментам описывается в YAML-файлах. Политики работают по модели allow/deny с приоритетами. Пример:
apiVersion: governance.toolkit/v1
version: "1.0"
name: mcp-governance-policy
default_action: deny
rules:
- name: allow-echo
condition: "tool_name == 'echo'"
action: allow
priority: 10
Если вызов запрещён, пакет вернёт governed error result вместо выполнения инструмента. Политики поддерживают identity: если есть аутентифицированный вызывающий, его идентификатор участвует в оценке. Если нет, используется fallback DID, например
did:mcp:anonymous.Санитизация ответов
Ответы инструментов проходят через санитайзер до возврата клиенту. Он ищет prompt-injection теги (
<system>...</system>), фразы переопределения вроде «ignore previous instructions», паттерны утечки credentials и URL для эксфильтрации данных. Опасные фрагменты вырезаются, остальной контент сохраняется.Дефолты из коробки
Пакет включает защиту по умолчанию без дополнительной настройки.
ScanToolsOnStartup, FailOnUnsafeTools, SanitizeResponses, GovernFallbackHandlers, EnableAudit, EnableMetrics — всё это true сразу после подключения.Итого
Пакет не требует форка SDK и не вводит отдельный прокси-процесс. Он оборачивает финальный
ToolCollection в стандартном builder-пайплайне и применяется ко всем инструментам, зарегистрированным до или после вызова .WithGovernance(). Работает с .NET 8+.Если вы строите MCP-серверы для внутренних копайлотов или корпоративных агентов, это готовый способ добавить контроль доступа, аудит и защиту от инъекций без ручной реализации в каждом сервисе.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2❤1👏1🎉1
Обычный JWT нельзя мгновенно отозвать без дополнительной инфраструктуры. Если токен украден, он остаётся валидным до окончания срока действия.
csharp new Client { ClientId = «banking_app», AccessTokenType = AccessTokenType.Reference };csharp await client.RevokeTokenAsync(new TokenRevocationRequest { Address = «https://identity.example.com/connect/revocation», Token = accessToken });json { «active»: false }— банковские приложения;
— медицинские системы;
— внутренние сервисы с повышенными требованиями к безопасности;
— критичные операции, где нельзя ждать истечения JWT.
Платой за мгновенный отзыв становится интроспекция токена на стороне сервера
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤3😢1🙏1
Чтобы не теряться в потоке постов, собрали удобную навигацию по рубрикам:
#sharp_view — короткие и полезные посты с кодом, best practices, фишки C#, сравнение подходов, code snippets и разбор багов
#il_люминатор — глубокие технические разборы: архитектурные паттерны, производительность, устройство .NET, CLR, GC, JIT и сложные концепции
#async_news — релизы .NET, обновления IDE, анонсы конференций, дайджесты и другие важные новости экосистемы
#entry_point — опросы, холивары, вопросы подписчикам, истории из практики и обсуждения с сообществом
#dotnet_challenge — задачи по C# и .NET, вопросы с собеседований, поиск ошибок в коде и интерактивные викторины
#схема — инфографика, диаграммы, шпаргалки, дорожные карты и визуальное объяснение сложных тем
#garbage_collector — мемы, юмор и забавные истории из жизни .NET-разработчиков
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7
Раньше приходилось писать так:
private string _email;
public string Email
{
get => _email;
set => _email = value.Trim().ToLowerInvariant();
}
Начиная с C# 14 можно использовать field — ссылку на автоматически сгенерированное backing field:
public string Email
{
get;
set => field = value.Trim().ToLowerInvariant();
}
Подходит для:
⚠️ Если в классе уже есть член с именем field, внутри аксессора приоритет получит новое ключевое слово. Для обращения к своему члену используйте @
field или this.field.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11
This media is not supported in your browser
VIEW IN TELEGRAM
Sololearn предлагает интерактивный курс по C# среднего уровня с практическими заданиями и встроенной AI-проверкой решений.
В программе:
— массивы
— ООП
— структуры и enum
— исключения
— работа с файлами
— generics
Подойдёт тем, кто уже знает основы языка и хочет закрепить их на практике.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6🤔2
Если ищете один символ, используйте перегрузку с char:
text.StartsWith(’s’);
вместо:
text.StartsWith(«s»);
То же самое касается:
text.EndsWith('s');
text.IndexOf(’s’);
Перегрузки с char обычно немного эффективнее и сразу показывают, что поиск идёт по одному символу.
📌 Для одного символа — char, для строки или подстроки — string.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤4🥱4
До .NET 6 проверки обычно выглядели так:
if (arg is null)
throw new ArgumentNullException(nameof(arg));
Сейчас для этого есть встроенный метод:
ArgumentNullException.ThrowIfNull(arg);
Что он даёт:
Например:
public UserService(IRepository repository)
{
ArgumentNullException.ThrowIfNull(repository);
_repository = repository;
}
Под капотом используется специальная реализация для guard clauses, поэтому этот вариант не только короче, но и хорошо оптимизируется рантаймом.
Используете ThrowIfNull() или по привычке всё ещё пишете if (arg is null)
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14👍5🥱2
Самый популярный вариант пагинации в EF Core выглядит так:
var customers = await dbContext.Customers
.AsNoTracking()
.Where(c => c.IsActive)
.OrderBy(c => c.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
SELECT *
FROM Customers
ORDER BY Id
OFFSET 200 ROWS
FETCH NEXT 100 ROWS ONLY
Для небольших таблиц этого обычно достаточно.
Поэтому для больших таблиц и бесконечной прокрутки часто используют Keyset Pagination:
var customers = await dbContext.Customers
.Where(c => c.Id > lastSeenId)
.OrderBy(c => c.Id)
.Take(pageSize)
.ToListAsync();
Такой подход не требует пропускать тысячи строк и обычно масштабируется заметно лучше.
📌 Простая навигация по страницам → Skip/Take
📌 Большие таблицы и высокая нагрузка → Keyset Pagination
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤5🔥5🤔3
pattern matching in csharp.jpg
69.9 KB
Вместо проверки типа с последующим приведением можно выполнить обе операции сразу в одном выражении. Меньше шаблонного кода, безопаснее приведение типов и лучше читаемость.
На картинке — простой пример, который стоит взять на вооружение, если ещё пишете код «по-старому»
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🥱5🔥1
Вместо того чтобы вручную вызывать десятки MapGet() и MapPost(), можно зарегистрировать все endpoint автоматически.
Идея простая:
В результате Program.cs сводится буквально к двум строкам:
builder.Services.AddEndpoints(typeof(Program).Assembly);
app.MapEndpoints();
Чем больше проект, тем заметнее становится преимущество такого подхода.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥰4❤3💯2🤔1
Если несколько операций не зависят друг от друга, не запускайте их по очереди. Запустите сразу все и дождитесь завершения.
var usersTask = LoadUsersAsync(ct);
var ordersTask = LoadOrdersAsync(ct);
var catalogTask = LoadCatalogAsync(ct);
await Task.WhenAll(usersTask, ordersTask, catalogTask);
var users = await usersTask;
var orders = await ordersTask;
var catalog = await catalogTask;
Такой подход позволяет выполнять операции параллельно и уменьшает общее время ожидания.
— передавайте CancellationToken во все методы;
— если операций очень много, ограничивайте параллелизм через SemaphoreSlim;
— Task.WhenAll завершится с ошибкой, если хотя бы одна из задач завершилась исключением.
Используйте WhenAll, когда итоговый результат нужен только после завершения всех операций.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
Проверка коллекций часто превращается в цепочку условий: сначала Length, потом обращения по индексам, затем ещё несколько if.
В C# 11 появились списочные паттерны (List Patterns), которые позволяют описать форму коллекции прямо в switch.
static string Describe(int[] numbers) => numbers switch
{
[] => "пусто",
[var single] => $"один элемент: {single}",
[var first, var second] => $"два: {first} и {second}",
[var first, .., var last] => $"первый {first}, последний {last}",
};
Каждая ветка описывает форму данных, а не последовательность проверок. Не нужно вручную проверять Length и обращаться к элементам по индексам.
static string Route(string[] args) => args switch
{
["run", var file] => $"запускаем {file}",
["build", .. var rest] => $"сборка с флагами: {string.Join(", ", rest)}",
_ => "неизвестная команда",
};
Обратите внимание на .. var rest: оператор среза не только пропускает элементы, но и позволяет сразу получить их в отдельную коллекцию. Списочные паттерны работают с массивами, списками и другими типами, которые поддерживают индексатор и имеют Length или Count.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥17
Невалидный пользовательский ввод — ожидаемый сценарий, а не авария. Такой код работает, но использует исключения для обычной проверки:
try
{
var age = int.Parse(input);
SaveAge(age);
}
catch
{
ShowError();
}
if (int.TryParse(input, out var age))
{
SaveAge(age);
}
else
{
ShowError();
}
TryParse() сразу показывает: преобразование может не удаться, и этот результат предусмотрен логикой программы. Используйте его для форм, query-параметров, настроек, файлов и других внешних данных.
📌 Ожидаемая ошибка — явная проверка. Исключение — действительно исключительная ситуация.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱6❤1