DotNet developer blog C# | .Net
803 subscribers
85 photos
36 links
Blog about C# .NET development. Almost all posts are published in english on my linkedin page https://www.linkedin.com/in/yegor-sychev/
Download Telegram
Используйте панель Toolbox в Visual Studio 2022

Панель Toolbox хорошо известна разработчикам, которые работали с WinForms - в ней располагались готовые компоненты.

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

Чтобы открыть панель, перейдите в View → Toolbox или используйте сочетание клавиш Ctrl+Alt+X.

Чтобы добавить сниппет, достаточно выделить нужные строки кода и перенести их в панель с помощью drag-n-drop. Затем, для удобства, можно переименовать сниппет.

После этого вы можете перетаскивать сниппеты (также с помощью drag-n-drop) в нужные участки кода в разных проектах и решениях.
👍12✍1
Архитектура которую я использую в своих проектах

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

При проектировании архитектуры приложения я поставил для себя следующие требования:

— Приложение должно легко покрываться тестами
— Приложение должно легко обновляться, переходить на новые версии .Net
— Код должен быть простым, так его становится легче поддерживать, новые разработчики быстрее втягиваются в процесс
— Легко можем заменить поставщика данных, например с БД на внешний сервис, такое часто происходит в крупных компаниях с множеством команд разработки
— Не использовать библиотеки которые влияют на архитектуру Revo, Marten, MediatR и т. д.

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

Данная архитектура является чистой, используется всего три слоя: Api, Core, Infrastructure, однако не стоит путать с трехзвенной архитектурой, в нашем случае используется принцип инверсии зависимостей. Так например, бизнес-слой (Core) не использует напрямую слой Infrastructure и не ссылается на него как проект.

Пример структуры приложения:
Api.csproj
├── Controllers
│ └── ProductController.cs

Core.csproj
├── RepositoriesContracts
│ └── IProductRepository.cs
├── UseCases
│ └── GetProduct
│ ├── GetProductUseCase.cs
│ └── IGetProductUseCase.cs

Infrastructure.csproj
├── Repositories
│ └── ProductRepository.cs


Использование только трех слоев позволяет избежать путаницы и возможной протечки абстракции в перспективе. Вся бизнес-логика расположена в слое Core.

Как мы помним, основное требование было возможность замены поставщика данных без внесения изменений в бизнес-слой. Это действительно важное требование в наших приложениях, так как источники данных часто меняются. Например, ранее отправляли данные по REST сейчас другая команда просит отправлять эти данные в Kafka или раннее данные получали из БД затем часть этого функционала забрала себе другая команда и выставила REST.

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

Далее для выполнения этого требования модель данных, которую мы получили от источники должна мапится в другую модель - возвращаемый результат IProductRepository. Маппинг должен происходить на уровне Infrastructure, помним что Core не должен иметь зависимость от других проектов, но Infrastructure имеет зависимость от Core т.к. реализует интерфейс IProductRepository.

Такая абстракция также позволяет нам легко писать юнит-тесты, а с началом использования Copilot в нашей команде с этим отлично справляется Github Copilot.

Я использую эту архитектуру на протяжении 3-х лет и она отлично себя зарекомендовала, мы начали применять ее для других проектов в команде.
За это время действительно несколько раз менялись источники данных и это происходило без серьезных изменений в коде проекта. Благодаря простоте архитектуры Copilot отлично генерирует юнит-тесты, которые, в большинстве случаев запускаются с первого раза.

Пост на LinkedIn, Medium
👍18🔥8
Архитектура, которую я использую в проектах. Продолжение

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

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

Эти требования ориентированы на практическое использование. Нет цели строго соответствовать DDD, CQRS, Event Sourcing и т.д.

Почему я против внешних библиотек, которые глубоко проникают в архитектуру

В целом, использование внешних библиотек — это всегда риск. Платные библиотеки могут существенно повысить стоимость поддержки проекта. Автор open source-библиотеки может потерять мотивацию, добавить вредоносный код или сделать проект платным. Конечно, можно сделать форк, но тогда поддержка и обновление лягут на вас. Если библиотека используется только в ограниченной части приложения, это приемлемо, но если она плотно интегрируется в архитектуру — это становится проблемой.
Такие библиотеки обычно используются для сокращения объёма кода, например, за счет рефлексии. Однако если вернуться к изначальным требованиям, такой цели не было. Не всегда "меньше кода" — это лучше, тем более сейчас IDE отлично помогают с автодополнением. Отказавшись от подобных библиотек, мы получаем преимущества: независимость проекта, удобство дебага и простоту навигации по коду.

Почему я использую юзкейсы, а не большие сервисы

