Старший Backend C# ASP. NET разработчик — от 300 000 до 400 000 ₽ , удалёнка или гибрид в Москве
Разработчик C# — офис в Минске
Unity Developer — удалёнка
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3👍1😁1
🛠 Native AOT в Node.js: пишем аддоны на C#
Исторически для платформоспецифичных задач в VS Code расширении (например, чтение реестра Windows) использовались нативные аддоны на C++. Их сборка требовала
Решение
Native AOT умеет компилировать .NET-код в нативную shared library с произвольными точками входа. Node.js аддоны требуют только одного: экспортированной функции
Как это работает
Аддон взаимодействует с Node.js через N-API — стабильный C-совместимый интерфейс. Язык реализации значения не имеет, важно лишь соответствие сигнатурам.
Минимальный
Точка входа модуля:
Атрибут
Вызов N-API из .NET
N-API функции экспортирует сам
После этого P/Invoke с именем
Пример экспортируемой функции
Функция читает значение из реестра Windows и возвращает его в JavaScript:
Исключения нужно обязательно перехватывать: необработанное исключение в методе с
Что в итоге
Команда убрала зависимость от Python и
Производительность сопоставима с C++-реализацией: Native AOT генерирует оптимизированный нативный код, а для задач типа доступа к реестру и маршалинга строк разницы на практике нет.
➡️ Источник
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
Исторически для платформоспецифичных задач в VS Code расширении (например, чтение реестра Windows) использовались нативные аддоны на C++. Их сборка требовала
node-gyp и конкретной версии Python на каждой машине разработчика. Для .NET-команды это лишняя зависимость, которую нужно поддерживать и в CI, и при онбординге новых участников.Решение
Native AOT умеет компилировать .NET-код в нативную shared library с произвольными точками входа. Node.js аддоны требуют только одного: экспортированной функции
napi_register_module_v1. Native AOT с этим справляется.Как это работает
Аддон взаимодействует с Node.js через N-API — стабильный C-совместимый интерфейс. Язык реализации значения не имеет, важно лишь соответствие сигнатурам.
Минимальный
.csproj:<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
PublishAot говорит SDK собрать нативную библиотеку при публикации. AllowUnsafeBlocks нужен из-за работы с указателями при интеропе с N-API.Точка входа модуля:
[UnmanagedCallersOnly(
EntryPoint = "napi_register_module_v1",
CallConvs = [typeof(CallConvCdecl)])]
public static nint Init(nint env, nint exports)
{
Initialize();
RegisterFunction(env, exports, "readStringValue"u8, &ReadStringValue);
return exports;
}
Атрибут
[UnmanagedCallersOnly] экспортирует метод с нужным именем и calling convention. Суффикс u8 создаёт ReadOnlySpan<byte> с UTF-8 строкой без аллокаций.Вызов N-API из .NET
N-API функции экспортирует сам
node.exe. Чтобы не линковаться с отдельной библиотекой, используется кастомный резолвер через NativeLibrary.SetDllImportResolver:NativeLibrary.SetDllImportResolver(
Assembly.GetExecutingAssembly(),
(libraryName, assembly, searchPath) =>
libraryName is "node"
? NativeLibrary.GetMainProgramHandle()
: 0);
После этого P/Invoke с именем
"node" будет резолвиться в хост-процесс:[LibraryImport("node", EntryPoint = "napi_create_string_utf8")]
internal static partial Status CreateStringUtf8(
nint env, ReadOnlySpan<byte> str, nuint length, out nint result);Пример экспортируемой функции
Функция читает значение из реестра Windows и возвращает его в JavaScript:
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static nint ReadStringValue(nint env, nint info)
{
try
{
var keyPath = GetStringArg(env, info, 0);
var valueName = GetStringArg(env, info, 1);
if (keyPath is null || valueName is null)
{
ThrowError(env, "Expected two string arguments: keyPath, valueName");
return 0;
}
using var key = Registry.CurrentUser.OpenSubKey(keyPath, writable: false);
return key?.GetValue(valueName) is string value
? CreateString(env, value)
: GetUndefined(env);
}
catch (Exception ex)
{
ThrowError(env, $"Registry read failed: {ex.Message}");
return 0;
}
}
Исключения нужно обязательно перехватывать: необработанное исключение в методе с
[UnmanagedCallersOnly] крашит хост-процесс. ThrowError пробрасывает ошибку в JavaScript как стандартный Error.Что в итоге
Команда убрала зависимость от Python и
node-gyp. Теперь yarn install работает только с Node.js и .NET SDK, которые и так нужны для разработки. CI стал проще, онбординг быстрее.Производительность сопоставима с C++-реализацией: Native AOT генерирует оптимизированный нативный код, а для задач типа доступа к реестру и маршалинга строк разницы на практике нет.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤1😁1
Представьте картину: CPU загружен на 30–40%, ошибок нет, но запросы внезапно начинают тормозить, а время ответа под нагрузкой улетает в небо. Это не магия и не баг в инфраструктуре. Скорее всего, вы столкнулись с голоданием пула потоков.
Потоки это ваша пропускная способность. Каждый заблокированный поток это запрос, который ждёт в очереди. Когда таких потоков становится много, латентность взрывается, хотя CPU при этом спокойно отдыхает.
Причина почти всегда в одном из таких паттернов:
var data = httpClient.GetStringAsync(url).Result; // блокирует поток
Task.Run(() => DoWork()).Wait(); // форсированная синхронизация
Эти вызовы берут асинхронную операцию и намеренно блокируют поток до её завершения. В результате поток занят, но не делает ничего полезного, а просто ждёт.
Попробуйте сделать так и замеряйте результат:
var data = await httpClient.GetStringAsync(url);
📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱11❤3👍2😁1🤔1
Валидация входящих данных в .NET-проектах это рутина, которую каждый решает по-своему. Кто-то пишет
if (value == null) throw new ArgumentNullException(...) в каждом методе, кто-то тащит FluentValidation и настраивает его под свои нужды. OrionGuard предлагает ещё один вариант: fluent-интерфейс, поддержку ASP.NET Core, MediatR, Blazor, gRPC и SignalR. Всё в одной экосистеме.Установка базового пакета:
dotnet add package OrionGuard
Простая валидация выглядят так:
using Moongazing.OrionGuard.Core;
using Moongazing.OrionGuard.Extensions;
Ensure.That(email).NotNull().NotEmpty().Email();
Ensure.That(age).InRange(18, 120);
Если нужна производительность без аллокаций, то есть
FastGuard на основе Span<T>:FastGuard.NotNullOrEmpty(name, nameof(name));
FastGuard.Email(email, nameof(email));
Что внутри
Библиотека разбита на 9 пакетов. Каждый подключается отдельно.
OrionGuard — ядро. OrionGuard.AspNetCore — middleware, фильтры, интеграция с IOptions. OrionGuard.MediatR — автоматическая валидация в CQRS-пайплайне. OrionGuard.Generators — source-генераторы для компайл-тайм валидации без рефлексии. OrionGuard.Blazor — интеграция с EditForm. OrionGuard.Grpc и OrionGuard.SignalR — перехватчики для gRPC и SignalR.Несколько конкретных примеров
Накопление ошибок без исключений:
var result = GuardResult.Combine(
Ensure.Accumulate(email, "Email").NotNull().Email().ToResult(),
Ensure.Accumulate(password, "Password").MinLength(8).ToResult()
);
if (result.IsInvalid)
return BadRequest(result.ToErrorDictionary());
Защита от SQL-инъекций и XSS:
userInput.AgainstSqlInjection(nameof(userInput));
userInput.AgainstXss(nameof(userInput));
filePath.AgainstPathTraversal(nameof(filePath));
Вложенная валидация с путями до поля:
var result = Validate.Nested(order)
.Property(o => o.OrderNumber, p => p.NotEmpty())
.Nested(o => o.Customer, customer => customer
.Property(c => c.Email, p => p.NotEmpty().Email())
.Nested(c => c.Address, address => address
.Property(a => a.ZipCode, p => p.NotEmpty())))
.Collection(o => o.Items, (item, _) => item
.Property(i => i.Quantity, p => p.GreaterThan(0)))
.ToResult();
// Ошибки будут выглядеть так: "Customer.Address.ZipCode", "Items[2].Quantity"
Динамические правила из JSON. Для случаев, когда правила хранятся в базе или конфиге:
var json = """
{
"Rules": [
{ "PropertyName": "Email", "RuleType": "Email" },
{ "PropertyName": "Age", "RuleType": "Range", "Parameters": { "Min": 18, "Max": 120 } }
]
}
""";
var validator = DynamicValidator.FromJson(json);
var result = validator.Validate(userDto);
Source-генератор для NativeAOT:
[GenerateValidator]
public sealed class CreateUserRequest
{
[NotNull, NotEmpty, Length(3, 50)]
public string Name { get; set; }
[NotNull, Email]
public string Email { get; set; }
}
// Валидатор генерируется на этапе компиляции — без рефлексии
var result = CreateUserRequestValidator.Validate(request);
Интеграция с ASP.NET Core:
// Program.cs
builder.Services.AddOrionGuardAspNetCore();
app.MapPost("/api/users", (CreateUserRequest req) => { ... })
.WithValidation<CreateUserRequest>();
Библиотека поддерживает 14 языков для сообщений об ошибках, включая русский. Есть слой совместимости с FluentValidation, миграция сводится к замене
using. Все regex-паттерны генерируются через GeneratedRegex, FrozenSet используется для O(1)-поиска в security-паттернах.📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍3
Это классика технических интервью в C#:
Статический конструктор — когда именно его вызывает рантайм
Вы не пишете
new MyClass(), не обращаетесь к объекту, не делаете вообще ничего явного. А он всё равно срабатывает.📍 Навигация: Вакансии • Задачи • Собесы
#dotnet_challenge
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7😁1
🛠 Структуры не всегда быстрее
Распространённое заблуждение среди разработчиков на C#, что структуры всегда эффективнее классов. На самом деле это работает только для маленьких структур.
Структуры живут на стеке и не создают нагрузку на GC. Но у этого есть цена: при каждой передаче структуры в метод или присваивании создаётся полная копия. Для маленьких структур это быстро. Для больших не очень.
Вот пример структуры, которая выглядит безобидно, но копирует 64+ байт при каждом вызове:
Обратите внимание на List<Items>. Как только структура содержит ссылочные типы, часть преимуществ стека теряется, ведь ссылка всё равно уходит в кучу.
Для маленьких значений без ссылочных типов
Для всего остального
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор
Распространённое заблуждение среди разработчиков на C#, что структуры всегда эффективнее классов. На самом деле это работает только для маленьких структур.
Структуры живут на стеке и не создают нагрузку на GC. Но у этого есть цена: при каждой передаче структуры в метод или присваивании создаётся полная копия. Для маленьких структур это быстро. Для больших не очень.
Вот пример структуры, которая выглядит безобидно, но копирует 64+ байт при каждом вызове:
// Копируется целиком при каждой передаче
public struct BigOrderStruct
{
public int Id;
public string Customer;
public decimal Total;
// ... ещё 12 полей
public List<Item> Items; // это уже ссылочный тип
}
Обратите внимание на List<Items>. Как только структура содержит ссылочные типы, часть преимуществ стека теряется, ведь ссылка всё равно уходит в кучу.
Для маленьких значений без ссылочных типов
readonly record struct идеален:public readonly record struct SmallOrderId(int Id);
Для всего остального
readonly record class чаще выигрывает и по читаемости, и по производительности за счёт лучшего поведения кэша.📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤2😁2
Вышел Ubuntu 26.04 LTS. Вместе с ним официальная поддержка .NET 10 прямо из стандартного репозитория.
Две команды и SDK готов:
sudo apt update
sudo apt install dotnet-sdk-10.0
Проверить, что всё работает:
dotnet run - << 'EOF'
using System.Runtime.InteropServices;
Console.WriteLine($"Hello {RuntimeInformation.OSDescription} from .NET {RuntimeInformation.FrameworkDescription}");
EOF
Вывод будет примерно таким:
Hello Ubuntu Resolute Raccoon from .NET .NET 10.0.5
Это так называемый file-based app. Он передаётся через
stdin напрямую в dotnet run. Стандартный unix-подход.Контейнеры
Образы с тегом
resolute уже доступны. Если вы использовали -noble, достаточно поменять суффикс:sed -i "s/noble/resolute/g" Dockerfile.chiseled
После этого собрать и запустить с ограничениями ресурсов:
docker build --pull -t aspnetapp -f Dockerfile.chiseled .
docker run --rm -it -p 8000:8080 -m 50mb --cpus .5 aspnetapp
Chiseled-образы остались, ничего не убрали
Native AOT
Если нужен маленький бинарь с быстрым стартом —
dotnet-sdk-aot-10.0 теперь в репозитории Ubuntu.Установка:
apt install -y dotnet-sdk-aot-10.0 clang
Публикация простого приложения:
dotnet publish app.cs
Результат:
1.4M artifacts/app/app
3.0M artifacts/app/app.dbg
Запуск занял 3 миллисекунды. Для веб-сервиса с
PublishAot=true итоговый размер около 13 МБ вместе с метаданными System.Text.Json..NET 8 и 9 не входят в основной репозиторий Ubuntu 26.04, но доступны через отдельный PPA от Canonical:
apt install -y software-properties-common
add-apt-repository ppa:dotnet/backports
После подключения появятся пакеты
dotnet-sdk-8.0, dotnet-sdk-9.0 и соответствующие aspnetcore-runtime-*. Поддержка там на уровне "best-effort", то есть официально, но без гарантий уровня LTS.Что важно
Ubuntu 26.04 принёс три заметных изменения на уровне ОС:
• Linux 7.0 — команда .NET начнёт тестирование, как только получит VM.
• Постквантовая криптография — поддержка уже есть в .NET 10.
• Удаление cgroup v1. Переход на cgroup v2 в .NET сделали несколько лет назад, так что сломаться ничего не должно.
Если переходите на Ubuntu 26.04 в продакшне, то всё основное готово с первого дня.
📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9❤1😁1
🛠 От горизонтальных слоёв к вертикальным срезам
Структура
Чтобы разобраться в одной фиче, приходится прыгать между тремя папками. Когда фича сложная код размазан по десятку файлов, и никто не держит в голове полную картину
Проблема: классические слои
Фича в типичном проекте выглядит так:
Каждый из файлов знает только о своём слое. Хотите понять одну операцию — обходите весь проект.
Вертикальные срезы
Принцип простой: код, который меняется вместе живёт вместе.
Вместо горизонтальных слоёв на весь проект вертикальные срезы: один срез на одну фичу. Всё нужное в одной папке:
Один файл содержит команду, валидацию и обработчик:
Пример использует MediatR для pipeline и Carter для регистрации. Они удобны, но не обязательны. Главное — положение кода
Что получаем
Высокая связность. Новый разработчик открывает одну папку и видит фичу целиком — от входа до выхода.
Низкое зацепление. Изменения в одном срезе не затрагивают другие. Никакого страха "а вдруг сломается что-то в другом месте".
Конец раздутым сервисам.
А что с дублированием
Между срезами иногда появляется похожий код. Это нормально.
Цена небольшого дублирования несравнимо меньше, чем цена высокого зацепления и низкой связности, которые приносят горизонтальные слои. Долгосрочно это огромная победа.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
Структура
Controllers / Services / Models настолько привычна, что мы перестали её замечать. Но она главный источник боли по мере роста проекта.Чтобы разобраться в одной фиче, приходится прыгать между тремя папками. Когда фича сложная код размазан по десятку файлов, и никто не держит в голове полную картину
Проблема: классические слои
Фича в типичном проекте выглядит так:
Controllers/ProductsController.cs
Services/IProductService.cs
Services/ProductService.cs
Models/CreateProductRequest.cs
Каждый из файлов знает только о своём слое. Хотите понять одну операцию — обходите весь проект.
Вертикальные срезы
Принцип простой: код, который меняется вместе живёт вместе.
Вместо горизонтальных слоёв на весь проект вертикальные срезы: один срез на одну фичу. Всё нужное в одной папке:
Features/
Products/
CreateProduct/
CreateProduct.cs ← всё здесь
Один файл содержит команду, валидацию и обработчик:
public static class CreateProduct
{
// Входные данные
public record Command(string Name, decimal Price) : IRequest<Guid>;
// Валидация
public class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Price).GreaterThan(0);
}
}
// Логика
internal class Handler(AppDbContext db) : IRequestHandler<Command, Guid>
{
public async Task<Guid> Handle(Command req, CancellationToken ct)
{
var product = new Product { Name = req.Name, Price = req.Price };
db.Products.Add(product);
await db.SaveChangesAsync(ct);
return product.Id;
}
}
}
Пример использует MediatR для pipeline и Carter для регистрации. Они удобны, но не обязательны. Главное — положение кода
Что получаем
Высокая связность. Новый разработчик открывает одну папку и видит фичу целиком — от входа до выхода.
Низкое зацепление. Изменения в одном срезе не затрагивают другие. Никакого страха "а вдруг сломается что-то в другом месте".
Конец раздутым сервисам.
IProductService неизбежно превращается в класс на тысячу строк с двадцатью ответственностями. Handler делает ровно одно дело.А что с дублированием
Между срезами иногда появляется похожий код. Это нормально.
Цена небольшого дублирования несравнимо меньше, чем цена высокого зацепления и низкой связности, которые приносят горизонтальные слои. Долгосрочно это огромная победа.
📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🤔6😁2❤1😢1
🔄 Мелкий релиз с нужными правками API
Вышел релиз v6.2.0 библиотеки OrionGuard для валидации в .NET. Версия небольшая, но устраняет несколько неудобств, которые вылезли при написании демо к v6.1.0.
Что добавили
Появился интерфейс
Добавили абстрактный record
Сгенерированные struct-идентификаторы теперь реализуют
Source-генератор стал умнее: он проверяет во время компиляции, подключён ли EF Core, и генерирует
Что изменили
Все sub-пакеты переименованы — префикс
И так далее для
C#-пространства имён и структура папок не изменились. Существующие
Миграция с v6.1.0
В
➡️ Источник
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
Вышел релиз v6.2.0 библиотеки OrionGuard для валидации в .NET. Версия небольшая, но устраняет несколько неудобств, которые вылезли при написании демо к v6.1.0.
Что добавили
Появился интерфейс
IStronglyTypedId<TValue> — теперь strongly-typed ID на основе record и ID, сгенерированные source-генератором через struct, работают под одним контрактом. Guards и DI-хелперы принимают оба варианта без дополнительного кода.Добавили абстрактный record
DomainEventBase. Раньше в каждом доменном событии нужно было вручную объявлять EventId и OccurredOnUtc. Теперь достаточно унаследоваться:public sealed record OrderShippedEvent(OrderId OrderId) : DomainEventBase;
Сгенерированные struct-идентификаторы теперь реализуют
IParsable<TSelf> и ISpanParsable<TSelf>. Это значит, что route-биндинг в ASP.NET Core minimal API работает из коробки без кастомных конверторов.Source-генератор стал умнее: он проверяет во время компиляции, подключён ли EF Core, и генерирует
ValueConverter<,> только если да. Лишний код в проект не попадает.Что изменили
Все sub-пакеты переименованы — префикс
Moongazing. убрали из NuGet PackageId:Moongazing.OrionGuard.AspNetCore → OrionGuard.AspNetCoreMoongazing.OrionGuard.MediatR → OrionGuard.MediatRMoongazing.OrionGuard.Generators → OrionGuard.GeneratorsИ так далее для
Swagger, OpenTelemetry, Blazor, Grpc, SignalR.C#-пространства имён и структура папок не изменились. Существующие
using-директивы продолжают работать.Миграция с v6.1.0
В
.csproj нужно обновить ссылки на пакеты: заменить Moongazing.OrionGuard.X на OrionGuard.X. Код трогать не нужно. Если хочется убрать бойлерплейт в доменных событиях — можно добавить : DomainEventBase, но это опционально.📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3😁1
🔥 База по ИИ-агентам от научного сотрудника Сколтеха и НИУ ВШЭ
Знакомьтесь, Екатерина Трофимова. Кандидат компьютерных наук, ресерчер в Центре ИИ Сколтеха и лаборатории LAMBDA. Она объединяет глубокую академическую экспертизу и практику: знает, как ИИ-системы устроены «под капотом» и как встроить их в реальные проекты (в т.ч. для Т-банка).
Мы попросили Екатерину собрать список мастхев материалов для тех, кто хочет проектировать агентов в проде. Сохраняйте список.
🛠 Стек и фреймворки:
DSPy — алгоритмическая оптимизация промптов (вместо ручного подбора слов).
Semantic Kernel и LangMem — инструменты для управления сессионной и долгосрочной памятью.
MCP (Model Context Protocol) — новый стандарт от Anthropic для подключения агентов к вашим БД и локальным файлам.
📖 Документация, которую нужно знать:
Anthropic Prompt Caching — как кэшировать контекст и радикально резать косты на API.
OpenAI Agents SDK / Cookbook — лучшие практики работы с памятью.
Augment — платформа для оптимизации работы ИИ-агентов и контроля токенов.
🔬 Хардкорные статьи и препринты (на выходные):
Lost in the Middle — почему LLM «слепнут» на длинных текстах и забывают середину контекста.
How Do Coding Agents Spend Your Money? — куда улетает бюджет при работе автономных кодинг-агентов.
MemGPT — архитектура операционной системы для LLM с иллюзией бесконечной памяти.
InjecAgent / AgentSentry — всё о безопасности и защите агентов от инъекций в промпты.
Екатерина Трофимова — один из ключевых экспертов нашего курса AgentOps. На своих лекциях она детально разбирает, как проектировать инструменты для агентов, как агент принимает решения о вызове инструментов и какие ограничения возникают в реальном проде
🎁 Акция в честь старта продаж!
Прямо сейчас при покупке Инженерного трека вы получаете полный доступ к материалам курса «Разработка ИИ-агентов» в подарок.
👉 Забрать 2 курса по цене 1 и начать обучение
Знакомьтесь, Екатерина Трофимова. Кандидат компьютерных наук, ресерчер в Центре ИИ Сколтеха и лаборатории LAMBDA. Она объединяет глубокую академическую экспертизу и практику: знает, как ИИ-системы устроены «под капотом» и как встроить их в реальные проекты (в т.ч. для Т-банка).
Мы попросили Екатерину собрать список мастхев материалов для тех, кто хочет проектировать агентов в проде. Сохраняйте список.
🛠 Стек и фреймворки:
DSPy — алгоритмическая оптимизация промптов (вместо ручного подбора слов).
Semantic Kernel и LangMem — инструменты для управления сессионной и долгосрочной памятью.
MCP (Model Context Protocol) — новый стандарт от Anthropic для подключения агентов к вашим БД и локальным файлам.
📖 Документация, которую нужно знать:
Anthropic Prompt Caching — как кэшировать контекст и радикально резать косты на API.
OpenAI Agents SDK / Cookbook — лучшие практики работы с памятью.
Augment — платформа для оптимизации работы ИИ-агентов и контроля токенов.
🔬 Хардкорные статьи и препринты (на выходные):
Lost in the Middle — почему LLM «слепнут» на длинных текстах и забывают середину контекста.
How Do Coding Agents Spend Your Money? — куда улетает бюджет при работе автономных кодинг-агентов.
MemGPT — архитектура операционной системы для LLM с иллюзией бесконечной памяти.
InjecAgent / AgentSentry — всё о безопасности и защите агентов от инъекций в промпты.
Екатерина Трофимова — один из ключевых экспертов нашего курса AgentOps. На своих лекциях она детально разбирает, как проектировать инструменты для агентов, как агент принимает решения о вызове инструментов и какие ограничения возникают в реальном проде
🎁 Акция в честь старта продаж!
Прямо сейчас при покупке Инженерного трека вы получаете полный доступ к материалам курса «Разработка ИИ-агентов» в подарок.
👉 Забрать 2 курса по цене 1 и начать обучение
❤3😁2
Кого смотрите и читаете по C# и .NET? Авторы на YouTube, Хабре, телеграм-каналы, подкасты — пишите в комменты 💬
Мы, естественно, следим за Ником Чапсасом, но может есть менее гигантские медиа-личности?
📍 Навигация: Вакансии • Задачи • Собесы
#entry_point
Please open Telegram to view this post
VIEW IN TELEGRAM
❤🔥3😁3
Я пойду работать в понедельник, ведь в выходные..
— Внеплановое обновление .NET 10.0.7
— ИИ найдёт слабые места в вашем резюме
— Группировка меток в картах MAUI
— Sudo for Windows
— Native AOT в Node.js
📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
😁3
Частая ошибка в .NET-проектах — использовать одни и те же модели на всех слоях. Контроллер принял
CreateUserRequest, передал его прямо в сервис, сервис вернул UserResponse обратно в контроллер. Выглядит просто. Но это ловушка.REST DTO это контракт с клиентом. Сервисный слой это бизнес-логика. Это разные ответственности.
Пример плохого кода:
// REST DTO
public class CreateUserRequest
{
public string Email { get; set; }
public string Password { get; set; }
public string Role { get; set; } // "admin", "user"
}
// Контроллер передаёт REST DTO прямо в сервис
[HttpPost]
public async Task<IActionResult> Create(CreateUserRequest request)
{
var user = await _userService.CreateAsync(request); // не надо
return Ok(user);
}
// Сервис знает о REST DTO
public async Task<UserResponse> CreateAsync(CreateUserRequest request)
{
// бизнес-логика завязана на HTTP-контракт
}
Теперь представьте, что появился второй клиент. Он шлёт другой формат. Или вы хотите вызвать
CreateAsync из фоновой задачи, где нет никакого HTTP-запроса. Приходится либо тащить ненужный DTO, либо переписывать сервис.Ещё хуже поле
Role в CreateUserRequest. Клиент сам указывает, кем хочет стать. Даже если оно не используется или перезаписывается далее, выглядит это не очень.Как правильно
Каждый слой работает со своей моделью. Контроллер маппит входящий DTO в команду или модель сервисного слоя. Сервис возвращает доменный результат. Контроллер маппит его в ответный DTO:
// REST DTO — только для HTTP-слоя
public class CreateUserRequest
{
public string Email { get; set; }
public string Password { get; set; }
// Role здесь нет — клиент не решает
}
public class UserResponse
{
public Guid Id { get; set; }
public string Email { get; set; }
public string Role { get; set; }
}
// Сервисная модель — внутренний контракт
public class CreateUserCommand
{
public string Email { get; set; }
public string Password { get; set; }
public UserRole Role { get; set; } // enum, не строка
}
public class UserResult
{
public Guid Id { get; set; }
public string Email { get; set; }
public UserRole Role { get; set; }
}
// Контроллер — маппит и не лезет в логику
[HttpPost]
public async Task<IActionResult> Create(CreateUserRequest request)
{
var command = new CreateUserCommand
{
Email = request.Email,
Password = request.Password,
Role = UserRole.User // роль задаётся здесь, не клиентом
};
var result = await _userService.CreateAsync(command);
var response = new UserResponse
{
Id = result.Id,
Email = result.Email,
Role = result.Role.ToString()
};
return Ok(response);
}
// Сервис — ничего не знает про HTTP
public async Task<UserResult> CreateAsync(CreateUserCommand command)
{
// чистая бизнес-логика
}
Почему это важно
Сервис перестаёт зависеть от формата HTTP-запроса. Его можно вызвать из воркера, gRPC-эндпоинта, теста без изменений.
Контракт с клиентом можно менять независимо от внутренней логики. Добавили поле в
CreateUserRequest, сервис об этом не знает.Валидация и маппинг живут в одном месте — в контроллере. Сервис получает уже проверенные данные в нужном формате.
Для маппинга удобно использовать
AutoMapper или Mapster, но даже ручной маппинг лучше, чем слитые слои.Граница между HTTP-слоем и бизнес-логикой — это не формальность. Это то, что позволяет менять одно, не трогая другое.
📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12👍11😁2🥱1
HeadHunter планомерно вводит верификацию соискателей через государственные базы данных. Резюме без подтверждённого опыта будут скрываться алгоритмами, а аккаунты с расхождениями между резюме и трудовой уходят в теневой бан.
Сильнее всего это бьёт по IT: здесь много фриланса, совмещений и серых периодов.
📍 Навигация: Вакансии • Задачи • Собесы
Please open Telegram to view this post
VIEW IN TELEGRAM
😁6❤4👍4😢2🤔1
Смотрите, какую годноту нашли. Заказать вряд ли получится, но вдохновиться — вполне.
Это лимитированные ремни, но есть ещё кое-что более лимитированное — места на нашем курсе по разработке ИИ агентов! До 30 апреля осталось всего 4 места.
👉 Занять место по ссылке
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
Это лимитированные ремни, но есть ещё кое-что более лимитированное — места на нашем курсе по разработке ИИ агентов! До 30 апреля осталось всего 4 места.
👉 Занять место по ссылке
📍 Навигация: Вакансии • Задачи • Собесы
#garbage_collector
Please open Telegram to view this post
VIEW IN TELEGRAM
😁3👍1
🗓️ Уже через пару часов стартует вебинар!
Тема:
Ждем вас сегодня в 19:00 по московскому времени. Не пропустите начало, будет много практики!
👉 Успей занять место
Тема:
Как эффективно управлять контекстным окном LLM в мультиагентных системах и не сливать бюджет на токены
Ждем вас сегодня в 19:00 по московскому времени. Не пропустите начало, будет много практики!
👉 Успей занять место
🥱2
⏳ Урок истории
До .NET 4.5 асинхронный код писали вручную через обратные вызовы и явный маршалинг между потоками. Это было сложно, многословно и доступно только опытным разработчикам. C# 5 и Visual Basic с новым синтаксисом
Что происходит внутри
Компилятор превращает каждый
После компиляции это превращается примерно в:
Плюс отдельная
Когда async лишний
Если метод всегда выполняется синхронно, оборачивать его в
Решение: возвращать кешированный результат вручную.
Это позволяет при повторяющихся вызовах с одним и тем же результатом не создавать новые объекты совсем.
SynchronizationContext и лишние переходы
По умолчанию
Если из UI-потока запустить цикл копирования с
Без
Локальные переменные и сбор мусора
Компилятор поднимает все локальные переменные асинхронного метода в поля конечного автомата, который упаковывается в кучу при первом реальном ожидании. На момент выхода статьи компиляторы поднимали иногда больше переменных, чем нужно: даже те, что после
Чем больше объектов создаётся, тем чаще срабатывает сборщик мусора. Это влияет на всю систему, а не только на конкретный метод.
Меньше await — лучше
Каждое
➡️ Блог разработчиков
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
До .NET 4.5 асинхронный код писали вручную через обратные вызовы и явный маршалинг между потоками. Это было сложно, многословно и доступно только опытным разработчикам. C# 5 и Visual Basic с новым синтаксисом
async/await убрали эту сложность.Что происходит внутри
Компилятор превращает каждый
async-метод в конечный автомат. Вот простой пример:public static async Task SimpleBodyAsync() {
Console.WriteLine("Hello, Async World!");
}После компиляции это превращается примерно в:
public static Task SimpleBodyAsync() {
var d = new <SimpleBodyAsync>d__0();
d.<>t__builder = AsyncTaskMethodBuilder.Create();
d.MoveNext();
return d.<>t__builder.Task;
}Плюс отдельная
struct с методом MoveNext, блоком try/catch и полями для хранения состояния. JIT не сможет встроить такой метод по месту вызова. Появляются издержки на вызов методов инфраструктуры SetResult, SetException и запись в поля конечного автомата.Когда async лишний
Если метод всегда выполняется синхронно, оборачивать его в
async нет смысла. Тауб приводит пример MemoryStream.ReadAsync: чтение из памяти и без того быстрое, и каждый вызов будет создавать новый объект Task<int> просто чтобы вернуть число. Решение: возвращать кешированный результат вручную.
private Task<int> m_lastTask;
public override Task<int> ReadAsync(
byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
int numRead = this.Read(buffer, offset, count);
return m_lastTask != null && numRead == m_lastTask.Result
? m_lastTask
: (m_lastTask = Task.FromResult(numRead));
}
Это позволяет при повторяющихся вызовах с одним и тем же результатом не создавать новые объекты совсем.
SynchronizationContext и лишние переходы
По умолчанию
await захватывает текущий SynchronizationContext и возвращает продолжение в него. Для UI-потока это удобно: не нужно вручную делать маршалинг. Но в библиотечном коде это создаёт лишние переходы между потоками.Если из UI-потока запустить цикл копирования с
await на каждой операции чтения и записи мегабайта данных, получится более 500 переходов из фоновых потоков обратно в UI-поток. Чтобы этого избежать, в библиотеках следует использовать ConfigureAwait(false):while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)
.ConfigureAwait(false)) > 0)
{
await destination.WriteAsync(buffer, 0, numRead)
.ConfigureAwait(false);
}
Без
ConfigureAwait(false) в библиотечном коде возможна и взаимоблокировка: если вызывающий код в UI-потоке зовёт t.Wait(), а продолжение пытается вернуться в тот же заблокированный поток, оба будут ждать друг друга бесконечно.Локальные переменные и сбор мусора
Компилятор поднимает все локальные переменные асинхронного метода в поля конечного автомата, который упаковывается в кучу при первом реальном ожидании. На момент выхода статьи компиляторы поднимали иногда больше переменных, чем нужно: даже те, что после
await уже не читались.// Лишнее поле в конечном автомате
public static async Task FooAsync() {
var dto = DateTimeOffset.Now;
var dt = dto.DateTime;
await Task.Yield();
Console.WriteLine(dt);
}
// Лучше так:
public static async Task FooAsync() {
var dt = DateTimeOffset.Now.DateTime;
await Task.Yield();
Console.WriteLine(dt);
}
Чем больше объектов создаётся, тем чаще срабатывает сборщик мусора. Это влияет на всю систему, а не только на конкретный метод.
Меньше await — лучше
Каждое
await-выражение несёт накладные расходы. Если нужно подождать несколько задач, лучше объединить их через Task.WhenAll, чем ждать по одной:// Хуже: три отдельных await
int ra = await a;
int rb = await b;
int rc = await c;
// Лучше: одно await на все три
int[] results = await Task.WhenAll(a, b, c);
async/await упростил жизнь разработчикам, но не отменил необходимость понимать, что происходит внутри. 📍 Навигация: Вакансии • Задачи • Собесы
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8🔥5🤔2⚡1
Когда API растёт, рано или поздно встаёт вопрос: как добавить новые возможности и не сломать тех, кто уже использует старую версию? Стандартный ответ — версионирование. В .NET 10 появился удобный способ совместить версионирование с OpenAPI-документацией без лишнего кода.
Зачем нужно версионирование
Без версионирования любое изменение контракта API потенциально ломает клиентов. Версионирование позволяет выпускать новые версии параллельно со старыми, пока клиенты не перейдут самостоятельно.
Популярные стратегии:
- По URL:
/api/v1/users- По query string:
/api/users?api-version=1.0- По заголовку:
X-API-Version: 1.0Что изменилось в .NET 10
С .NET 9
Microsoft.AspNetCore.OpenApi стал стандартным инструментом для генерации OpenAPI вместо Swashbuckle.AspNetCore. Но удобной интеграции с версионированием не было.В .NET 10 вышел пакет
Asp.Versioning.OpenApi версии 10 — первый, который официально поддерживает и .NET 10, и новую OpenAPI-библиотеку от Microsoft.Как подключить: Minimal APIs
Установите пакеты:
Asp.Versioning.Http@10.0.0
Asp.Versioning.Mvc.ApiExplorer@10.0.0
Asp.Versioning.OpenApi@10.0.0-rc.1
Настройка:
builder.Services.AddApiVersioning()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
})
.AddOpenApi();
app.MapOpenApi().WithDocumentPerVersion();
Регистрация эндпоинтов:
var usersApi = app.NewVersionedApi("Users");
var v1 = usersApi.MapGroup("api/users").HasApiVersion("1.0");
var v2 = usersApi.MapGroup("api/users").HasApiVersion("2.0");
v1.MapGet("", () => TypedResults.Ok(new[]
{
new UserV1(1, "John Doe"),
}));
v2.MapGet("", () => TypedResults.Ok(new[]
{
new UserV2(1, "John Doe", new DateOnly(1990, 1, 1)),
}));После запуска OpenAPI-документы доступны по адресам
/openapi/v1.json и /openapi/v2.json.Как подключить контроллеры
Пакеты:
Asp.Versioning.Mvc@10.0.0
Asp.Versioning.Mvc.ApiExplorer@10.0.0
Asp.Versioning.OpenApi@10.0.0-rc.1
Настройка идентична Minimal APIs, только добавляется
.AddMvc():builder.Services.AddApiVersioning()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
})
.AddMvc()
.AddOpenApi();
Контроллеры с версиями:
[ApiController]
[Route("api/users")]
[ApiVersion("1.0")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public ActionResult<UserV1[]> Get() =>
Ok(new[] { new UserV1(1, "John Doe") });
}
[ApiController]
[Route("api/users")]
[ApiVersion("2.0")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public ActionResult<UserV2[]> Get() =>
Ok(new[] { new UserV2(1, "John Doe", new DateOnly(1990, 1, 1)) });
}
Визуализация: SwaggerUI и Scalar
Оба инструмента умеют показывать версионированные документы. SwaggerUI подключается через
Swashbuckle.AspNetCore.SwaggerUI, Scalar через Scalar.AspNetCore.SwaggerUI:
app.UseSwaggerUI(options =>
{
foreach (var desc in app.DescribeApiVersions().Reverse())
{
options.SwaggerEndpoint(
$"/openapi/{desc.GroupName}.json",
desc.GroupName.ToUpperInvariant());
}
});
Scalar:
app.MapScalarApiReference(options =>
{
var descriptions = app.DescribeApiVersions();
for (var i = 0; i < descriptions.Count; i++)
{
var desc = descriptions[i];
options.AddDocument(desc.GroupName, desc.GroupName,
isDefault: i == descriptions.Count - 1);
}
});
SwaggerUI откроется по
/swagger, Scalar по /scalar.Что изменилось по сравнению с v8
В старой версии
Asp.Versioning.OpenApi v8 нужно было вызывать AddOpenApi() отдельно для каждой версии:// v8
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
Теперь достаточно одного вызова, а
WithDocumentPerVersion() берёт на себя генерацию отдельного документа для каждой версии автоматически.📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍5
Уровень кортизола явно будет пониже, если пройти наш курс по разработке ИИ-агентов. Но нужно успевать, ведь осталось всего пару мест, а набор закроется уже завтра.
🔗 Успеть на обучение
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
🔗 Успеть на обучение
📍 Навигация: Вакансии • Задачи • Собесы
#garbage_collector
Please open Telegram to view this post
VIEW IN TELEGRAM
😁5🤔1
Лямбда-функция в C# это чуть больше, чем кусок кода. Компилятор переписывает ваш код, и если понять как именно, многое встаёт на своё место.
Простой случай
Когда лямбда не захватывает никаких переменных, компилятор просто превращает её в обычный метод:
public class LambdaDemo2
{
private System.Timers.Timer? _timer;
private void HiddenMethodForLambda(
object? sender, System.Timers.ElapsedEventArgs args)
{
Console.WriteLine("Выполнено");
}
public void InitTimer()
{
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += HiddenMethodForLambda;
_timer.Enabled = true;
}
}
Тело лямбды перемещается в отдельный метод. Среда выполнения работает с ним как с обычным методом — никаких особых структур.
Когда лямбда захватывает переменную
Ситуация усложняется, если внутри лямбды используется локальная переменная из внешнего метода:
public void InitTimer()
{
int aVariable = 5;
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += (sender, args) => Console.WriteLine(aVariable);
_timer.Enabled = true;
}
Здесь уже один метод не поможет: нужно где-то хранить
aVariable так, чтобы к ней имели доступ сразу два места — InitTimer и лямбда. В .NET для этого используют классы.Компилятор генерирует вспомогательный класс и переносит туда локальную переменную:
public class LambdaDemo4
{
private System.Timers.Timer? _timer;
private class HiddenClassForLambda
{
public int aVariable;
public void HiddenMethodForLambda(
object? sender, System.Timers.ElapsedEventArgs args)
{
Console.WriteLine(aVariable);
}
}
public void InitTimer()
{
var hiddenObject = new HiddenClassForLambda();
hiddenObject.aVariable = 5;
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += hiddenObject.HiddenMethodForLambda;
_timer.Enabled = true;
}
}
Локальная переменная стала полем класса. Метод
InitTimer и лямбда теперь обращаются к одному объекту hiddenObject.Когда лямбда захватывает переменную, способ доступа к ней меняется. То, что выглядело как работа с локальной переменной, после компиляции становится обращением к полю объекта в куче. Если в одном методе объявить несколько лямбд, захватывающих общие переменные, компилятор поместит их всех в один класс — чтобы они могли использовать одни и те же поля.
Это объясняет, почему захват переменных в лямбдах влияет на выделение памяти и время жизни объектов: захваченная переменная живёт столько, сколько живёт объект-носитель, а не до конца метода.
📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1👍1