Частая ошибка в .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
Вот сколько общаюсь с разработчиками, постоянно слышу убеждение, что есть какой-то правильный способ писать софт. Все ищут секретную архитектуру, вылизывают паттерны, чтобы хоба и тимлид заплакал от счастья от твоего идеального кода.
но, я собрал 4 привычки адептов «чистого кода», (которые обычно все практикуют)
• Бесконечный рефакторинг рабочего кода.
Кажется, что так ты делаешь продукт лучше. Итог: жестко падаешь в перфекционизм. Переписываешь функцию по три раза, а бизнес ждет релиз. Закрываешь вкладку и в голове абсолютная пустота, время потрачено, а новых фичей ноль.
• Упарывание в сложную архитектуру
Сеньоры на ютубе обещают золотые горы, если внедрить микросервисы куда угодно. Итог: получаешь красивый overengineering-проект для мамы и 0 запущенных продуктов в срок, пока конкуренты клепают MVP на коленке.
• Душные споры на ревью
Неплохо, но как итог: ты пишешь полотна текста и тратишь часы на поиск глупой придирки к стилю, потому что банально фокус сместился с реальной задачи на эго.
• Ручная микро-оптимизация
Классика для тех, кто любит алгоритмы из универа. Итог: убиваешь дни жизни и выжимаешь миллисекунды, хотя бизнесу нужен был просто грязный, но рабочий скрипт еще вчера.
Проблема в том, что ни один из этих путей не дает самого главного - скорости и проверки гипотез. Реальному рынку плевать на твой идеальный код за 3 дня. Бизнес предпочтет код от ИИ-агента за 5 минут, который уже завтра начнет приносить деньги.
Хочешь обкатанный на нас лично и 100х учениках метод, как перестать кодить руками и начать делегировать задачи автономным системам?
P. S. Если интересно еще что-нибудь почитать от меня, то заходите в «Азбуку Айтишника», там я рассказываю об айти-базе, также у меня там есть бесплатный гайд на 15 глав по ии-агентам
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2❤1💯1
Разработчик C# — от 365 000 ₽, удалёнка/гибрид в Москве, Санкт-Петербурге, или Казани
C# Developer (WinForms + SQL) — от 350 000 ₽, офис в Санкт-Петербурге
C#/. NET-разработчик — удалёнка
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2👾1
Автор книг по исходникам Quake и Doom собрал ретро-машину 1997 года. Материнка, HDD — дорого. А вот 384 МБ SDRAM обошлись всего в $60. Почему бы не взять с запасом?
Запустил Quake. Pentium MMX 233MHz выдаёт стабильные 44 fps. Всё отлично, пишет статью, уходит на месяц. Возвращается, запускает тот же бенчмарк и результат: 33 fps.
Перебрал всё: видеокарты, драйверы, переустановка системы. Ничего не помогает. Случайно попробовал вытащить одну планку — 33 fps. Вытащил ещё одну — 44. Вернул обратно и опять 33.
Оказалось, чипсет материнской платы кешировал только ограниченный объём RAM через L2-кеш. Всё, что сверх лимита — работало напрямую, без кеша вообще. А Windows 95 загружается «сверху вниз» по адресному пространству, то есть некешируемая зона задействовалась сразу при старте.
📍 Навигация: Вакансии • Задачи • Собесы
#entry_point
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔5❤3
Начиная с .NET 11 Preview 4 и Visual Studio 18.8, VSTest больше не будет тянуть
Newtonsoft.Json. Вместо него на .NET будет использоваться System.Text.Json, на .NET Framework будет`JSONite`.Почему
Все версии
Newtonsoft.Json ниже 13.0.0 теперь помечены как уязвимые на NuGet.org. Это часть более широкой работы по удалению Newtonsoft.Json из .NET SDK.Что не меняется
Формат сообщений VSTest остаётся прежним. Сериализация идентична независимо от используемой библиотеки. Старые тестовые хосты совместимы с обновлённой платформой и наоборот. Производительность сериализации не ухудшилась.
Кого затронет
Большинство проектов изменений не почувствуют. Проблемы возникнут в трёх случаях.
1. Ошибка сборки — если тестовый проект использует типы
Newtonsoft.Json (JObject, JsonConvert) без явной ссылки на пакет. Раньше пакет «протекал» через VSTest. Теперь нет.Решение — добавить зависимость:
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
2. Ошибка в рантайме — если пакет подключён с
<ExcludeAssets>runtime</ExcludeAssets> и проект рассчитывал на копию из VSTest. Тест упадёт с FileNotFoundException.Решение — убрать
<ExcludeAssets>runtime</ExcludeAssets>.3. Ошибка загрузки адаптера — если тестовый адаптер или data collector использует
Newtonsoft.Json без явной зависимости. Среди известных адаптеров таких случаев пока не обнаружено.Превью-пакеты уже доступны на NuGet как
Microsoft.TestPlatform.* версии 1.0.0-alpha-stj.📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5❤2😁1😢1
System.Text.Json работает из коробки, но умолчания выбирались для удобства, а не безопасности. Разбираем пять настроек, которые стоит менять перед релизом.• Дублирующиеся ключи
{"role":"user","role":"admin"}По умолчанию десериализатор молча возьмёт
admin. Именно это эксплуатировал CVE-2022-25757: шлюз видел одно значение, бэкенд — другое. Фикс: AllowDuplicateProperties = false.• Неизвестные поля
Поля, которых нет в модели, тихо выбрасываются. Переименовали свойство в DTO и забыли обновить клиент — данные исчезают без ошибки. Фикс:
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow.•
required компилятора ≠ required для десериализатораC# 11 следит, чтобы объект нельзя было создать без обязательных полей. Десериализатор работает через рефлексию и обходит этот контроль — запишет
null туда, где поля нет в JSON. Баг всплывёт на три слоя глубже в виде NullReferenceException. Фикс: RespectRequiredConstructorParameters = true.• Регистр не важен — по умолчанию
ASP.NET Core включает
PropertyNameCaseInsensitive = true в веб-дефолтах. IsAdmin, isadmin и ISADMIN — одно и то же. Если рядом есть middleware с проверкой строк по сырому JSON — они уже видят разные вещи. Фикс: PropertyNameCaseInsensitive = false.• Nullable-аннотации игнорируются
string Name говорит, что поле не может быть null. Десериализатор это не соблюдает — запишет null без предупреждений. Фикс: RespectNullableAnnotations = true.В .NET 10 всё это одна строка:
var options = JsonSerializerOptions.Strict;
Для ASP.NET Core пайплайна
JsonOptions.SerializerOptions не заменяется целиком, флаги выставляются вручную:builder.Services.Configure<JsonOptions>(o =>
{
var s = o.SerializerOptions;
s.AllowDuplicateProperties = false;
s.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow;
s.PropertyNameCaseInsensitive = false;
s.RespectNullableAnnotations = true;
s.RespectRequiredConstructorParameters = true;
});
Для прямых вызовов
JsonSerializer.Deserialize вне MVC-пайплайна — передавайте JsonSerializerOptions.Strict напрямую.📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23❤3😁1
🧑💻 SkiaSharp 4.0: движок обновлён, новый сопровождающий, переменные шрифты
Вышел первый превью
Что поменялось в движке
Главное обновление — переход на Skia milestone 147. Это 2,5 года апстрим-изменений, которые достаются автоматически без правки кода.
Качество изображений. Mipmap-шарпенинг включён по умолчанию, уменьшенные изображения стали чётче. Кодеки теперь читают Exif-метаданные и автоматически применяют поворот фото. Большие битмапы, которые не влезают в лимиты текстур GPU, тайлятся автоматически.
Цвет. Поправлены передаточные функции для Rec.709, HLG и PQ. Для тех, кто работает с видео или профессиональной цветокоррекцией, это важно.
Производительность. Незначительный прирост по всем операциям рендеринга. Более заметные улучшения в noise-шейдерах и canvas-операциях.
Безопасность. Обновлены нативные зависимости, включены современные митигации компилятора на всех платформах.
Новые возможности
Переменные шрифты. Полная поддержка OpenType variable fonts через
Палитры цветных шрифтов. Поддержка OpenType CPAL для эмодзи и иконочных шрифтов. Можно переключать палитры или переопределять цвет отдельных глифов.
SKPathBuilder. Новый способ строить пути.
Новые платформы. Добавлены нативные сборки для Linux Bionic и Tizen x64/arm64.
Uno Platform стала сопровождающим
Вместе с релизом объявили, что Uno Platform становится co-maintainer
Для тех, кто зависит от
➡️ Интерактивная галерея с примерами и шейдер-плейграунд.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
Вышел первый превью
SkiaSharp 4.0. Библиотека существует 10 лет и лежит в основе кроссплатформенной 2D-графики в .NET: её используют .NET MAUI, WebAssembly, WinUI 3. Это первый крупный мейджор за долгое время.Что поменялось в движке
Главное обновление — переход на Skia milestone 147. Это 2,5 года апстрим-изменений, которые достаются автоматически без правки кода.
Качество изображений. Mipmap-шарпенинг включён по умолчанию, уменьшенные изображения стали чётче. Кодеки теперь читают Exif-метаданные и автоматически применяют поворот фото. Большие битмапы, которые не влезают в лимиты текстур GPU, тайлятся автоматически.
Цвет. Поправлены передаточные функции для Rec.709, HLG и PQ. Для тех, кто работает с видео или профессиональной цветокоррекцией, это важно.
Производительность. Незначительный прирост по всем операциям рендеринга. Более заметные улучшения в noise-шейдерах и canvas-операциях.
Безопасность. Обновлены нативные зависимости, включены современные митигации компилятора на всех платформах.
Новые возможности
Переменные шрифты. Полная поддержка OpenType variable fonts через
SkiaSharp и HarfBuzz. Можно получить доступные оси, задать их значения и создавать варианты шрифта по весу, ширине, наклону или кастомным осям.Палитры цветных шрифтов. Поддержка OpenType CPAL для эмодзи и иконочных шрифтов. Можно переключать палитры или переопределять цвет отдельных глифов.
SKPathBuilder. Новый способ строить пути.
SKPath теперь иммутабелен, а SKPathBuilder предоставляет привычный API с MoveTo, LineTo, CubicTo и фабриками фигур. Старые методы SKPath сохранены для обратной совместимости.Новые платформы. Добавлены нативные сборки для Linux Bionic и Tizen x64/arm64.
Uno Platform стала сопровождающим
Вместе с релизом объявили, что Uno Platform становится co-maintainer
SkiaSharp. Они используют библиотеку в собственном рендер-пайплайне и уже сделали значимые вклады: обновления движка Skia, полная реализация API переменных шрифтов, фикс краша с typeface на Android API 36, поддержка генератора биндингов на Linux, интерактивная Wasm-галерея.Для тех, кто зависит от
SkiaSharp в продакшне: библиотека теперь поддерживается двумя организациями, что ускорит обновления и тришаж.📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7😁1
Чем SelectMany отличается от Select в LINQ
Оба метода принимают функцию-проекцию. Оба работают с коллекциями. Но результат — принципиально разный.
var data = new List<List<int>>
{
new() { 1, 2, 3 },
new() { 4, 5, 6 }
};
var a = data.Select(x => x); // ???
var b = data.SelectMany(x => x); // ???
Подсказка:
Ответ:
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱15😁3❤1🌚1
🛠 yield return под капотом
Многие используют
Что делает компилятор
Вот простой метод:
Компилятор создаёт отдельный класс с полем
Главное свойство — ленивость
Код выполняется только при вызове
Практически
При загрузке данных из БД
Каждый объект создаётся только при переходе к следующей итерации, а не все сразу в памяти.
При каждом вызове метода создаётся новый объект сгенерированного класса в куче. При частых вызовах это влияет на аллокации.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор
Многие используют
yield return, но не задумываются что происходит внутри. Компилятор перестраивает весь метод в конечный автомат.Что делает компилятор
Вот простой метод:
public IEnumerable<int> GetNumbers()
{
yield return 1;
yield return 2;
yield return 3;
}
Компилятор создаёт отдельный класс с полем
_state. Каждый yield return становится отдельным состоянием в switch:public bool MoveNext()
{
switch (_state)
{
case 0: _state = 1; _current = 1; return true;
case 1: _state = 2; _current = 2; return true;
case 2: _state = 3; _current = 3; return true;
default: return false;
}
}
foreach под капотом вызывает MoveNext() на каждой итерации. Локальные переменные метода становятся полями этого класса — так состояние и сохраняется между вызовами.Главное свойство — ленивость
Код выполняется только при вызове
MoveNext(). Поэтому можно работать с бесконечными последовательностями:public IEnumerable<int> Infinite()
{
int i = 0;
while (true) yield return i++;
}
Infinite().Take(5); // {0, 1, 2, 3, 4} — не зависнет
Практически
При загрузке данных из БД
yield return позволяет начать обработку до того, как все данные загрузятся:public IEnumerable<Order> GetOrders(IDataReader reader)
{
while (reader.Read())
yield return new Order { Id = reader.GetInt32(0) };
}
Каждый объект создаётся только при переходе к следующей итерации, а не все сразу в памяти.
При каждом вызове метода создаётся новый объект сгенерированного класса в куче. При частых вызовах это влияет на аллокации.
async/await работает по той же схеме — это два применения одного паттерна трансформации кода.📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤7🔥1
📉 Кандидатов мало, а собесы становятся сложнее
Казалось бы, в кризис найма логично снижать барьеры. Но Яндекс и Сбер делают ровно наоборот: добавляют этапы, запускают ИИ-скрининг, вводят психологические тесты и проверку культурного фита.
Поиск работы даже для сильных специалистов теперь занимает 6–9 месяцев, а воронка найма растёт.
➡️ Разбираем на цифрах и фактах
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
Казалось бы, в кризис найма логично снижать барьеры. Но Яндекс и Сбер делают ровно наоборот: добавляют этапы, запускают ИИ-скрининг, вводят психологические тесты и проверку культурного фита.
Поиск работы даже для сильных специалистов теперь занимает 6–9 месяцев, а воронка найма растёт.
📍 Навигация: Вакансии • Задачи • Собесы
Please open Telegram to view this post
VIEW IN TELEGRAM
🥰5👏1🌚1
sealed в C#: зачем закрывать классыМодификатор
sealed существует в языке давно, но в современных версиях .NET он приобрёл дополнительный вес за счёт оптимизаций JIT-компилятора.Что делает
sealedsealed запрещает наследование. Если класс помечен как sealed, от него нельзя унаследоваться. Можно применять и к отдельным методам с override, тогда этот метод нельзя переопределить дальше по цепочке.public sealed class PaymentConfiguration
{
public string ApiKey { get; set; }
public int TimeoutInSeconds { get; set; }
}
// Ошибка компиляции: нельзя наследоваться от sealed-типа
public class GatewayX : PaymentConfiguration { }
Почему это влияет на производительность
Когда класс открыт для наследования, JIT не может знать наверняка, какой именно метод будет вызван в рантайме. Поэтому он генерирует косвенный вызов через
vtable.; Открытый класс — несколько обращений к памяти перед вызовом
mov eax, [edx]
mov eax, [eax+0x28]
call dword ptr [eax+0x10]
Для
sealed-класса JIT точно знает, какой метод будет вызван. Он убирает обращение к vtable, встраивает тело метода прямо в место вызова и оставляет минимум инструкций:; Sealed-класс — только проверка на null и возврат
cmp [edx], dl
ret
Дополнительно ускоряются операторы
is и as, потому что среде не нужно проходить по всему дереву наследования.Когда
sealed не подойдётEntity Framework создаёт прокси-классы в рантайме для ленивой загрузки. Если сущность
sealed, фреймворк не сможет её расширить:// EF не сможет создать прокси для sealed-класса
public class Order
{
public int Id { get; set; }
public virtual ICollection<OrderItem> Items { get; set; }
}
Аналогичная проблема с библиотеками для мокирования (Moq, NSubstitute). Они создают наследников в рантайме, и
sealed ломает это:public sealed class IntegrationService
{
public bool SendData(string data) => true;
}
// Moq выбросит исключение: нельзя наследоваться от sealed-типа
var mock = new Mock<IntegrationService>();
Архитекторы рекомендуют подход "sealed by default": закрывать все классы сразу, а наследование открывать только когда оно действительно нужно.
Это защищает дизайн от случайных расширений и даёт прирост производительности без дополнительных усилий.
📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23❤4
Кто куда, а админ на шашлыки.
— hh.ru + Госуслуги + трудовая
— VSTest убирает зависимость от Newtonsoft.Json
— Как настроить JsonSerializerOptions
— SkiaSharp 4.0
📍 Навигация: Вакансии • Задачи • Собесы
#async_news
Please open Telegram to view this post
VIEW IN TELEGRAM
🛠 CQRS: что это и зачем оно вам
Мы часто упоминаем CQRS в контексте архитектуры. Пора разобраться, что за ним стоит и когда его стоит применять.
Что такое CQRS
CQRS расшифровывается как Command Query Responsibility Segregation. Суть простая: операции чтения и записи разделяются на два независимых потока.
В классическом подходе один репозиторий или сервис отвечает и за чтение, и за запись. Это удобно, пока система небольшая. Когда нагрузка растёт или модели чтения и записи начинают расходиться, появляются проблемы.
Какую боль решает CQRS
Представьте интернет-магазин. Команда на оформление заказа
Если один и тот же объект обслуживает оба сценария, вы получаете:
• модель данных, которая пытается угодить всем сразу
• сложные запросы с JOIN там, где нужна простая выборка
• трудности с масштабированием — читать нужно в 10 раз чаще, чем писать
CQRS позволяет разделить эти ответственности явно.
Как это выглядит на практике
Допустим, у нас есть приложение на C# с заказами. Для удобства используем
Сначала определяем интерфейсы:
Команда на создание заказа:
Запрос на получение заказов пользователя:
Вызов из контроллера выглядит одинаково для команд и запросов:
Обратите внимание:
Для чтения можно подключить read-replica или отдельную схему, оптимизированную под конкретные запросы. Запись при этом идёт в основное хранилище, а синхронизация происходит через события.
Когда применять
CQRS оправдан, если:
• нагрузка на чтение и запись сильно различается
• модели чтения и записи расходятся, например, вам нужны агрегированные данные на фронте, но нормализованное хранилище на бэке
• вы строите систему с событийной архитектурой или Event Sourcing
CQRS не нужен, если:
• у вас простой CRUD без сложной логики
• команда небольшая и накладные расходы на поддержку двух моделей не оправданы
Если вы чувствуете, что один репозиторий тащит на себе слишком много, это сигнал задуматься о разделении.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор
Мы часто упоминаем CQRS в контексте архитектуры. Пора разобраться, что за ним стоит и когда его стоит применять.
Что такое CQRS
CQRS расшифровывается как Command Query Responsibility Segregation. Суть простая: операции чтения и записи разделяются на два независимых потока.
Query — читает данные, ничего не меняет.Command — меняет состояние, ничего не возвращает (кроме подтверждения).В классическом подходе один репозиторий или сервис отвечает и за чтение, и за запись. Это удобно, пока система небольшая. Когда нагрузка растёт или модели чтения и записи начинают расходиться, появляются проблемы.
Какую боль решает CQRS
Представьте интернет-магазин. Команда на оформление заказа
PlaceOrderCommand обновляет остатки, создаёт запись заказа и запускает цепочку событий. А запрос на страницу каталога GetProductsQuery просто возвращает список товаров с ценами.Если один и тот же объект обслуживает оба сценария, вы получаете:
• модель данных, которая пытается угодить всем сразу
• сложные запросы с JOIN там, где нужна простая выборка
• трудности с масштабированием — читать нужно в 10 раз чаще, чем писать
CQRS позволяет разделить эти ответственности явно.
Как это выглядит на практике
Допустим, у нас есть приложение на C# с заказами. Для удобства используем
MediatR — библиотеку, которая берёт на себя маршрутизацию команд и запросов.Сначала определяем интерфейсы:
public interface ICommand : IRequest { }
public interface IQuery<TResult> : IRequest<TResult> { }Команда на создание заказа:
public record PlaceOrderCommand(string UserId, string ProductId, int Quantity) : ICommand;
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand>
{
private readonly IOrderRepository _repo;
public PlaceOrderCommandHandler(IOrderRepository repo) => _repo = repo;
public async Task Handle(PlaceOrderCommand command, CancellationToken ct)
{
var order = Order.Create(command.UserId, command.ProductId, command.Quantity);
await _repo.SaveAsync(order, ct);
}
}
Запрос на получение заказов пользователя:
public record GetUserOrdersQuery(string UserId) : IQuery<IReadOnlyList<OrderDto>>;
public class GetUserOrdersQueryHandler : IRequestHandler<GetUserOrdersQuery, IReadOnlyList<OrderDto>>
{
private readonly IOrderReadRepository _readRepo;
public GetUserOrdersQueryHandler(IOrderReadRepository readRepo) => _readRepo = readRepo;
public async Task<IReadOnlyList<OrderDto>> Handle(GetUserOrdersQuery query, CancellationToken ct)
=> await _readRepo.GetByUserIdAsync(query.UserId, ct);
}
Вызов из контроллера выглядит одинаково для команд и запросов:
// Команда
await _mediator.Send(new PlaceOrderCommand(userId, productId, quantity));
// Запрос
var orders = await _mediator.Send(new GetUserOrdersQuery(userId));
Обратите внимание:
PlaceOrderCommandHandler работает с доменной моделью Order, а GetUserOrdersQueryHandler возвращает OrderDto — упрощённую структуру специально для отображения. Это ключевой момент.IOrderRepository и IOrderReadRepository — два разных интерфейса. Первый смотрит в основную базу и знает о доменных правилах. Второй может работать с read-replica или отдельной проекцией данных.Для чтения можно подключить read-replica или отдельную схему, оптимизированную под конкретные запросы. Запись при этом идёт в основное хранилище, а синхронизация происходит через события.
Когда применять
CQRS оправдан, если:
• нагрузка на чтение и запись сильно различается
• модели чтения и записи расходятся, например, вам нужны агрегированные данные на фронте, но нормализованное хранилище на бэке
• вы строите систему с событийной архитектурой или Event Sourcing
CQRS не нужен, если:
• у вас простой CRUD без сложной логики
• команда небольшая и накладные расходы на поддержку двух моделей не оправданы
Если вы чувствуете, что один репозиторий тащит на себе слишком много, это сигнал задуматься о разделении.
📍 Навигация: Вакансии • Задачи • Собесы
#il_люминатор
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍2😁2