Самые частые способы уменьшить размер Docker-образа
Сохраните себе — это реально частый вопрос на собесах.
• Использовать минимальную базу
Брать
• Multi-stage сборка
Собрать всё тяжёлое и зависимости в одном этапе, а в финальный образ скопировать только готовые артефакты.
• Меньше слоёв
Группировать команды в один
• Убирать зависимости для сборки
Компиляторы, менеджеры пакетов и прочий мусор — только на build-стейдже, не в рантайме.
• Чистить кеш пакетного менеджера
Чистить
• .dockerignore
Не тащить лишнее в образ:
• Не копировать всё подряд
Вместо
• Меньше и легче рантайм
Статические бинарники, минимальные рантаймы, без лишних зависимостей.
• Чистить бинарники и ресурсы
Убирать отладочную информацию, неиспользуемые ассеты и т.п.
• Настраивать права сразу
Ставить
👉 @KodBlog
Сохраните себе — это реально частый вопрос на собесах.
• Использовать минимальную базу
Брать
alpine, distroless или slim, а не полноценные образы с целой ОС.• Multi-stage сборка
Собрать всё тяжёлое и зависимости в одном этапе, а в финальный образ скопировать только готовые артефакты.
• Меньше слоёв
Группировать команды в один
RUN, когда это уместно, и не плодить лишние слои.• Убирать зависимости для сборки
Компиляторы, менеджеры пакетов и прочий мусор — только на build-стейдже, не в рантайме.
• Чистить кеш пакетного менеджера
Чистить
apt, yum, apk и прочее в том же слое, где ставятся пакеты.• .dockerignore
Не тащить лишнее в образ:
.git, тесты, доки, локальные конфиги и т.д.• Не копировать всё подряд
Вместо
COPY . . копировать только то, что реально нужно для приложения.• Меньше и легче рантайм
Статические бинарники, минимальные рантаймы, без лишних зависимостей.
• Чистить бинарники и ресурсы
Убирать отладочную информацию, неиспользуемые ассеты и т.п.
• Настраивать права сразу
Ставить
chmod/chown рано, чтобы не плодить лишние слои с изменёнными разрешениями.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6❤3👍1
Уходим от анемичных моделей. Пример DDD-рефакторинга.
Если работали с древней кодовой базой на C#, то наверняка видели анемичную доменную модель. Открываешь какой-нибудь OrderService и ловишь фейспалм: в одном классе и ценообразование, и скидки, и проверка склада, и работа с БД. Оно вроде крутится, пока продукт маленький, но дальше каждая новая фича превращается в рулетку с регрессиями. Тесты тоже летят к черту, потому что доменная логика спрятана под инфраструктурой.
Классика анемичного домена: сущности как DTO с геттерами и сеттерами, а вся логика в сервисах. Понимать систему тяжело, любое изменение наугад, а поведение расползлось по слоям.
В этой серии мы:
* посмотрим на типичную анемичную реализацию
* найдем скрытые бизнес-правила, которые делают её хрупкой
* шаг за шагом переложим ответственность в агрегат и дадим ему нормальное поведение
* разберем выгоды, чтобы можно было аргументированно защитить изменения в команде
Старт. Божественный OrderService
Ниже довольно жизненный пример. Помимо подсчета итоговой суммы он:
* навешивает VIP-скидку 5 процентов
* падает, если товара нет на складе
* отклоняет заказ, если клиент вышел за кредитный лимит
Что здесь рыхлое
1. Правила размазаны по сервису. Скидки, склад, кредит — всё в кучу. Чтобы понять бизнес-логику, нужно разбирать инфраструктурный код.
2. Жесткая связка. OrderService тащит в себя знание о ценах, остатках и EF Core, хотя цель — просто разместить заказ.
3. Больно тестировать. Для юнит-теста нужно мокать БД, ценообразование, склад и сценарии для VIP и не VIP. Тесты получаются хрупкие и громоздкие.
Наша цель — втащить правила в домен, чтобы приложение занималось оркестрацией, а не бизнес-логикой.
Принципы рефакторинга
1. Инварианты держим рядом с данными. Проверки склада, скидки, кредит — в агрегате Order, а не в сервисе сверху.
2. Слой приложения читался как сценарий. Хочется "разместить заказ", а не "подсчитать сумму, проверить лимит, сохранить в БД".
3. Рефакторим кусками. Маленькие шаги, компилируемые на каждом этапе. Никаких переписываний с нуля.
4. Баланс чистоты и практичности. Меняем только там, где выигрыш в читабельности, безопасности и тестируемости действительно окупает вложения.
👉 @KodBlog
Если работали с древней кодовой базой на C#, то наверняка видели анемичную доменную модель. Открываешь какой-нибудь OrderService и ловишь фейспалм: в одном классе и ценообразование, и скидки, и проверка склада, и работа с БД. Оно вроде крутится, пока продукт маленький, но дальше каждая новая фича превращается в рулетку с регрессиями. Тесты тоже летят к черту, потому что доменная логика спрятана под инфраструктурой.
Классика анемичного домена: сущности как DTO с геттерами и сеттерами, а вся логика в сервисах. Понимать систему тяжело, любое изменение наугад, а поведение расползлось по слоям.
В этой серии мы:
* посмотрим на типичную анемичную реализацию
* найдем скрытые бизнес-правила, которые делают её хрупкой
* шаг за шагом переложим ответственность в агрегат и дадим ему нормальное поведение
* разберем выгоды, чтобы можно было аргументированно защитить изменения в команде
Старт. Божественный OrderService
Ниже довольно жизненный пример. Помимо подсчета итоговой суммы он:
* навешивает VIP-скидку 5 процентов
* падает, если товара нет на складе
* отклоняет заказ, если клиент вышел за кредитный лимит
// OrderService.cs
public void PlaceOrder(
Guid customerId,
IEnumerable<OrderItemDto> items)
{
var customer = _db.Customers.Find(customerId);
if (customer is null)
throw new ArgumentException("Клиент не найден");
var order = new Order { CustomerId = customerId };
foreach (var dto in items)
{
var inventory = _invService.GetStock(dto.ProductId);
if (inventory < dto.Quantity)
throw new InvalidOperationException("Товара недостаточно");
var price = _pricingService.GetPrice(dto.ProductId);
var lineTotal = price * dto.Quantity;
if (customer.IsVip)
lineTotal *= 0.95m;
order.Items.Add(new OrderItem
{
ProductId = dto.ProductId,
Quantity = dto.Quantity,
UnitPrice = price,
LineTotal = lineTotal
});
}
order.Total = order.Items.Sum(i => i.LineTotal);
if (customer.CreditUsed + order.Total > customer.CreditLimit)
throw new InvalidOperationException("Кредитный лимит превышен");
_db.Orders.Add(order);
_db.SaveChanges();
}
Что здесь рыхлое
1. Правила размазаны по сервису. Скидки, склад, кредит — всё в кучу. Чтобы понять бизнес-логику, нужно разбирать инфраструктурный код.
2. Жесткая связка. OrderService тащит в себя знание о ценах, остатках и EF Core, хотя цель — просто разместить заказ.
3. Больно тестировать. Для юнит-теста нужно мокать БД, ценообразование, склад и сценарии для VIP и не VIP. Тесты получаются хрупкие и громоздкие.
Наша цель — втащить правила в домен, чтобы приложение занималось оркестрацией, а не бизнес-логикой.
Принципы рефакторинга
1. Инварианты держим рядом с данными. Проверки склада, скидки, кредит — в агрегате Order, а не в сервисе сверху.
2. Слой приложения читался как сценарий. Хочется "разместить заказ", а не "подсчитать сумму, проверить лимит, сохранить в БД".
3. Рефакторим кусками. Маленькие шаги, компилируемые на каждом этапе. Никаких переписываний с нуля.
4. Баланс чистоты и практичности. Меняем только там, где выигрыш в читабельности, безопасности и тестируемости действительно окупает вложения.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12
C# Portal | Программирование
Уходим от анемичных моделей. Пример DDD-рефакторинга. Если работали с древней кодовой базой на C#, то наверняка видели анемичную доменную модель. Открываешь какой-нибудь OrderService и ловишь фейспалм: в одном классе и ценообразование, и скидки, и проверка…
Пример DDD-рефакторинга.
Пошаговый рефакторинг
Цель здесь не в догонялках за академической чистотой или «идеальным» DDD. Идея в том, чтобы постепенно усилить связность и дать домену пространство для нормального выражения своей логики. На каждом шаге задаём простой вопрос: это поведение должно жить в домене? Если да — переносим его туда.
Внедрение логики создания и проверки
Первый шаг — заставить агрегат сам собирать себя. Статический метод Create становится единой точкой входа, где инварианты сразу проверяются по принципу fail fast.
Проверка остатков прямо в Order делает код тестируемее, но жёстко привязывает поток создания заказа к доступности товара. Не всегда это хорошо: в некоторых доменах правильнее оформлять это через доменное событие и проверять асинхронно.
Зачем это всё?
Теперь заказ сразу падает с ошибкой, если нарушен какой-то инвариант. Сервис больше не занимается микроменеджментом скидок или склада — всё это живёт внутри домена.
Обратите внимание, как мы теперь следуем принципу «Скажи, не спрашивай». Вместо того, чтобы сервис проверял условия и затем манипулировал заказом, мы говорим заказу создать себя с необходимыми встроенными проверками. Это фундаментальный сдвиг в сторону инкапсуляции.
Замечание о передаче сервисов в методы домена
Передача зависимостей вроде IPricingService или IInventoryService в доменный метод Order.Create может выглядеть нестандартно, но это осмысленное решение. Так мы оставляем оркестрацию в домене, а не превращаем app-сервис в procedural god-object.
Зависимости передаются явно, без скрытых контейнеров внутри сущности. Подход пушка, но применять его стоит там, где операция реально принадлежит домену и выигрывает от прямого доступа к сервисам.
👉 @KodBlog
Пошаговый рефакторинг
Цель здесь не в догонялках за академической чистотой или «идеальным» DDD. Идея в том, чтобы постепенно усилить связность и дать домену пространство для нормального выражения своей логики. На каждом шаге задаём простой вопрос: это поведение должно жить в домене? Если да — переносим его туда.
Внедрение логики создания и проверки
Первый шаг — заставить агрегат сам собирать себя. Статический метод Create становится единой точкой входа, где инварианты сразу проверяются по принципу fail fast.
Проверка остатков прямо в Order делает код тестируемее, но жёстко привязывает поток создания заказа к доступности товара. Не всегда это хорошо: в некоторых доменах правильнее оформлять это через доменное событие и проверять асинхронно.
// Order.cs (фабричный метод)
public static Order Create(
Customer customer,
IEnumerable<(Guid productId, int quantity)> lines,
IPricingService pricingService,
IInventoryService invService)
{
var order = new Order(customer.Id);
foreach (var (id, quantity) in lines)
{
if (invService.GetStock(id) < quantity)
throw new InvalidOperationException("Товара недостаточно");
var price = pricingService.GetPrice(id);
order.AddItem(id, quantity, price, customer.IsVip);
}
order.EnsureCreditWithinLimit(customer);
return order;
}
Зачем это всё?
Теперь заказ сразу падает с ошибкой, если нарушен какой-то инвариант. Сервис больше не занимается микроменеджментом скидок или склада — всё это живёт внутри домена.
Обратите внимание, как мы теперь следуем принципу «Скажи, не спрашивай». Вместо того, чтобы сервис проверял условия и затем манипулировал заказом, мы говорим заказу создать себя с необходимыми встроенными проверками. Это фундаментальный сдвиг в сторону инкапсуляции.
Замечание о передаче сервисов в методы домена
Передача зависимостей вроде IPricingService или IInventoryService в доменный метод Order.Create может выглядеть нестандартно, но это осмысленное решение. Так мы оставляем оркестрацию в домене, а не превращаем app-сервис в procedural god-object.
Зависимости передаются явно, без скрытых контейнеров внутри сущности. Подход пушка, но применять его стоит там, где операция реально принадлежит домену и выигрывает от прямого доступа к сервисам.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍3🤔1
Принёс вам классный ресурс для изучения паттернов проектирования
На сайте Refactoring Guru собрали примеры для каждого паттерна на таких языках, как C#, Java, Python, PHP, Rust и ещё куча других.
Примеры суперпонятные, с кодом и пояснениями.
👉 @KodBlog
На сайте Refactoring Guru собрали примеры для каждого паттерна на таких языках, как C#, Java, Python, PHP, Rust и ещё куча других.
Примеры суперпонятные, с кодом и пояснениями.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
Тестирование текущего времени с помощью TimeProvider и FakeTimeProvider
В тестах тяжело работать с вещами, которые зависят от конкретных данных. Представьте файловую систему: чтобы тесты корректно отрабатывали, нужно гарантировать, что структура файлов именно такая, как ожидается. С датами и временем та же проблема. Если вы пишете тесты, завязанные на текущую дату, при следующем запуске они просто упадут. Поэтому такие зависимости нужно абстрагировать, чтобы их можно было нормально тестировать.
В этом посте разберем класс TimeProvider, как его использовать и как подменять в тестах.
Раньше: ручной интерфейс
Раньше самый простой способ абстрагироваться от работы с датой и временем был в том, чтобы вручную завести интерфейс или абстрактный класс для доступа к текущему времени:
И его стандартную реализацию, которая возвращает время в UTC:
Либо похожий вариант через абстрактный класс:
Дальше этот класс просто регистрировался в DI-контейнере, и можно было использовать. Минус очевидный: в каждом проекте приходилось писать одно и то же.
Сейчас: класс TimeProvider
Начиная с .NET 8, команда .NET добавила абстрактный класс TimeProvider. Он не только даёт абстракцию над локальным временем, но и предоставляет методы для работы с высокоточными временными метками и часовыми поясами.
Важно: даты возвращаются в виде DateTimeOffset, а не DateTime.
TimeProvider доступен из коробки в консольных приложениях .NET:
Если вы используете dependency injection, его достаточно зарегистрировать как синглтон:
Пример использования:
Тестирование TimeProvider
Для тестов можно использовать NuGet-пакет
С помощью FakeTimeProvider можно вручную задавать текущее UTC-время, локальное время и другие параметры, которые предоставляет TimeProvider:
На самом деле TimeProvider умеет намного больше, чем просто возвращать UTC и локальное время. Подробности можно посмотреть в официальной документации
👉 @KodBlog
В тестах тяжело работать с вещами, которые зависят от конкретных данных. Представьте файловую систему: чтобы тесты корректно отрабатывали, нужно гарантировать, что структура файлов именно такая, как ожидается. С датами и временем та же проблема. Если вы пишете тесты, завязанные на текущую дату, при следующем запуске они просто упадут. Поэтому такие зависимости нужно абстрагировать, чтобы их можно было нормально тестировать.
В этом посте разберем класс TimeProvider, как его использовать и как подменять в тестах.
Раньше: ручной интерфейс
Раньше самый простой способ абстрагироваться от работы с датой и временем был в том, чтобы вручную завести интерфейс или абстрактный класс для доступа к текущему времени:
public interface IDateTimeWrapper
{
DateTime GetCurrentDate();
}
И его стандартную реализацию, которая возвращает время в UTC:
public class DateTimeWrapper : IDateTimeWrapper
{
public DateTime GetCurrentDate()
=> DateTime.UtcNow;
}
Либо похожий вариант через абстрактный класс:
public abstract class DateTimeWrapper
{
public virtual DateTime GetCurrentDate() => DateTime.UctNow;
}
Дальше этот класс просто регистрировался в DI-контейнере, и можно было использовать. Минус очевидный: в каждом проекте приходилось писать одно и то же.
Сейчас: класс TimeProvider
Начиная с .NET 8, команда .NET добавила абстрактный класс TimeProvider. Он не только даёт абстракцию над локальным временем, но и предоставляет методы для работы с высокоточными временными метками и часовыми поясами.
Важно: даты возвращаются в виде DateTimeOffset, а не DateTime.
TimeProvider доступен из коробки в консольных приложениях .NET:
DateTimeOffset utc = TimeProvider.System.GetUtcNow();
Console.WriteLine(utc);
DateTimeOffset local = TimeProvider.System.GetLocalNow();
Console.WriteLine(local);
Если вы используете dependency injection, его достаточно зарегистрировать как синглтон:
builder.Services.AddSingleton(TimeProvider.System);
Пример использования:
public class Vacation(TimeProvider _time)
{
public bool IsVacation
=> _time.GetLocalNow().Month == 8;
}
Тестирование TimeProvider
Для тестов можно использовать NuGet-пакет
Microsoft.Extensions.TimeProvider.Testing, в котором есть класс FakeTimeProvider. Это заглушка для абстрактного TimeProvider.С помощью FakeTimeProvider можно вручную задавать текущее UTC-время, локальное время и другие параметры, которые предоставляет TimeProvider:
[Fact]
public void WhenItsAugust_ShouldReturnTrue()
{
// Arrange
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(
new DateTimeOffset(2025, 8, 14,
22, 24, 12, TimeSpan.Zero));
var sut = new Vacation(fakeTime);
Assert.True(sut.IsVacation);
}
На самом деле TimeProvider умеет намного больше, чем просто возвращать UTC и локальное время. Подробности можно посмотреть в официальной документации
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9❤3
Настоятельно рекомендую фри PDF-книгу:
Algorithms от Jeff Erickson — с кучей наглядных иллюстраций и упражнений.
👉 @KodBlog
Algorithms от Jeff Erickson — с кучей наглядных иллюстраций и упражнений.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😁1
Создание параметризованных тестов в xUnit
xUnit использует атрибуты для объявления тестовых методов.
Атрибут Fact описывает обычный тест, а Theory — параметризованный.
Допустим, у нас есть тест, который проверяет, что email-парсер корректно извлекает домен:
Рассмотрим четыре способа написания параметризованных тестов.
1. InlineData
Самый простой вариант — передать тестовые данные напрямую через атрибут:
В
Минус в том, что при большом количестве кейсов код быстро разрастается. Плюс можно использовать только константные значения параметров.
2. MemberData
В атрибуте
3. ClassData
Тестовые данные загружаются из класса, который реализует
Подход более громоздкий — нужно вручную реализовывать
4. TheoryData
Пример с использованием
Для добавления одного тестового кейса вызывается
👉 @KodBlog
xUnit использует атрибуты для объявления тестовых методов.
Атрибут Fact описывает обычный тест, а Theory — параметризованный.
Допустим, у нас есть тест, который проверяет, что email-парсер корректно извлекает домен:
public void Should_Return_Domain(
string email, string expectedDomain)
{
var parser = new EmailParser();
var domain = parser.GetDomain(email);
Assert.Equal(domain, expectedDomain);
}
Рассмотрим четыре способа написания параметризованных тестов.
1. InlineData
Самый простой вариант — передать тестовые данные напрямую через атрибут:
[Theory]
[InlineData("test@test.com", "test.com")]
[InlineData("user@github.io", "github.io")]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }
В
InlineData мы передаём значения аргументов тестируемого метода и ожидаемый результат. Атрибут можно указывать сколько угодно раз — по одному на каждый тестовый кейс.Минус в том, что при большом количестве кейсов код быстро разрастается. Плюс можно использовать только константные значения параметров.
2. MemberData
MemberData позволяет программно задавать тестовые данные через статическое свойство или метод:[Theory]
[MemberData(nameof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }
public static IEnumerable<object[]>
EmailTestData => new List<object>
{
new object[] { "test@test.com", "test.com" },
new object[] { "user@github.io", "github.io" }
};
В атрибуте
MemberData указывается имя члена. Ограничение здесь одно — свойство или метод должны возвращать IEnumerable<object[]>, из-за чего строгая типизация отсутствует.3. ClassData
ClassData выносит тестовые данные в отдельный класс. Это удобно для структурирования данных и повторного использования.Тестовые данные загружаются из класса, который реализует
IEnumerable<object[]>:[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }
public class EmailTestData : IEnumerable<object[]>
{
public IEnumerable<object[]> GetEnumerator()
{
yield return new object[] {
"test@test.com", "test.com" };
yield return new object[] {
"user@github.io", "github.io" };
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
Подход более громоздкий — нужно вручную реализовывать
IEnumerable. При этом проблема с типобезопасностью всё ещё остаётся.4. TheoryData
TheoryData позволяет хранить тестовые данные с сохранением строгой типизации.Пример с использованием
ClassData (аналогично можно применять и с MemberData, возвращая TheoryData из свойства или метода):[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }
public class EmailTestData :
TheoryData<string, string>
{
public EmailTestData()
{
Add("test@test.com", "test.com");
Add("user@github.io", "github.io");
}
}
TheoryData — это обобщённый класс, в котором явно задаются типы параметров теста.Для добавления одного тестового кейса вызывается
Add, а новые сценарии просто добавляются дополнительными вызовами этого метода.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Это аккуратный и чистый способ работать с Git из терминала
Froggit — минималистичный терминальный UI для Git. Он помогает стейджить файлы, делать коммиты, управлять ветками и смотреть логи без необходимости заучивать сложные команды.
Всё управляется с клавиатуры, работает быстро и без лишних отвлекающих элементов, поэтому проще оставаться в потоке при работе с Git.
Тестим и оцениваем😁
👉 @KodBlog
Froggit — минималистичный терминальный UI для Git. Он помогает стейджить файлы, делать коммиты, управлять ветками и смотреть логи без необходимости заучивать сложные команды.
Всё управляется с клавиатуры, работает быстро и без лишних отвлекающих элементов, поэтому проще оставаться в потоке при работе с Git.
Тестим и оцениваем
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4👏2
Разбираем генераторы исходного кода.
Генераторы исходного кода, появившиеся в C# 9, стали мощным инструментом метапрограммирования в .NET. Они позволяют разработчикам автоматически генерировать дополнительный C#-код. Генераторы выполняются на этапе компиляции: анализируют существующий код и создают новые файлы исходников. Эти файлы затем компилируются вместе с остальной кодовой базой, что даёт возможность динамически расширять проект на основе уже написанного кода.
Генераторы исходного кода можно использовать для разных задач: скафолдинга, валидации, повышения читаемости и упрощения поддержки кода.
В C# 12 генераторы получили дальнейшее развитие. Появилась поддержка более сложных сценариев и выросла производительность за счёт уменьшения шаблонного кода и более эффективных проверок на этапе компиляции.
1. Инкрементные генераторы
Инкрементные генераторы регенерируют код только тогда, когда это действительно нужно. Они кэшируют результаты предыдущих запусков и пересобирают сгенерированный код только при изменении входных данных, что заметно ускоряет сборку.
2. Анализ зависимостей исходного кода
Компилятор точнее определяет, какие части проекта зависят от сгенерированного кода. В результате сборки становятся эффективнее, а количество лишних перекомпиляций сокращается.
3. Улучшенная диагностика
Ошибки и предупреждения можно более точно сообщать прямо через сгенерированный код. Разработчик получает понятную обратную связь во время разработки, что уп shows упрощает отладку и сопровождение генераторов.
4. Улучшения API Roslyn
API Roslyn получил дополнительные возможности для анализа и модификации синтаксического дерева, что позволяет реализовывать более сложную логику генерации кода.
Простой пример генератора кода
1. Настройка проекта
Создадим проект библиотеки и установим необходимые NuGet-пакеты:
2. Генератор кода
Нам понадобится класс, реализующий интерфейс IIncrementalGenerator:
Этот генератор добавляет в проект класс HelloWorld с методом SayHello. Сгенерированный код компилируется вместе с основным проектом, поэтому метод HelloGenerated.HelloWorld.SayHello() можно вызывать напрямую из вашего кода.
3. Интеграция с проектом
Создадим консольное приложение:
Добавим ссылку на проект генератора:
Обратите внимание на два нестандартных атрибута:
OutputItemType="Analyzer" — указывает, что проект должен использоваться как анализатор;
ReferenceOutputAssembly="false" — гарантирует, что целевой проект не будет ссылаться на DLL генератора во время компиляции.
После этого сгенерированный класс HelloWorld станет доступен в консольном проекте.
4. Сборка и запуск
Соберём проект и запустим его. В консоли появится вывод из сгенерированного метода:
В след. посте про чтение кода из файла😌
👉 @KodBlog
Генераторы исходного кода, появившиеся в C# 9, стали мощным инструментом метапрограммирования в .NET. Они позволяют разработчикам автоматически генерировать дополнительный C#-код. Генераторы выполняются на этапе компиляции: анализируют существующий код и создают новые файлы исходников. Эти файлы затем компилируются вместе с остальной кодовой базой, что даёт возможность динамически расширять проект на основе уже написанного кода.
Генераторы исходного кода можно использовать для разных задач: скафолдинга, валидации, повышения читаемости и упрощения поддержки кода.
В C# 12 генераторы получили дальнейшее развитие. Появилась поддержка более сложных сценариев и выросла производительность за счёт уменьшения шаблонного кода и более эффективных проверок на этапе компиляции.
1. Инкрементные генераторы
Инкрементные генераторы регенерируют код только тогда, когда это действительно нужно. Они кэшируют результаты предыдущих запусков и пересобирают сгенерированный код только при изменении входных данных, что заметно ускоряет сборку.
2. Анализ зависимостей исходного кода
Компилятор точнее определяет, какие части проекта зависят от сгенерированного кода. В результате сборки становятся эффективнее, а количество лишних перекомпиляций сокращается.
3. Улучшенная диагностика
Ошибки и предупреждения можно более точно сообщать прямо через сгенерированный код. Разработчик получает понятную обратную связь во время разработки, что уп shows упрощает отладку и сопровождение генераторов.
4. Улучшения API Roslyn
API Roslyn получил дополнительные возможности для анализа и модификации синтаксического дерева, что позволяет реализовывать более сложную логику генерации кода.
Простой пример генератора кода
1. Настройка проекта
Создадим проект библиотеки и установим необходимые NuGet-пакеты:
dotnet new classlib -n MySourceGenerator
cd MySourceGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp
2. Генератор кода
Нам понадобится класс, реализующий интерфейс IIncrementalGenerator:
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
public void Initialize(
IncrementalGeneratorInitializationContext context)
{
var src =
"""
using System;
namespace HelloGenerated;
public static class HelloWorld
{
public static void SayHello()
=> Console.WriteLine("Hello from the generated code!");
}
""";
context.RegisterPostInitializationOutput(
ctx => ctx.AddSource(
"HelloWorldGenerated",
SourceText.From(src, Encoding.UTF8)));
}
}
Этот генератор добавляет в проект класс HelloWorld с методом SayHello. Сгенерированный код компилируется вместе с основным проектом, поэтому метод HelloGenerated.HelloWorld.SayHello() можно вызывать напрямую из вашего кода.
3. Интеграция с проектом
Создадим консольное приложение:
dotnet new console -n UseSourceGenerator
Добавим ссылку на проект генератора:
<ProjectReference
Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
Обратите внимание на два нестандартных атрибута:
OutputItemType="Analyzer" — указывает, что проект должен использоваться как анализатор;
ReferenceOutputAssembly="false" — гарантирует, что целевой проект не будет ссылаться на DLL генератора во время компиляции.
После этого сгенерированный класс HelloWorld станет доступен в консольном проекте.
4. Сборка и запуск
Соберём проект и запустим его. В консоли появится вывод из сгенерированного метода:
// Program.cs
HelloGenerated.HelloWorld.SayHello();
В след. посте про чтение кода из файла
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍1🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Это рай для программистов: репозиторий с сотнями бесплатных API.
Отлично подходит для практики и прокачки навыков программирования
Забираем здесь🥶
👉 @KodBlog
Отлично подходит для практики и прокачки навыков программирования
Забираем здесь
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Знаешь, что в .NET 10 сильно снизили накладные расходы при работе с любыми встроенными коллекциями через интерфейсы (да, не только с массивами)?
Итерация по списку через IList<T> или по словарю через IDictionary<K, V> и т.п. теперь почти не даёт абстрактного оверхеда.
👉 @KodBlog
Итерация по списку через IList<T> или по словарю через IDictionary<K, V> и т.п. теперь почти не даёт абстрактного оверхеда.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯11👍10🔥4❤2
Держи инструмент для запросов к данным, если уже надоели классические клиенты для БД: agx
Внутри есть умный SQL-редактор с подсветкой синтаксиса и помощью LLM при написании запросов, браузер схем для быстрого просмотра структуры данных, а также импорт данных из файлов через drag-and-drop.
Работает как нативное десктопное приложение и при этом поддерживает подключение к удалённым серверам через веб-интерфейс, что удобно под разные сценарии использования.
🛌
👉 @KodBlog
Внутри есть умный SQL-редактор с подсветкой синтаксиса и помощью LLM при написании запросов, браузер схем для быстрого просмотра структуры данных, а также импорт данных из файлов через drag-and-drop.
Работает как нативное десктопное приложение и при этом поддерживает подключение к удалённым серверам через веб-интерфейс, что удобно под разные сценарии использования.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Разбираем Server-Sent Events в ASP.NET Core и .NET 10.
Обновления UI в реальном времени больше не считаются просто желательной фичей. Большинство современных приложений ожидают поток данных в реальном времени от сервера. Долгие годы основным решением в экосистеме .NET был SignalR. Он действительно очень мощный, но для более простых сценариев приятно иметь альтернативы.
В ASP.NET Core 10 появился собственный высокоуровневый API для Server-Sent Events (SSE). Он закрывает разрыв между примитивным HTTP-поллингом и полнодуплексными WebSockets через SignalR.
Зачем?
SignalR — инструмент, который автоматически работает с WebSockets, Long Polling и SSE, предоставляя полнодуплексный (двусторонний) канал связи. Но за это приходится платить: свой протокол, обязательная клиентская библиотека и необходимость в «липких сессиях» или отдельном бэкенде (например, Redis) для масштабирования.
В отличие от SignalR, SSE:
- Однонаправленные - разработаны специально для потоковой передачи данных с сервера на клиент.
- Нативны для HTTP - это стандартный HTTP-запрос с типом содержимого text/event-stream. Никаких пользовательских протоколов.
- Поддерживают автоматическое переподключение - браузеры обрабатывают переподключения напрямую через API EventSource.
- Легковесны - нет тяжёлых клиентских библиотек или сложной логики рукопожатия.
Простейшая SSE-конечная точка
Мы можем использовать новый объект Results.ServerSentEvents, чтобы вернуть поток событий из любого IAsyncEnumerable<T>. Поскольку IAsyncEnumerable представляет поток данных, который может приходить со временем, сервер понимает, что HTTP-соединение нужно держать открытым, а не закрывать его после первого куска данных.
Минимальный пример SSE-эндпоинта, который в реальном времени передаёт события о размещении заказов:
Когда клиент обращается к этой конечной точке:
Сервер отправляет заголовок Content-Type: text/event-stream.
Соединение остаётся открытым и ждёт данные.
Как только приложение пишет новый заказ в канал, IAsyncEnumerable возвращает элемент, и .NET сразу отправляет его в браузер по открытому HTTP-соединению.
Это очень эффективный способ реализовать push-уведомления без накладных расходов, характерных для stateful-протоколов.
В примере используется Channel. В реальном приложении это может быть фоновый сервис, который слушает очередь сообщений (например, RabbitMQ или Azure Service Bus) или поток изменений БД и прокидывает новые события в канал для доставки подключённым клиентам.
В след. посте поговорим про обработку пропущенных событий👍
👉 @KodBlog
Обновления UI в реальном времени больше не считаются просто желательной фичей. Большинство современных приложений ожидают поток данных в реальном времени от сервера. Долгие годы основным решением в экосистеме .NET был SignalR. Он действительно очень мощный, но для более простых сценариев приятно иметь альтернативы.
В ASP.NET Core 10 появился собственный высокоуровневый API для Server-Sent Events (SSE). Он закрывает разрыв между примитивным HTTP-поллингом и полнодуплексными WebSockets через SignalR.
Зачем?
SignalR — инструмент, который автоматически работает с WebSockets, Long Polling и SSE, предоставляя полнодуплексный (двусторонний) канал связи. Но за это приходится платить: свой протокол, обязательная клиентская библиотека и необходимость в «липких сессиях» или отдельном бэкенде (например, Redis) для масштабирования.
В отличие от SignalR, SSE:
- Однонаправленные - разработаны специально для потоковой передачи данных с сервера на клиент.
- Нативны для HTTP - это стандартный HTTP-запрос с типом содержимого text/event-stream. Никаких пользовательских протоколов.
- Поддерживают автоматическое переподключение - браузеры обрабатывают переподключения напрямую через API EventSource.
- Легковесны - нет тяжёлых клиентских библиотек или сложной логики рукопожатия.
Простейшая SSE-конечная точка
Мы можем использовать новый объект Results.ServerSentEvents, чтобы вернуть поток событий из любого IAsyncEnumerable<T>. Поскольку IAsyncEnumerable представляет поток данных, который может приходить со временем, сервер понимает, что HTTP-соединение нужно держать открытым, а не закрывать его после первого куска данных.
Минимальный пример SSE-эндпоинта, который в реальном времени передаёт события о размещении заказов:
app.MapGet("orders/realtime", (
ChannelReader<OrderPlacement> reader,
CancellationToken ct) =>
{
// ReadAllAsync возвращает IAsyncEnumerable
// Results.ServerSentEvents заставляет браузер держать соединение открытым
// Новые данные отправляются клиенту по мере поступления из канала
return Results.ServerSentEvents(
reader.ReadAllAsync(ct),
eventType: "orders");
});Когда клиент обращается к этой конечной точке:
Сервер отправляет заголовок Content-Type: text/event-stream.
Соединение остаётся открытым и ждёт данные.
Как только приложение пишет новый заказ в канал, IAsyncEnumerable возвращает элемент, и .NET сразу отправляет его в браузер по открытому HTTP-соединению.
Это очень эффективный способ реализовать push-уведомления без накладных расходов, характерных для stateful-протоколов.
В примере используется Channel. В реальном приложении это может быть фоновый сервис, который слушает очередь сообщений (например, RabbitMQ или Azure Service Bus) или поток изменений БД и прокидывает новые события в канал для доставки подключённым клиентам.
В след. посте поговорим про обработку пропущенных событий
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14👍6👏2❤1
Выбирай направление:
Промпты, обучение, шпаргалки и полезные ресурсы на каждую тему!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2👎1🥴1
EF Core наконец поправил проблему, вокруг которой мы делали костыли годами.
Фикс завезли в EF 10
В EF Core 10 глобальные query filters переделали в named query filters.
Global query filters в Entity Framework Core — это мощная фича для управления паттернами доступа к данным.
Глобальные query filters — это предикаты LINQ, применяемые к entity-моделям EF Core.
Фильтры автоматически накладываются на все запросы, где участвуют соответствующие сущности. Особенно полезно для multi-tenant приложений или soft delete сценариев.
Апдейт в EF 10 закрыл одно серьёзное ограничение EF Core: теперь можно вешать несколько global query filters на одну сущность.
В реальном приложении нам часто нужно иметь несколько фильтров на одну и ту же сущность, например:
• один для soft delete
• второй для multi-tenancy
👉 @KodBlog
Фикс завезли в EF 10
В EF Core 10 глобальные query filters переделали в named query filters.
Global query filters в Entity Framework Core — это мощная фича для управления паттернами доступа к данным.
Глобальные query filters — это предикаты LINQ, применяемые к entity-моделям EF Core.
Фильтры автоматически накладываются на все запросы, где участвуют соответствующие сущности. Особенно полезно для multi-tenant приложений или soft delete сценариев.
Апдейт в EF 10 закрыл одно серьёзное ограничение EF Core: теперь можно вешать несколько global query filters на одну сущность.
В реальном приложении нам часто нужно иметь несколько фильтров на одну и ту же сущность, например:
• один для soft delete
• второй для multi-tenancy
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤3
Топ-20 NuGet пакетов для .NET разработчиков в 2026
Не надо изобретать колесо
Вот список отличных библиотек, которые я лично использовал и которые могу смело порекомендовать для эффективной backend-разработки на .NET:
👉 @KodBlog
Не надо изобретать колесо
Вот список отличных библиотек, которые я лично использовал и которые могу смело порекомендовать для эффективной backend-разработки на .NET:
1. Entity Framework Core
2. Dapper
3. Serilog
4. FluentValidation
5. Polly
6. Refit
7. Wolverine
8. Dapr
9. Quartz .NET
10. Swagger (Swashbuckle)
11. xUnit
12. Shouldly
13. TestContainers
14. Bogus
15. Moq
16. FluentEmail
17. FastEndpoints
18. HotChocolate GraphQL
19. SignalR
20. UnitsNet
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤7😐2
Обработка пропущенных событий
У простой конечной точки, которую мы делали ранее , есть один минус: у неё нет отказоустойчивости.
Одна из самых неприятных проблем с потоками в реальном времени — обрывы соединения. Пока браузер автоматически переподключится, сервер уже может отправить несколько событий, и они потеряются. Для этого в SSE предусмотрен встроенный механизм — заголовок Last-Event-ID. Когда браузер переподключается, он отправляет этот ID обратно на сервер.
В .NET 10 можно использовать тип SseItem<T> для добавления метаданных к данным (ID и retry-интервалы).
Можно завести простой OrderEventBuffer в памяти, который содержит объекты SseItem<OrderPlacement> и умеет отдавать все элементы после Last-Event-ID, который пришлёт браузер. Так мы можем «переиграть» пропущенные сообщения при переподключении:
Далее фильтрация SSE по пользователю -❤️
👉 @KodBlog
У простой конечной точки, которую мы делали ранее , есть один минус: у неё нет отказоустойчивости.
Одна из самых неприятных проблем с потоками в реальном времени — обрывы соединения. Пока браузер автоматически переподключится, сервер уже может отправить несколько событий, и они потеряются. Для этого в SSE предусмотрен встроенный механизм — заголовок Last-Event-ID. Когда браузер переподключается, он отправляет этот ID обратно на сервер.
В .NET 10 можно использовать тип SseItem<T> для добавления метаданных к данным (ID и retry-интервалы).
Можно завести простой OrderEventBuffer в памяти, который содержит объекты SseItem<OrderPlacement> и умеет отдавать все элементы после Last-Event-ID, который пришлёт браузер. Так мы можем «переиграть» пропущенные сообщения при переподключении:
app.MapGet("orders/realtime/with-replays", (
ChannelReader<OrderPlacement> reader,
OrderEventBuffer buffer,
[FromHeader(Name = "Last-Event-ID")]
string? lastEventId,
CancellationToken ct) =>
{
async IAsyncEnumerable<SseItem<OrderPlacement>>
StreamEvents()
{
// «Доливаем» пропущенные события
if (!string.IsNullOrWhiteSpace(lastEventId))
{
var missed = buffer.GetEventsAfter(lastEventId);
foreach (var m in missed)
yield return m;
}
// Отдаём новые события по мере появления в канале
await foreach (var order in reader.ReadAllAsync(ct))
{
// Буфер назначает уникальный ID
var sseItem = buffer.Add(order);
yield return sseItem;
}
}
return TypedResults.ServerSentEvents(
StreamEvents(), "orders");
});Далее фильтрация SSE по пользователю -
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7
Худший вид бага тот, который взрывается через 3 дня после деплоя.
Это скрытая проблема стандартного Options Pattern в .NET.
Ты биндишь appsettings.json к классу, инжектишь его, всё запускается, ошибок нет. Жизнь вроде хороша.
Но если, например, отсутствует обязательный API Key? Узнаешь об этом только когда пользователь дёрнет нужный сценарий, и приложение упадёт.
И вот тут важно правило — конфиг должен валиться сразу. Fail Fast.
Если конфигурация некорректна, приложение не должно даже стартовать. Точка.
Исправляется это расширением Options Pattern через IValidateOptions.
Вместо простого биндинга ты навешиваешь валидацию:
Определяешь правила: ApiKey не null, RetryCount > 0 и т.п.
1. Регистрируешь валидатор
2. Если конфиг не проходит правила, DI выбрасывает исключение на старте
3. Автор пошёл дальше и прикрутил FluentValidation, чтобы правила выглядели чище и понятнее.
Полная реализация тут
👉 @KodBlog
Это скрытая проблема стандартного Options Pattern в .NET.
Ты биндишь appsettings.json к классу, инжектишь его, всё запускается, ошибок нет. Жизнь вроде хороша.
Но если, например, отсутствует обязательный API Key? Узнаешь об этом только когда пользователь дёрнет нужный сценарий, и приложение упадёт.
И вот тут важно правило — конфиг должен валиться сразу. Fail Fast.
Если конфигурация некорректна, приложение не должно даже стартовать. Точка.
Исправляется это расширением Options Pattern через IValidateOptions.
Вместо простого биндинга ты навешиваешь валидацию:
Определяешь правила: ApiKey не null, RetryCount > 0 и т.п.
1. Регистрируешь валидатор
2. Если конфиг не проходит правила, DI выбрасывает исключение на старте
3. Автор пошёл дальше и прикрутил FluentValidation, чтобы правила выглядели чище и понятнее.
Полная реализация тут
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍4❤2🤯2🤔1