В комментариях часто спрашивают, зачем создавать отдельный класс use case для каждого действия, если можно сделать один сервис с несколькими методами. Такой подход действительно не противоречит архитектурным требованиям — и раньше я тоже использовал сервисы. Но со временем сервисы разрастаются и превращаются в классы на 1000+ строк с десятками методов. Внутри появляются приватные методы, которые используются разными публичными, что вроде бы способствует переиспользованию, но на деле сильно увеличивает связность кода. Кроме того, такие сервисы часто имеют десятки зависимостей, что сильно усложняет тестирование.

Можно ли использовать базовые классы для use case или репозиториев?

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

EF и Repository

Существует распространённое мнение, что если мы используем EF, то не нужно создавать дополнительную абстракцию — ведь EF уже абстрагирует работу с БД. Но в этом случае мы получаем сильную зависимость от ORM, хотя можем захотеть заменить часть операций, например, отправлять данные в Kafka вместо сохранения в БД. В моей архитектуре работа с EF происходит только на уровне Infrastructure, внутри реализации репозитория (важно, у меня нет базового репозитория, каждый — независим).

Пост на LinkedIn
🔥17👍3
Как NASA измеряет риски в коде. Уроки цикломатической сложности

После программных сбоев во время испытательного полета Boeing CST-100, Центр инженерной безопасности NASA провел масштабное исследование цикломатической сложности — и его выводы стоит знать каждому разработчику.

Что такое цикломатическая сложность?

Это метрика, которая показывает количество точек принятия решений в коде на основе его графа управления (Control Flow Graph). Проще говоря - сколько разных путей может выбрать программа. Каждый оператор ветвления или проверки добавляет один путь: if , switch, циклы for, while, do…while (когда условие прерывает поток) и т. д.

Главные выводы исследования NASA:

— Для критически важных систем NASA рекомендует держать цикломатическую сложность функции ≤ 15. В индустрии стандарт обычно колеблется от 10 до 20.

— Функцию со сложностью 28 пришлось тестировать 31 час, тогда как сложность 5 заняла меньше часа. Зависимость не линейная, а экспоненциальная!

— Низкая сложность упрощает не только тестирование — она напрямую связана с меньшим числом дефектов.

— Не путать с объемом кода. Количество строк не отражает сложность, можно написать 100 строк "прямолинейного" кода со сложностью 1 и всего 10 строк с несколькими вложенными условиями и куда большей сложностью.

Как посчитать в Visual Studio 2022 (C#)

Правый клик на Solution → Analyze and Code Cleanup → Calculate Code Metrics.
В отчёте для каждого метода будет показана Cyclomatic Complexity вместе с другими метриками.

Полный отчет можно найти по названию — "Cyclomatic Complexity and Basis Path Testing Study, 2020"
🔥14👍3
Какой длины должен быть метод?

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

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

Ниже прямые цитаты из ключевых источников, которые чаще всего всплывают в дискуссиях о размере методов.

Роберт Мартин, «Чистый код»
«Первое правило функций — они должны быть маленькими. Второе правило функций — они должны быть ещё меньше… Желательно, чтобы длина функции не превышала 20 строк»

Стив Макконнел, «Совершенный код»
«…Время от времени реализация сложного алгоритма будет требовать создания более длинного метода, и тогда методу можно будет позволить вырасти до 100–200 строк… Десятилетия исследований говорят о том, что методы такой длины не более подвержены ошибкам, чем методы меньших размеров.»

Android Java Style Guide
«Методы должны быть небольшими и решать конкретную задачу. Если метод превышает 40 строк, подумайте, можно ли разбить его на части, не нарушив структуру программы.»

Я же придерживаюсь рекомендаций статического анализатора Meziontou - не более 60 строк кода. Для меня и моей команды это стало хорошей практикой.
👍6🔥3
Оптимизируем nullable-аннотации с помощью [NotNullWhen(true)]

Если вы используете nullable-аннотации в C#, скорее всего сталкивались с предупреждениями CS8602/CS8603.

Часть этих сообщений действительно помогает ловить потенциальные null-референции, однако в более сложных методах компилятор не всегда способен учесть пользовательские проверки и трактует значения как "возможно null", даже когда они проверены.

Эту проблему могут решить атрибуты из System.Diagnostics.CodeAnalysis, позволяющие формально описать контракты методов.

Ниже показан пример, как [NotNullWhen(true)] устраняет лишние предупреждения в классическом Try*-паттерне.

1. Без использования атрибута, получаем предупреждение
bool TryGetDomain(string? url, out string? domain)
{
if (url is null || !url.Contains("://"))
{
domain = null;
return false;
}

domain = url.Split("://")[1];
return true; // здесь domain гарантированно не null
}

if (TryGetDomain(link, out var d))
{
Console.WriteLine(d.Length); // Предупреждение CS8602 «возможен null»
}


2. С использованием атрибута [NotNullWhen(true)] анализатор понимает что при true, out-параметр не будет null.
bool TryGetDomain(
string? url,
[NotNullWhen(true)] out string? domain)
{
if (url is null || !url.Contains("://"))
{
domain = null;
return false; // при false null допустим
}

domain = url.Split("://")[1];
return true; // при true null невозможен
}

if (TryGetDomain(link, out var d))
{
Console.WriteLine(d.Length); // без варнингов и лишних проверок
}


Зачем это нужн
о

Меньше проверок на null — сокращается объtм кода.
Явные контракты в сигнатуре — упрощается чтение и ревью.
Точнее статический анализ — компилятор концентрируется на реальных ошибках.


Другие атрибуты, которые стоит знат
ь

[MaybeNullWhen(true)
] — метод возвращает true, однако значение может быть null.

[NotNullIfNotNull("param")
] — если входной параметр не null, результат тоже не будет null.

[MemberNotNull("Field")
] — метод гарантирует инициализацию поля или свойства до выхода.

[DoesNotReturnIf(true)
] — метод не возвращает управление при выполнении условия (обычно выбрасывает исключение).

Посты на LinkedIn, Medium
👍15🔥3
Как добавить юнит-тесты в легаси‑проект

Может возникнуть вопрос - зачем писать тесты для старого кода?

Давайте рассмотрим несколько причин:

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

— Быстрая отладка. Вместо настройки БД под определенный кейс, SMTP‑сервера и других сервисов, вы можете протестировать нужный кусок кода отдельно.

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

Подход "Простой объект" (Humble Object)

Чтобы начать писать тесты, не нужна идеальная архитектура. С шаблоном Humble Object:

1. Выносите бизнес‑логику в отдельный класс без внешних зависимостей.
2. Оставляете доступ к базе, отправку email, логирование и прочую инфраструктуру в исходном методе.
3. Пишете тесты только для этого нового простого класса. Остальной код не меняется.

Пример

Вот пример контроллера, который смешивает вычисления с вызовами email, базы данных и логирования. Тестирование такого кода напрямую потребует замокать EF Core, DateTime.Now, SMTP и логгер одновременно

[HttpPost("create")]
public IActionResult Create(OrderDto dto)
{
var customer = _db.Customers.Find(dto.CustomerId);
var now = DateTime.Now;
decimal subtotal = dto.Lines.Sum(l => l.Price * l.Qty);

// Скидка на черную пятницу
if (now.Month == 11 && now.Day >= 25 && now.Day <= 30)
subtotal *= 0.8m;

decimal vat = subtotal * 0.12m;
decimal total = subtotal + vat;

customer.Balance -= total;
_db.SaveChanges();

_smtp.Send("sales@corp", customer.Email,
"Thanks for your order", $"Total = {total:C}");

_log.Write($"{now:u} Order {dto.Id} for {customer.Id} = {total}");

return Ok(new { id = dto.Id, total });
}


Выносим расчет цены в отдельный класс

public class OrderPricer
{
private readonly IClock _clock;
private const decimal VatRate = 0.12m;

// в .NET 8 и выше можно использовать встроенный TimeProvider вместо IClock
public OrderPricer(IClock clock) => _clock = clock;

public decimal CalculateTotal(IEnumerable<OrderLineDto> lines)
{
decimal subtotal = lines.Sum(l => l.Price * l.Qty);
var now = _clock.UtcNow;

// Скидка на черную пятницу
if (now.Month == 11 && now.Day >= 25 && now.Day <= 30)
subtotal *= 0.8m;

decimal vat = subtotal * VatRate;
return subtotal + vat;
}
}


Пишем тесты

public class FixedClock : IClock
{
public DateTime UtcNow { get; init; }
}

public class OrderPricerTests
{
[Fact]
public void CalculatesTotalWithVat_NoDiscount()
{
var clock = new FixedClock { UtcNow = new DateTime(2025, 7, 19) };
var pricer = new OrderPricer(clock);

var total = pricer.CalculateTotal(new[]
{
new OrderLineDto { Price = 100, Qty = 1 }
});

Assert.Equal(112m, total);
}
// остальные тесты
}


Сейчас контроллер просто связывает компоненты

[HttpPost("create")]
public IActionResult Create(OrderDto dto)
{
var customer = _db.Customers.Find(dto.CustomerId);
var pricer = new OrderPricer(_clock);
decimal total = pricer.CalculateTotal(dto.Lines);

customer.Balance -= total;
_db.SaveChanges();

_smtp.Send("sales@corp", customer.Email,
"Thanks for your order", $"Total = {total:C}");
_log.Write($"{_clock.UtcNow:u} Order {dto.Id} for {customer.Id} = {total}");

return Ok(new { id = dto.Id, total });
}


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

Посты на LinkedIn, Medium
🔥7✍6
Как подключить локальный MCP сервер к GitHub Copilot

MCP (Model Context Protocol) описывает, как ИИ-агент взаимодействует с внешними инструментами. Это простой способ встроить ваши процессы в Copilot Chat.

Для этого поста я написал MCP-сервер, который запускается локально (пример на TypeScript. Поддержка .NET уже в превью).
Этот MCP сравнивает c помощью Git две ветки и помогает провести раннее код-ревью прямо в Copilot Chat.

Что сделаем

— Поднимем сервер с тремя функциями: quick_merge_summary, show_merge_diff, show_file_diff.
— Подключим его к Copilot через mcp.json.
— Запустим промпт и получим обзор пулл-реквеста.

Зачем это разработчику

— Подключайте готовые серверы, оборачивайте свои приложения, скрипты в MCP серверы для расширения возможностей Copilot или других ИИ агентов.
— Прокачиваете навыки работы с ИИ сегодня это требование к AI-ready developer.
— Все операции выполняются в привычном интерфейсе Copilot Chat.

Шаги

— Клонируйте репозиторий https://github.com/sigmade/Git-MCP-Server
— Выполните команды ниже
npm install
npm run build
npm run start


— В папке build появится index.js.

— В C:\Users\YourUserName\AppData\Roaming\Code\User создайте mcp.json и укажите путь к сбилженному файлу index.js.

— Откройте любой проект в VS Code с двумя ветками в моем случае это master и new-feature .

— Убедитесь что MCP стал доступен в VS Code, для этого в Copilot Chat в режиме Agent нажмите на кнопку Configure Tools... в выпадающем списке появится список доступных инструментов, найдите секцию MCP Server в ней должен отобразиться наш MCP - simple-merge-review а так же его функции quick_merge_summary, show_merge_diff, show_file_diff.

— В чат введите промпт например -
Из ветки new-feature в master будет создан пулл-реквест. Сделай предварительное код-ревью


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

*Если вы используете Cursor возможно ему придется явно прописать в промпт какой MCP использовать.

Итог

MCP дает разработчику возможность подключать любые скрипты, API или внутренние сервисы к ИИ-агенту и использовать их в Copilot Chat для автоматизации рутины и расширения возможностей для работы с ИИ агентами
🔥5👍4
Чистая архитектура против многослойной

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

Сегодня я хочу подробно остановиться на ключевом отличии многослойной архитектуры от чистой - инверсии зависимостей.

Многослойная архитектура

В многослойной архитектуре API вызывает методы класса Core (UseCase), а Core в свою очередь вызывает методы класса Infrastructure (Repository). Такое разделение уже помогает распределить обязанности, но Core все еще знает о конкретных классах из Infrastructure. Это усложняет изоляцию и тестирование доменной логики.

Чистая архитектура с инверсией зависимостей

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

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

Итог

Инвертируя зависимости, вы делаете свой слой Core чистым и тестируемым. Это ускоряет разработку и снижает риски при изменении требований. Сделайте инверсию зависимостей фундаментальной частью своего инструментария, чтобы обеспечить гибкость и надежность вашего кода по мере его роста. Для более глубокого погружения в тему, рекомендую Архитектурные принципы и Распространенные архитектуры веб-приложений
👍13
Популяризирую C# среди казахстанского IT комьюнити. Ну и кстати подписывайтесь на меня в инстаграме https://www.ddinstagram.com/p/DNpRRw5swpJ https://www.instagram.com/landromad?igsh=MW51ZDM4aG04OGtjdA%3D%3D&utm_source=qr
👍14🔥5
Фича-флаги в .NET с IFeatureManager

IFeatureManager из пакета Microsoft.FeatureManagement управляет фича-флагами в .NET. Он позволяет выбирать нужную ветку логики во время работы приложения.

Где пригодится

— Временное включение или быстрое отключение фичи, например при релизе новой версии или хотфиксе
— Отключение проблемного функционала при сбоях
— A/B-тесты
— Временные сценарии, такие как акции

Как это устроено

Регистрируем Feature Management в DI, проверяем состояние флага через IFeatureManager и задаем значения в appsettings.json. При необходимости добавляем фильтры например, TimeWindow активирует флаг только в заданном интервале времени.

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

Такой подход экономит время, снижает риск ошибок при релизах и дает команде больше контроля над функциональностью прямо в продакшене.
👍17🔥3