День 2415. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Начало
Мы, разработчики, любим абстракции. Репозитории, сервисы, конвертеры, обёртки. Они делают наш код «чистым», обещают тестируемость и дают нам ощущение гибкости. Некоторые абстракции оправдывают себя, изолируя реальную волатильность и защищая систему от изменений. Другие же незаметно увеличивают сложность, замедляют внедрение и скрывают проблемы производительности за слоями косвенности. Рассмотрим, когда абстракции приносят дивиденды, а когда они становятся техническим долгом.
Когда абстракции окупаются
Лучшие абстракции изолируют реальную волатильность — те части вашей системы, которые вы действительно ожидаете изменить. Пример: обработка платежей. Ваша бизнес-логика не должна напрямую зависеть от API или SDK платёжной системы. Если вы когда-нибудь перейдёте на другую, вы не хотите, чтобы это повлияло на множество мест вашей кодовой базы. Здесь абстракция имеет смысл:
Теперь бизнес-логика может сфокусироваться на домене:
Эта абстракция изолирует действительно нестабильную зависимость (платёжного провайдера), сохраняя при этом независимость логики оформления заказа. Когда Stripe поменяет свой API или вы поменяете провайдера, нужно изменить только один класс. Это хорошая абстракция. Она даёт опциональность там, где она действительно нужна.
Когда абстракции становятся техническим долгом
Проблемы возникают, когда мы абстрагируем то, что на самом деле не является изменчивым. В итоге мы оборачиваем стабильные библиотеки или создаём слои, которые не приносят реальной ценности. «Чистый» слой, добавленный вами сегодня, завтра становится обузой для обслуживания.
Большинство команд начинают с чего-то разумного:
Но по мере изменения требований, растёт и интерфейс:
Внезапно репозиторий начинает пропускать логику запросов в свой интерфейс. Каждый новый способ получения пользователей означает новый метод, и «абстракция» становится сборищем всевозможных запросов.
Между тем, Entity Framework уже предоставляет всё это через LINQ: строго типизированные запросы, которые напрямую соответствуют SQL. Вместо того, чтобы использовать эту мощь, вы ввели слой косвенности. Паттерн репозитория имел смысл, когда ORM были незрелыми. Сегодня это часто просто формальность.
Частью взросления разработчика является умение распознавать, когда паттерны становятся антипаттернами. Репозитории имеют смысл, когда они инкапсулируют сложную логику запросов или предоставляют унифицированный API для нескольких источников данных. Но вы должны стремиться, чтобы они были сосредоточены на логике предметной области. Как только они разрастаются в мириады методов для каждого возможного запроса, это признак того, что абстракция дала сбой.
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
Реальная Цена Абстракций в .NET. Начало
Мы, разработчики, любим абстракции. Репозитории, сервисы, конвертеры, обёртки. Они делают наш код «чистым», обещают тестируемость и дают нам ощущение гибкости. Некоторые абстракции оправдывают себя, изолируя реальную волатильность и защищая систему от изменений. Другие же незаметно увеличивают сложность, замедляют внедрение и скрывают проблемы производительности за слоями косвенности. Рассмотрим, когда абстракции приносят дивиденды, а когда они становятся техническим долгом.
Когда абстракции окупаются
Лучшие абстракции изолируют реальную волатильность — те части вашей системы, которые вы действительно ожидаете изменить. Пример: обработка платежей. Ваша бизнес-логика не должна напрямую зависеть от API или SDK платёжной системы. Если вы когда-нибудь перейдёте на другую, вы не хотите, чтобы это повлияло на множество мест вашей кодовой базы. Здесь абстракция имеет смысл:
public interface IPaymentProcessor
{
Task ProcessAsync(
Order order, CancellationToken ct);
}
public class StripePaymentProcessor
: IPaymentProcessor
{
public async Task ProcessAsync(
Order order, CancellationToken ct)
{
// Реализация для Stripe
}
}
Теперь бизнес-логика может сфокусироваться на домене:
public class CheckoutService(
IPaymentProcessor processor)
{
public Task CheckoutAsync(
Order order, CancellationToken ct) =>
processor.ProcessAsync(order, ct);
}
Эта абстракция изолирует действительно нестабильную зависимость (платёжного провайдера), сохраняя при этом независимость логики оформления заказа. Когда Stripe поменяет свой API или вы поменяете провайдера, нужно изменить только один класс. Это хорошая абстракция. Она даёт опциональность там, где она действительно нужна.
Когда абстракции становятся техническим долгом
Проблемы возникают, когда мы абстрагируем то, что на самом деле не является изменчивым. В итоге мы оборачиваем стабильные библиотеки или создаём слои, которые не приносят реальной ценности. «Чистый» слой, добавленный вами сегодня, завтра становится обузой для обслуживания.
Большинство команд начинают с чего-то разумного:
public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
}
Но по мере изменения требований, растёт и интерфейс:
public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
Task<User?> GetByEmailAsync(string email);
Task<IEnumerable<User>> GetActiveUsersAsync();
Task<IEnumerable<User>> GetUsersByRoleAsync(string role);
Task<IEnumerable<User>> SearchAsync(string keyword, int page, int pageSize);
// ...и т.д.
}
Внезапно репозиторий начинает пропускать логику запросов в свой интерфейс. Каждый новый способ получения пользователей означает новый метод, и «абстракция» становится сборищем всевозможных запросов.
Между тем, Entity Framework уже предоставляет всё это через LINQ: строго типизированные запросы, которые напрямую соответствуют SQL. Вместо того, чтобы использовать эту мощь, вы ввели слой косвенности. Паттерн репозитория имел смысл, когда ORM были незрелыми. Сегодня это часто просто формальность.
Частью взросления разработчика является умение распознавать, когда паттерны становятся антипаттернами. Репозитории имеют смысл, когда они инкапсулируют сложную логику запросов или предоставляют унифицированный API для нескольких источников данных. Но вы должны стремиться, чтобы они были сосредоточены на логике предметной области. Как только они разрастаются в мириады методов для каждого возможного запроса, это признак того, что абстракция дала сбой.
Продолжение следует…
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍22
День 2416. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Продолжение
Начало
Обёртки Сервисов: контекст имеет значение
✅ Хороший пример
При интеграции с внешними API обёртка действительно полезна, поскольку централизует задачи:
Эта обёртка изолирует детали API GitHub. При изменении аутентификации или развитии конечных точек вы обновляете одно место. Вашей бизнес-логике не нужно разбираться с HTTP-заголовками, базовыми URL или JSON-сериализацией.
❌ Плохой пример
Проблемы начинаются, когда мы обёртываем наши собственные стабильные сервисы, не добавляя им ценности:
Этот UserService только добавляет косвенности. Всё, что он делает, — перенаправляет вызовы в IUserRepository. Он не обеспечивает соблюдение бизнес-правил, не добавляет валидацию, не реализует кэширование и не предоставляет никакой реальной функциональности. Это слой, существующий только потому, что «сервисы — это хорошая архитектура».
По мере того, как эти атрофированные обёртки множатся, ваша кодовая база превращается в лабиринт. Разработчики тратят время на перемещение по слоям вместо того, чтобы сосредоточиться на том, где на самом деле находится бизнес-логика.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
Реальная Цена Абстракций в .NET. Продолжение
Начало
Обёртки Сервисов: контекст имеет значение
✅ Хороший пример
При интеграции с внешними API обёртка действительно полезна, поскольку централизует задачи:
public interface IGitHubClient
{
Task<UserDto?> GetUserAsync(string username);
Task<IReadOnlyList<RepoDto>>
GetRepositoriesAsync(string username);
}
public class GitHubClient(HttpClient httpClient)
: IGitHubClient
{
public Task<UserDto?> GetUserAsync(string username) =>
httpClient
.GetFromJsonAsync<UserDto>($"/users/{username}");
public Task<IReadOnlyList<RepoDto>>
GetRepositoriesAsync(string username) =>
httpClient
.GetFromJsonAsync<IReadOnlyList<RepoDto>>(
$"/users/{username}/repos");
}
Эта обёртка изолирует детали API GitHub. При изменении аутентификации или развитии конечных точек вы обновляете одно место. Вашей бизнес-логике не нужно разбираться с HTTP-заголовками, базовыми URL или JSON-сериализацией.
❌ Плохой пример
Проблемы начинаются, когда мы обёртываем наши собственные стабильные сервисы, не добавляя им ценности:
public class UserService(IUserRepository userRepository)
{
// Просто перенаправляем вызовы
public Task<User?> GetByIdAsync(Guid id)
=> userRepository.GetByIdAsync(id);
public Task<IEnumerable<User>> GetAllAsync()
=> userRepository.GetAllAsync();
public Task SaveAsync(User user)
=> userRepository.SaveAsync(user);
}
Этот UserService только добавляет косвенности. Всё, что он делает, — перенаправляет вызовы в IUserRepository. Он не обеспечивает соблюдение бизнес-правил, не добавляет валидацию, не реализует кэширование и не предоставляет никакой реальной функциональности. Это слой, существующий только потому, что «сервисы — это хорошая архитектура».
По мере того, как эти атрофированные обёртки множатся, ваша кодовая база превращается в лабиринт. Разработчики тратят время на перемещение по слоям вместо того, чтобы сосредоточиться на том, где на самом деле находится бизнес-логика.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍16👎1
День 2417. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Окончание
Начало
Продолжение
Принимаем более взвешенные решения
Вот как следует понимать, когда абстракции стоят того:
1. Абстрактные политики, а не механизмы
Политики — решения, которые могут измениться: какой платёжный сервис использовать, как обрабатывать кэширование, стратегии повторных попыток для внешних вызовов.
Механизмы — стабильные детали реализации: LINQ в EF Core, конфигурация HttpClient, сериализация JSON.
Абстрагируйте политики, т.к. они обеспечивают гибкость. Не абстрагируйте механизмы — это уже стабильные API, которые редко меняются критически.
2. Дождитесь второй реализации
Если у вас только одна реализация, не поддавайтесь соблазну создать интерфейс. Одна реализация не оправдывает абстрагирование, это преждевременное обобщение, которое добавляет сложности без какой-либо пользы.
Абстракция возникает естественным образом, когда она действительно нужна. Интерфейс раскрывается через реальные требования, а не воображаемые.
3. Реализации внутри, абстракции на границах
Внутри приложения отдавайте предпочтение конкретным типам. Используйте EF напрямую, настраивайте HttpClient как типизированные клиенты, работайте с сущностями домена. Вводите абстракции только там, где система взаимодействует с внешним миром: внешними API, сторонними SDK, инфраструктурными сервисами. Именно там изменения наиболее вероятны, и там абстракции оправдывают себя.
Рефакторинг плохих абстракций
Регулярно задавайте себе вопрос: если я уберу эту абстракцию, код станет проще или сложнее? Если удаление интерфейса или сервисного уровня сделает код более понятным и прямолинейным, эта абстракция, вероятно, стоит больше, чем приносит пользы. Не бойтесь удалять ненужные уровни.
Выявив проблемные абстракции, вот как их безопасно удалить:
1. Определите реальных потребителей. Кому на самом деле нужна абстракция?
2. Встройте интерфейс. Замените абстрактные вызовы конкретными реализациями.
3. Удалите обёртку - ненужные косвенные обращения.
4. Упростите вызывающий код. Воспользуйтесь возможностями конкретного API.
Например, замените репозиторий прямым использованием EF:
Конкретная версия более явно описывает, какие данные она извлекает и как. Если вам нужен один и тот же запрос в нескольких местах, вы можете перенести его в метод расширения, чтобы сделать его общим.
Итого
Абстракции — мощные инструменты для управления сложностью и изменениями, но они не бесплатны. Каждая добавляет косвенные издержки, когнитивные накладные расходы и нагрузку на поддержку. Самая чистая архитектура — та, где каждый слой имеет чёткое и обоснованное назначение.
Прежде чем добавлять следующую абстракцию, спросите себя:
- Я абстрагирую политику или просто механизм?
- У меня две реализации, или я размышляю о будущих потребностях?
- Сделает ли это мою систему более адаптивной или просто более сложной для понимания?
- Если я уберу этот слой, станет ли код проще?
Помните: абстракции — это кредиты, по которым со временем начисляются проценты. Убедитесь, что вы берёте их по правильным причинам, а не просто по привычке.
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
Реальная Цена Абстракций в .NET. Окончание
Начало
Продолжение
Принимаем более взвешенные решения
Вот как следует понимать, когда абстракции стоят того:
1. Абстрактные политики, а не механизмы
Политики — решения, которые могут измениться: какой платёжный сервис использовать, как обрабатывать кэширование, стратегии повторных попыток для внешних вызовов.
Механизмы — стабильные детали реализации: LINQ в EF Core, конфигурация HttpClient, сериализация JSON.
Абстрагируйте политики, т.к. они обеспечивают гибкость. Не абстрагируйте механизмы — это уже стабильные API, которые редко меняются критически.
2. Дождитесь второй реализации
Если у вас только одна реализация, не поддавайтесь соблазну создать интерфейс. Одна реализация не оправдывает абстрагирование, это преждевременное обобщение, которое добавляет сложности без какой-либо пользы.
// 1: Начинаем с конкретного
public class EmailNotifier
{
public async Task SendAsync(
string to, string subject, string body)
{
// Реализация в SMTP
}
}
// 2: Нужны SMS? Теперь абстрагируемся
public interface INotifier
{
Task SendAsync(string to, string subject, string body);
}
public class EmailNotifier : INotifier { … }
public class SmsNotifier : INotifier { … }
Абстракция возникает естественным образом, когда она действительно нужна. Интерфейс раскрывается через реальные требования, а не воображаемые.
3. Реализации внутри, абстракции на границах
Внутри приложения отдавайте предпочтение конкретным типам. Используйте EF напрямую, настраивайте HttpClient как типизированные клиенты, работайте с сущностями домена. Вводите абстракции только там, где система взаимодействует с внешним миром: внешними API, сторонними SDK, инфраструктурными сервисами. Именно там изменения наиболее вероятны, и там абстракции оправдывают себя.
Рефакторинг плохих абстракций
Регулярно задавайте себе вопрос: если я уберу эту абстракцию, код станет проще или сложнее? Если удаление интерфейса или сервисного уровня сделает код более понятным и прямолинейным, эта абстракция, вероятно, стоит больше, чем приносит пользы. Не бойтесь удалять ненужные уровни.
Выявив проблемные абстракции, вот как их безопасно удалить:
1. Определите реальных потребителей. Кому на самом деле нужна абстракция?
2. Встройте интерфейс. Замените абстрактные вызовы конкретными реализациями.
3. Удалите обёртку - ненужные косвенные обращения.
4. Упростите вызывающий код. Воспользуйтесь возможностями конкретного API.
Например, замените репозиторий прямым использованием EF:
// До: Скрыто за репозиторием
var users = await _userRepo
.GetActiveUsersWithRecentOrders();
// После: Прямой запрос
var users = await _context.Users
.Where(u => u.IsActive)
.Where(u => u.Orders.Any(o => o.CreatedAt > DateTime.Now.AddDays(-30)))
.Include(u => u.Orders.Take(5))
.ToListAsync();
Конкретная версия более явно описывает, какие данные она извлекает и как. Если вам нужен один и тот же запрос в нескольких местах, вы можете перенести его в метод расширения, чтобы сделать его общим.
Итого
Абстракции — мощные инструменты для управления сложностью и изменениями, но они не бесплатны. Каждая добавляет косвенные издержки, когнитивные накладные расходы и нагрузку на поддержку. Самая чистая архитектура — та, где каждый слой имеет чёткое и обоснованное назначение.
Прежде чем добавлять следующую абстракцию, спросите себя:
- Я абстрагирую политику или просто механизм?
- У меня две реализации, или я размышляю о будущих потребностях?
- Сделает ли это мою систему более адаптивной или просто более сложной для понимания?
- Если я уберу этот слой, станет ли код проще?
Помните: абстракции — это кредиты, по которым со временем начисляются проценты. Убедитесь, что вы берёте их по правильным причинам, а не просто по привычке.
Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍15👎2
День 2418. #ЧтоНовенького
Visual Studio 2026
Вышла Insiders (для предварительной оценки) версия Visual Studio 2026. В этой версии VS ИИ становится неотъемлемой частью рабочего процесса разработчика, улучшена производительность, которая меняет ожидания относительно скорости, а современный дизайн делает рабочую среду более лёгкой и сфокусированной.
Новый канал предварительной оценки (Insiders Channel), представленный Microsoft, призван заменить канал превью (Preview Channel) и позволяет разработчикам получать ранний доступ к новым функциям.
Скачать и попробовать новую VS 2026 можно отсюда, подробные примечания к выпуску тут. Также попробуйте бесплатную версию Copilot, чтобы раскрыть всю мощь ИИ в Visual Studio 2026.
Вот некоторые новинки, которые вы найдёте в новой версии:
1. Интеграция ИИ в разработку
ИИ в Visual Studio 2026 вплетён в повседневный кодинг. Вы заметите это, когда перейдёте к новой кодовой базе: IDE поможет вам понять, что вы видите, предложит типы тестов, которые обычно пишутся в вашем репозитории, и будет поддерживать документацию и комментарии в соответствии с вашим кодом. Вы почувствуете, что «адаптивная вставка» (Shift+Alt+V) станет вашим вариантом вставки кода по умолчанию, потому что редактор адаптирует фрагмент к шаблонам и соглашениям вашего проекта. А когда возникнут вопросы по производительности, вам не придётся гадать — рекомендации основаны на реальных трассировках и проверены бенчмарками.
Аудит кода начинается с чётких, применимых на практике данных о корректности, производительности и безопасности — на вашем компьютере, ещё до того, как вы откроете пул-реквест. Всё это время вы контролируете ситуацию. IDE берёт на себя всю рутинную работу, а за вами остаётся окончательное решение. Результат прост: вы работаете быстрее, а ваш код становится лучше.
2. Производительность
Скорость определяет то, что вы можете создать за день. В VS 2026 операции, которые вы чаще всего запускаете — открытие решений, навигация по коду, сборка и нажатие F5 — стали быстрее. Вы заметите, что первый запуск происходит быстрее, крупные решения — легче, а время между идеей и запуском приложения продолжает сокращаться. Выигрыш заметен на больших кодовых базах и сохраняется как на x64, так и на Arm64, так что мощность вашего компьютера напрямую влияет на скорость разработки.
3. Современный внешний вид
Ясность важна, когда вы работаете в IDE. VS 2026 представляет более чистый и современный дизайн. В результате рабочее пространство ощущается спокойным и продуманным. Вы заметите более четкие линии, улучшенные иконки и оптимальное расположение визуальных элементов. Настройки не перегружены, поэтому настройка IDE в соответствии с вашими предпочтениями выполняется быстро. Управление расширениями упрощено, а множество новых цветовых тем делают вашу среду персонализированной — комфортной для длительных сеансов кодинга и позволяющей сосредоточиться в критические моменты. Этот дизайн учитывает ваше внимание и помогает оставаться сконцентрированным.
Это только начало. В Microsoft обещают ежемесячные обновления с последними улучшениями производительности, дизайна и инновациями в ИИ.
PS
Поскольку это только предварительная версия, она, конечно, не лишена косяков. У меня, например, часто не подгружалась подсветка синтаксиса и навигация по коду в декомпилированных файлах. Нажимаешь, например, F12 на классе Task, и открывается простой текстовый файл. Поэтому использовать VS 2026 в проде пока рано. Но скорость, по сравнению с VS 2022, действительно впечатляет.
Источник: https://devblogs.microsoft.com/visualstudio/visual-studio-2026-insiders-is-here/
Visual Studio 2026
Вышла Insiders (для предварительной оценки) версия Visual Studio 2026. В этой версии VS ИИ становится неотъемлемой частью рабочего процесса разработчика, улучшена производительность, которая меняет ожидания относительно скорости, а современный дизайн делает рабочую среду более лёгкой и сфокусированной.
Новый канал предварительной оценки (Insiders Channel), представленный Microsoft, призван заменить канал превью (Preview Channel) и позволяет разработчикам получать ранний доступ к новым функциям.
Скачать и попробовать новую VS 2026 можно отсюда, подробные примечания к выпуску тут. Также попробуйте бесплатную версию Copilot, чтобы раскрыть всю мощь ИИ в Visual Studio 2026.
Вот некоторые новинки, которые вы найдёте в новой версии:
1. Интеграция ИИ в разработку
ИИ в Visual Studio 2026 вплетён в повседневный кодинг. Вы заметите это, когда перейдёте к новой кодовой базе: IDE поможет вам понять, что вы видите, предложит типы тестов, которые обычно пишутся в вашем репозитории, и будет поддерживать документацию и комментарии в соответствии с вашим кодом. Вы почувствуете, что «адаптивная вставка» (Shift+Alt+V) станет вашим вариантом вставки кода по умолчанию, потому что редактор адаптирует фрагмент к шаблонам и соглашениям вашего проекта. А когда возникнут вопросы по производительности, вам не придётся гадать — рекомендации основаны на реальных трассировках и проверены бенчмарками.
Аудит кода начинается с чётких, применимых на практике данных о корректности, производительности и безопасности — на вашем компьютере, ещё до того, как вы откроете пул-реквест. Всё это время вы контролируете ситуацию. IDE берёт на себя всю рутинную работу, а за вами остаётся окончательное решение. Результат прост: вы работаете быстрее, а ваш код становится лучше.
2. Производительность
Скорость определяет то, что вы можете создать за день. В VS 2026 операции, которые вы чаще всего запускаете — открытие решений, навигация по коду, сборка и нажатие F5 — стали быстрее. Вы заметите, что первый запуск происходит быстрее, крупные решения — легче, а время между идеей и запуском приложения продолжает сокращаться. Выигрыш заметен на больших кодовых базах и сохраняется как на x64, так и на Arm64, так что мощность вашего компьютера напрямую влияет на скорость разработки.
3. Современный внешний вид
Ясность важна, когда вы работаете в IDE. VS 2026 представляет более чистый и современный дизайн. В результате рабочее пространство ощущается спокойным и продуманным. Вы заметите более четкие линии, улучшенные иконки и оптимальное расположение визуальных элементов. Настройки не перегружены, поэтому настройка IDE в соответствии с вашими предпочтениями выполняется быстро. Управление расширениями упрощено, а множество новых цветовых тем делают вашу среду персонализированной — комфортной для длительных сеансов кодинга и позволяющей сосредоточиться в критические моменты. Этот дизайн учитывает ваше внимание и помогает оставаться сконцентрированным.
Это только начало. В Microsoft обещают ежемесячные обновления с последними улучшениями производительности, дизайна и инновациями в ИИ.
PS
Поскольку это только предварительная версия, она, конечно, не лишена косяков. У меня, например, часто не подгружалась подсветка синтаксиса и навигация по коду в декомпилированных файлах. Нажимаешь, например, F12 на классе Task, и открывается простой текстовый файл. Поэтому использовать VS 2026 в проде пока рано. Но скорость, по сравнению с VS 2022, действительно впечатляет.
Источник: https://devblogs.microsoft.com/visualstudio/visual-studio-2026-insiders-is-here/
👍13
День 2419. #SystemDesign101 #Docker
9 Рекомендаций по Docker, Которые Стоит Знать
1. Используйте официальные образы
Это обеспечивает безопасность, надежность и регулярные обновления.
2. Используйте конкретную версию образа
Тег по умолчанию (latest) непредсказуем и приводит к непредвиденному поведению.
3. Многоэтапные сборки
Уменьшает размер конечного образа за счет исключения инструментов сборки и зависимостей.
4. Используйте .dockerignore
Исключает ненужные файлы, ускоряет сборку и уменьшает размер образа.
5. Используйте пользователя с минимальными привилегиями
Повышает безопасность за счёт ограничения привилегий контейнера.
6. Используйте переменные окружения
Повышает гибкость и переносимость в различных средах.
7. Упорядочивайте задачи для кэширования
Упорядочивайте шаги от наименее к наиболее часто изменяемым для оптимизации кэширования.
8. Маркируйте образы
Это улучшает организацию и помогает в управлении образами.
9. Сканируйте образы
Находите уязвимости безопасности, прежде чем они станут серьезными проблемами.
Источник: https://blog.bytebytego.com
9 Рекомендаций по Docker, Которые Стоит Знать
1. Используйте официальные образы
Это обеспечивает безопасность, надежность и регулярные обновления.
2. Используйте конкретную версию образа
Тег по умолчанию (latest) непредсказуем и приводит к непредвиденному поведению.
3. Многоэтапные сборки
Уменьшает размер конечного образа за счет исключения инструментов сборки и зависимостей.
4. Используйте .dockerignore
Исключает ненужные файлы, ускоряет сборку и уменьшает размер образа.
5. Используйте пользователя с минимальными привилегиями
Повышает безопасность за счёт ограничения привилегий контейнера.
6. Используйте переменные окружения
Повышает гибкость и переносимость в различных средах.
7. Упорядочивайте задачи для кэширования
Упорядочивайте шаги от наименее к наиболее часто изменяемым для оптимизации кэширования.
8. Маркируйте образы
Это улучшает организацию и помогает в управлении образами.
9. Сканируйте образы
Находите уязвимости безопасности, прежде чем они станут серьезными проблемами.
Источник: https://blog.bytebytego.com
👍14
День 2420. #BestPractices
Вещи, Которые Я Делаю в Каждом Проекте .NET. Начало
Как и обещал, разбираю советы Скотта Заубера из его доклада на NDC Conference. Их много, так что эта серия затянется.
И получается это очень просто. Вы видите метод из тысячи строк, и вы думаете: «Мне нужно добавить ещё 4 строки». Какая разница, 1000 строк или 1004. Но так же думали те, кто его редактировал, когда он был 800, 600 и 200 строк. В конце концов, нужно, поставить точку и сказать: «Так, нам нужно это отрефакторить». Потому что, если не следить за этим, то всё быстро выйдет из-под контроля, и тогда, возможно, придётся делать великое переписывание, которое всё исправит. Все же знают, что переписать с нуля - всегда работает )))
Эта цитата о том, что необходимо фокусироваться на поддерживаемости во многих подобных вещах, потому что единственное, что постоянно в ПО, — это изменения. Технологии, требования, бизнес - всё меняется, и мы не можем это контролировать. Поэтому старайтесь оптимизировать приложение с учётом изменяемости.
1. Структура папок
Если вы создаёте новый проект, выбираете, например, MVC и работаете с традиционным серверным приложением (без Blazor, Angular, React), то каждый раз, когда вам нужно добавить новую функцию, вам приходится перемещаться по множеству папок. Нужно зайти в папку контроллеров, в папку представлений, в папку моделей, в CSS, в JavaScript, и т.п., которые разбросаны по всему приложению. Таким образом, область действия вашей функции как бы разбросана по всему проекту. И если вам когда-нибудь понадобится удалить функцию, придётся найти все эти файлы и удалить их. А если нужно добавить новую функцию, вам нужно знать, как разместить логику во всех этих разных папках.
Решение в том, чтобы создавать папки для функций, что упрощает поддержку и приводит к так называемой высокой связности, которая просто означает, что связанные элементы должны оставаться вместе. Вы создаёте функцию управления профилем пользователя? Создайте для неё папку и добавьте туда всё, что имеет отношение к этому: контроллер, модель представления, само представление и т.п. В React и Blazor – то же самое. Все компоненты – и даже тесты этих компонентов – всё в этой папке.
Подумайте, где вы храните мыло в вашем доме или квартире. Всё вместе в одном шкафу для мыла? Мыло для рук, шампунь, гель для душа, стиральный порошок и т.п. Нет. Вы храните все разные виды мыла там, где вы ими пользуетесь. Мыло для рук у раковины, шампунь и гель для душа в ванной, порошок – рядом со стиральной машиной и т.п. Почему бы не применить ту же концепцию в проекте?
2. Предупреждения
Бывало ли, что вы открывали проект, собирали его и получали сотни или даже тысячи предупреждений? Не очень приятно, правда? Так вот, предупреждений быть не должно! Либо это ошибка, которая меня волнует, и я её исправляю, либо я её игнорирую по какой-то причине, например, знаю, что она не появится. Поэтому включите флажок «Рассматривать предупреждения как ошибки». Это особенно актуально для новых проектов. Сделайте это с самого начала, потому что в противном случае у вас будут копиться самые разные предупреждения, и вы не будет знать, какие из них важны, а какие можно проигнорировать». Поэтому постарайтесь разбираться с ними с самого начала.
В существующем проекте либо создайте отдельную задачу, чтобы разобраться со всеми предупреждениями, либо делайте это по ходу выполнения других задач, когда работаете с кодом, где возникает предупреждение. Если вы абсолютно уверены, что какой-то вид предупреждений можно безболезненно игнорировать, его можно включить в исключения, чтобы этот вид предупреждений не выдавал ошибку при сборке.
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
Вещи, Которые Я Делаю в Каждом Проекте .NET. Начало
Как и обещал, разбираю советы Скотта Заубера из его доклада на NDC Conference. Их много, так что эта серия затянется.
Каждая система стремится к запутанности, медленности и сложности. Поддержание простоты, скорости и лёгкости использования — это битва, в которой приходится бороться каждый день.
— Гильермо Раух
И получается это очень просто. Вы видите метод из тысячи строк, и вы думаете: «Мне нужно добавить ещё 4 строки». Какая разница, 1000 строк или 1004. Но так же думали те, кто его редактировал, когда он был 800, 600 и 200 строк. В конце концов, нужно, поставить точку и сказать: «Так, нам нужно это отрефакторить». Потому что, если не следить за этим, то всё быстро выйдет из-под контроля, и тогда, возможно, придётся делать великое переписывание, которое всё исправит. Все же знают, что переписать с нуля - всегда работает )))
Эта цитата о том, что необходимо фокусироваться на поддерживаемости во многих подобных вещах, потому что единственное, что постоянно в ПО, — это изменения. Технологии, требования, бизнес - всё меняется, и мы не можем это контролировать. Поэтому старайтесь оптимизировать приложение с учётом изменяемости.
1. Структура папок
Если вы создаёте новый проект, выбираете, например, MVC и работаете с традиционным серверным приложением (без Blazor, Angular, React), то каждый раз, когда вам нужно добавить новую функцию, вам приходится перемещаться по множеству папок. Нужно зайти в папку контроллеров, в папку представлений, в папку моделей, в CSS, в JavaScript, и т.п., которые разбросаны по всему приложению. Таким образом, область действия вашей функции как бы разбросана по всему проекту. И если вам когда-нибудь понадобится удалить функцию, придётся найти все эти файлы и удалить их. А если нужно добавить новую функцию, вам нужно знать, как разместить логику во всех этих разных папках.
Решение в том, чтобы создавать папки для функций, что упрощает поддержку и приводит к так называемой высокой связности, которая просто означает, что связанные элементы должны оставаться вместе. Вы создаёте функцию управления профилем пользователя? Создайте для неё папку и добавьте туда всё, что имеет отношение к этому: контроллер, модель представления, само представление и т.п. В React и Blazor – то же самое. Все компоненты – и даже тесты этих компонентов – всё в этой папке.
Подумайте, где вы храните мыло в вашем доме или квартире. Всё вместе в одном шкафу для мыла? Мыло для рук, шампунь, гель для душа, стиральный порошок и т.п. Нет. Вы храните все разные виды мыла там, где вы ими пользуетесь. Мыло для рук у раковины, шампунь и гель для душа в ванной, порошок – рядом со стиральной машиной и т.п. Почему бы не применить ту же концепцию в проекте?
2. Предупреждения
Бывало ли, что вы открывали проект, собирали его и получали сотни или даже тысячи предупреждений? Не очень приятно, правда? Так вот, предупреждений быть не должно! Либо это ошибка, которая меня волнует, и я её исправляю, либо я её игнорирую по какой-то причине, например, знаю, что она не появится. Поэтому включите флажок «Рассматривать предупреждения как ошибки». Это особенно актуально для новых проектов. Сделайте это с самого начала, потому что в противном случае у вас будут копиться самые разные предупреждения, и вы не будет знать, какие из них важны, а какие можно проигнорировать». Поэтому постарайтесь разбираться с ними с самого начала.
В существующем проекте либо создайте отдельную задачу, чтобы разобраться со всеми предупреждениями, либо делайте это по ходу выполнения других задач, когда работаете с кодом, где возникает предупреждение. Если вы абсолютно уверены, что какой-то вид предупреждений можно безболезненно игнорировать, его можно включить в исключения, чтобы этот вид предупреждений не выдавал ошибку при сборке.
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
👍27
День 2421. #BestPractices
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
Начало
3. Логирование
Конкретная библиотека не имеет значения: Serilog, NLog или log4net. Но не используйте их напрямую. Используйте везде ILogger, чтобы единственное место, которое бы знало о конкретной его реализации – был файл program.cs. По возможности используйте структурное логирование. Также помните о том, что каждый журнал должен содержать некоторую ключевую информацию, например, ID пользователя или ID корреляции. Так вы сможете отслеживать как один запрос проходит через разные сервисы, и что там происходит. Иногда полезно включать короткий sha Git-коммита, чтобы если появляется ошибка, и вы её исправляете, вы могли сравнить, получаем ли мы больше исключений в последней версии, чем получали раньше?
Также важно разделять, как вы логируете нужную информацию. Например, в системе управления пользователями нужно регистрировать, кто внёс каждое изменение. Иногда мы думаем: «Есть же логгер, запишем в лог». Скорей всего, это не лучшее место. Рассмотрим о логи, метрики и аудиты.
Логи должны быть ориентированы на разработчиков. Вы не должны их передавать бизнесу. Это исключения с трассировками стека, ответы от внешних API, разные этапы процесса обработки и т.п.
Кроме того, многие путают уровни логирования. Например, сваливают всё в Information. Вы вызываете 3 API и хотите логировать их ответы. Это информация уровня отладки (Debug). А Information – это результат запроса: мы обработали столько-то записей за такое-то время, т.е. одна запись на запрос пользователя.
Используйте предупреждения (Warning) вместо ошибок (Error). Логи уровня предупреждения работают так: если у вас одно предупреждение, это не страшно, но если их много, то что-то не так. Классический пример: пользователь вводит неверный пароль и блокирует свою учётную запись на пять минут. Одно событие – не важно, но если их тысячи за короткий промежуток времени, возможно, происходит что-то не то (например, кто-то пытается проникнуть в систему).
Ошибки (Error) — это необработанные исключения. Многие просто игнорируют ошибки в логах. Не стоит так делать. Нужно выяснить первопричину и определить, волнует ли нас это. Может это предупреждение, а может реальная ошибка в коде. Постарайтесь очистить ошибки из лога, потому что худший человек, которому стоит сообщать о проблемах, — это ваши пользователи.
И, наконец, критический уровень (Critical) используется, когда приложение не может загрузиться. Например, нужно сначала обратиться к БД или к Azure Key Vault, чтобы извлечь секреты. Если это не удаётся, приложение не может работать.
Стоит подумать, как долго хранятся логи. Azure Log Analytics по умолчанию хранит только 30 дней. И если вы пытаетесь сохранить историческую информацию, например: «кто что изменил?», то ваш ответ бизнесу «Мы храним только 30 дней логов» вряд ли будет удовлетворительным. Аналогично, многие системы записывают логи в буфер и потом скидывают на диск. Так что, если отключится питание, последние логи потеряются. Это нормально, если логи используются только как описано выше, и не хранят важной для бизнеса информации.
Метрики делятся на 2 типа:
- уровня приложения: процессор, сеть, память, глубина очереди и т.п.
- бизнес-метрики: сколько размещено заказов, сколько раз нажимали эту кнопку, или сколько просмотрели эту страницу.
Как долго нужно хранить эти данные? Это должен быть разговор с бизнесом.
Аудиты - это регистрация того, кто, когда и что делал в приложении. В таких случаях вообще нельзя использовать логи. Самый простой способ — хранить данные аудита в БД вместе с данными. В одной транзакции вносите и изменение, и запись, кто его сделал.
Поэтому, когда кто-то говорит: «Нам нужно логировать это в следующий раз», - подумайте, какое хранилище данных использовать и на какой срок нужны эти данные.
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
Начало
3. Логирование
Конкретная библиотека не имеет значения: Serilog, NLog или log4net. Но не используйте их напрямую. Используйте везде ILogger, чтобы единственное место, которое бы знало о конкретной его реализации – был файл program.cs. По возможности используйте структурное логирование. Также помните о том, что каждый журнал должен содержать некоторую ключевую информацию, например, ID пользователя или ID корреляции. Так вы сможете отслеживать как один запрос проходит через разные сервисы, и что там происходит. Иногда полезно включать короткий sha Git-коммита, чтобы если появляется ошибка, и вы её исправляете, вы могли сравнить, получаем ли мы больше исключений в последней версии, чем получали раньше?
Также важно разделять, как вы логируете нужную информацию. Например, в системе управления пользователями нужно регистрировать, кто внёс каждое изменение. Иногда мы думаем: «Есть же логгер, запишем в лог». Скорей всего, это не лучшее место. Рассмотрим о логи, метрики и аудиты.
Логи должны быть ориентированы на разработчиков. Вы не должны их передавать бизнесу. Это исключения с трассировками стека, ответы от внешних API, разные этапы процесса обработки и т.п.
Кроме того, многие путают уровни логирования. Например, сваливают всё в Information. Вы вызываете 3 API и хотите логировать их ответы. Это информация уровня отладки (Debug). А Information – это результат запроса: мы обработали столько-то записей за такое-то время, т.е. одна запись на запрос пользователя.
Используйте предупреждения (Warning) вместо ошибок (Error). Логи уровня предупреждения работают так: если у вас одно предупреждение, это не страшно, но если их много, то что-то не так. Классический пример: пользователь вводит неверный пароль и блокирует свою учётную запись на пять минут. Одно событие – не важно, но если их тысячи за короткий промежуток времени, возможно, происходит что-то не то (например, кто-то пытается проникнуть в систему).
Ошибки (Error) — это необработанные исключения. Многие просто игнорируют ошибки в логах. Не стоит так делать. Нужно выяснить первопричину и определить, волнует ли нас это. Может это предупреждение, а может реальная ошибка в коде. Постарайтесь очистить ошибки из лога, потому что худший человек, которому стоит сообщать о проблемах, — это ваши пользователи.
И, наконец, критический уровень (Critical) используется, когда приложение не может загрузиться. Например, нужно сначала обратиться к БД или к Azure Key Vault, чтобы извлечь секреты. Если это не удаётся, приложение не может работать.
Стоит подумать, как долго хранятся логи. Azure Log Analytics по умолчанию хранит только 30 дней. И если вы пытаетесь сохранить историческую информацию, например: «кто что изменил?», то ваш ответ бизнесу «Мы храним только 30 дней логов» вряд ли будет удовлетворительным. Аналогично, многие системы записывают логи в буфер и потом скидывают на диск. Так что, если отключится питание, последние логи потеряются. Это нормально, если логи используются только как описано выше, и не хранят важной для бизнеса информации.
Метрики делятся на 2 типа:
- уровня приложения: процессор, сеть, память, глубина очереди и т.п.
- бизнес-метрики: сколько размещено заказов, сколько раз нажимали эту кнопку, или сколько просмотрели эту страницу.
Как долго нужно хранить эти данные? Это должен быть разговор с бизнесом.
Аудиты - это регистрация того, кто, когда и что делал в приложении. В таких случаях вообще нельзя использовать логи. Самый простой способ — хранить данные аудита в БД вместе с данными. В одной транзакции вносите и изменение, и запись, кто его сделал.
Поэтому, когда кто-то говорит: «Нам нужно логировать это в следующий раз», - подумайте, какое хранилище данных использовать и на какой срок нужны эти данные.
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
👍21
День 2422. #BestPractices
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
1-2
3
4. Задаём авторизацию глобально
Если вы используете, например, контроллеры, вам нужно не забыть добавить атрибут Authorize во всех контроллерах и действиях, которые вы хотите защитить (а это обычно большинство). Либо нужно не забыть наследовать от какого-нибудь базового контроллера, у которого есть этот атрибут. Но проблема в том, что если вы забудете, то этот метод/контроллер полностью открыты для внешнего мира.
Решение этой проблемы — так называемая резервная политика (FallbackPolicy) в ASP.NET Core. Резервная политика работает так: если вы не укажете другую политику, эта будет срабатывать каждый раз. В контейнере DI вы просто добавляете авторизацию и устанавливаете эту резервную политику:
Вы можете создать любую политику, но проще использовать конструктор политик авторизации и запросить аутентифицированного пользователя. Это сработает только в том случае, если вы не укажете ничего другого в контроллере. Если в контроллер или метод контроллера вы добавите атрибут AllowAnonymous, либо атрибут с более строгой политикой, будет использован он. А так вы защищены по умолчанию.
5. Валидация
Наверное, большинство из вас использовали аннотации данных для валидации. Проблема с ними в том, что, если нужно наложить несколько условий на свойство, добавление аннотации для каждого условия быстро становится громоздким и загружает модель. Не говоря уже о случае, когда нам требуется какая-то нестандартная валидация. Кроме того, для них сложно писать автоматизированные тесты.
Fluent Validation позволяет создавать бизнес-правила, которые легко читать даже бизнесу. Их также легко тестировать. Сложные правила создавать тоже просто. Например, в США, вы не можете пользоваться медицинской страховкой родителей после 26 лет. И вот правило для этого:
Вы можете показать это бизнесу, и они поймут эти условия, даже не зная кода.
6. Заголовки сервера
По умолчанию ASP.NET Core в ответ добавляет HTTP-заголовок с именем сервера. Если вы используете Kestrel, это значение будет Kestrel. Зачем он это делает? Скорей всего, в Microsoft просто хотят знать, сколько сайтов используют ASP.NET Core. В .NET Framework было ещё хуже, потому что выдавались и версии MVC и .NET. Но это раскрывает хакерам, что вы используете. Т.е. им не нужно проверять ваш сайт на уязвимости Java или Python, только на уязвимости, специфичные для .NET. Удаление заголовка не избавит вас от атак, но увеличит количество времени, которое атакующий потратит. Если у вас есть публичный веб-сайт, он гарантированно подвергался различным атакам. Удалить заголовок легко, просто добавьте эту строку:
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
1-2
3
4. Задаём авторизацию глобально
Если вы используете, например, контроллеры, вам нужно не забыть добавить атрибут Authorize во всех контроллерах и действиях, которые вы хотите защитить (а это обычно большинство). Либо нужно не забыть наследовать от какого-нибудь базового контроллера, у которого есть этот атрибут. Но проблема в том, что если вы забудете, то этот метод/контроллер полностью открыты для внешнего мира.
Решение этой проблемы — так называемая резервная политика (FallbackPolicy) в ASP.NET Core. Резервная политика работает так: если вы не укажете другую политику, эта будет срабатывать каждый раз. В контейнере DI вы просто добавляете авторизацию и устанавливаете эту резервную политику:
bulder.Services.AddAuthorization(opts =>
{
opts.FallbackPolicy = new
AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Вы можете создать любую политику, но проще использовать конструктор политик авторизации и запросить аутентифицированного пользователя. Это сработает только в том случае, если вы не укажете ничего другого в контроллере. Если в контроллер или метод контроллера вы добавите атрибут AllowAnonymous, либо атрибут с более строгой политикой, будет использован он. А так вы защищены по умолчанию.
5. Валидация
Наверное, большинство из вас использовали аннотации данных для валидации. Проблема с ними в том, что, если нужно наложить несколько условий на свойство, добавление аннотации для каждого условия быстро становится громоздким и загружает модель. Не говоря уже о случае, когда нам требуется какая-то нестандартная валидация. Кроме того, для них сложно писать автоматизированные тесты.
Fluent Validation позволяет создавать бизнес-правила, которые легко читать даже бизнесу. Их также легко тестировать. Сложные правила создавать тоже просто. Например, в США, вы не можете пользоваться медицинской страховкой родителей после 26 лет. И вот правило для этого:
public class InsuranceValidator
: AbstractValidator<Insurance>
{
public InsuranceValidator()
{
RuleFor(model => model.Age)
.Must(age => age < 26)
.When(model => model.IsDependent)
.WithMessage(“A dependent must be younger than 26”);
}
}
Вы можете показать это бизнесу, и они поймут эти условия, даже не зная кода.
6. Заголовки сервера
По умолчанию ASP.NET Core в ответ добавляет HTTP-заголовок с именем сервера. Если вы используете Kestrel, это значение будет Kestrel. Зачем он это делает? Скорей всего, в Microsoft просто хотят знать, сколько сайтов используют ASP.NET Core. В .NET Framework было ещё хуже, потому что выдавались и версии MVC и .NET. Но это раскрывает хакерам, что вы используете. Т.е. им не нужно проверять ваш сайт на уязвимости Java или Python, только на уязвимости, специфичные для .NET. Удаление заголовка не избавит вас от атак, но увеличит количество времени, которое атакующий потратит. Если у вас есть публичный веб-сайт, он гарантированно подвергался различным атакам. Удалить заголовок легко, просто добавьте эту строку:
builder.WebHost.UseKestrel(opts => opts.AddServerHeader = false);
Продолжение следует…
Источник: https://youtu.be/SvcRvolP2NE
👍25
День 2423. #BestPractices
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
1-2
3
4-6
7. Не внедряйте IOptions
Проблема с параметрами IOptions в том, что вам нужна зависимость от Microsoft.Extensions. И если вы используете IOptions в других проектах ниже по стеку вызовов, придётся добавлять зависимость во все эти проекты. И тестировать параметр типа IOptions может быть непросто. Кроме того, везде приходится использовать .Value вместо просто имени параметра:
Решение - регистрируйте класс IOptions напрямую, так вы получите нужный класс значений в DI контейнере, который уже можно внедрять:
Если вы хотите, чтобы параметры изменялись без перезагрузки приложения, это тоже будет работать. Регистрируете значения как AddScoped и используйте IOptionsSnapshot<T> вместо IOptions<T>.
8. Запахи кода
Во-первых, возьмите за правило располагать «счастливый путь» в конце метода. Если вы смотрите на незнакомый код, как быстро узнать, что происходит, когда всё идёт хорошо? Проще всего – когда нужно всегда смотреть в конец.
Чтобы этого добиться, используйте технику раннего возврата. Вместо нескольких вложенных if-else, просто возвращайте ошибку (или любой другой результат), если что-то идёт не так. Таким образом, в начале метода следуют необходимые проверки, которые завершаются ранним возвратом, если они не проходят, а в конце (если все проверки прошли) – возврат результата, когда всё хорошо:
Это не только позволяет быстро найти «счастливый путь», но и избавляет от избыточной вложенности конструкций if.
И некоторые не жёсткие правила, а просто предупредительные сигналы о запахах кода:
- метод длиннее 20 строк,
- класс длиннее 200 строк,
- использование областей (#region).
Это всё сигналы о том, что метод или класс можно разбить на части.
9. Новые файлы решений
Это относительно новая функция. Технически она всё ещё в стадии превью версии, но сейчас довольно стабильна. Если коротко, новые файлы .slnx – это старый добрый XML, они гораздо короче и понятнее. Обновить файл решения можно, просто перейдя в папку решения и выполнив:
Затем не забудьте удалить старый файл .sln. Он больше не нужен, но утилита его не удаляет.
Окончание следует…
Источник: https://youtu.be/SvcRvolP2NE
Вещи, Которые Я Делаю в Каждом Проекте .NET. Продолжение
1-2
3
4-6
7. Не внедряйте IOptions
Проблема с параметрами IOptions в том, что вам нужна зависимость от Microsoft.Extensions. И если вы используете IOptions в других проектах ниже по стеку вызовов, придётся добавлять зависимость во все эти проекты. И тестировать параметр типа IOptions может быть непросто. Кроме того, везде приходится использовать .Value вместо просто имени параметра:
public class MyController(
IOptions<AppSettings> appSettings)
{
private readonly AppSettings _appSettings =
appSettings.Value;
…
}
Решение - регистрируйте класс IOptions напрямую, так вы получите нужный класс значений в DI контейнере, который уже можно внедрять:
services.Configure<AppSettings>(
Configuration.GetSection("AppSettings"));
services.AddSingleton(s =>
s.GetRequiredService<IOptions<AppSettings>>().Value);
…
public class MyController(AppSettings appSettings)
{
…
}
Если вы хотите, чтобы параметры изменялись без перезагрузки приложения, это тоже будет работать. Регистрируете значения как AddScoped и используйте IOptionsSnapshot<T> вместо IOptions<T>.
8. Запахи кода
Во-первых, возьмите за правило располагать «счастливый путь» в конце метода. Если вы смотрите на незнакомый код, как быстро узнать, что происходит, когда всё идёт хорошо? Проще всего – когда нужно всегда смотреть в конец.
Чтобы этого добиться, используйте технику раннего возврата. Вместо нескольких вложенных if-else, просто возвращайте ошибку (или любой другой результат), если что-то идёт не так. Таким образом, в начале метода следуют необходимые проверки, которые завершаются ранним возвратом, если они не проходят, а в конце (если все проверки прошли) – возврат результата, когда всё хорошо:
public ActionResult SendMessage(Message msg)
{
if(!ModelState.IsValid())
return View(msg);
if(emailSender.Send(msg) != Result.OK)
{
_logger.Log(…);
return LocalRedirect("/message/error");
}
…
return LocalRedirect("/message/success");
}
Это не только позволяет быстро найти «счастливый путь», но и избавляет от избыточной вложенности конструкций if.
И некоторые не жёсткие правила, а просто предупредительные сигналы о запахах кода:
- метод длиннее 20 строк,
- класс длиннее 200 строк,
- использование областей (#region).
Это всё сигналы о том, что метод или класс можно разбить на части.
9. Новые файлы решений
Это относительно новая функция. Технически она всё ещё в стадии превью версии, но сейчас довольно стабильна. Если коротко, новые файлы .slnx – это старый добрый XML, они гораздо короче и понятнее. Обновить файл решения можно, просто перейдя в папку решения и выполнив:
dotnet sln migrate
Затем не забудьте удалить старый файл .sln. Он больше не нужен, но утилита его не удаляет.
Окончание следует…
Источник: https://youtu.be/SvcRvolP2NE
👍21
День 2424. #BestPractices
Вещи, Которые Я Делаю в Каждом Проекте .NET. Окончание
1-2
3
4-6
7-9
10. HTTP-заголовки безопасности
Они сообщают браузеру о необходимости применения дополнительных правил. Это означает, что вы можете предотвратить определённые типы атак, такие как «человек посередине», кликджекинг, кросс-скриптинг и многие другие. Однако будьте осторожны. Можно, например, запретить загрузку JavaScript отовсюду, кроме вашего домена, но если вы при этом используете CDN, то ваше приложение сломается. Есть стратегии по постепенному внедрению HTTP-заголовков в существующее приложение. Если ваше приложение сканируется на безопасность внешними аналитиками, они всегда проверяют это. Есть публичные сайты, вроде securityheaders.com, которые просканируют ваш сайт и сообщат о состоянии дел. Примерно половина веб-сайтов вообще не имеет заголовков безопасности. И только у 10% рейтинг A или A+. У Эндрю Лока есть NuGet-пакет для реализации некоторых заголовков безопасности.
11. Валидация при сборке
Эта функция проверяет, что ваш DI контейнер настроен правильно. Синглтоны зависят только от синглтонов, а не принимают scoped-объекты. Иначе возникает проблема захвата зависимости. ASP.NET Core обнаруживает это, в режиме локальной разработки, но не в других средах. Но вы можете заставить это работать всегда:
12. Автоматизированные тесты
Надеюсь, не нужно объяснять, почему нужно писать тесты? Здесь рассмотрим один нюанс. Слышали выражение «чеховское ружьё»? Если кратко, Чехов писал, что если в главе 1 у вас на стене висит ружьё, то в последующих главах оно должно выстрелить. Иначе нечего его изначально «вешать». То есть, у всего должно быть предназначение. Вот как это касается тестов.
Что здесь не так? Имя, дата рождения и адрес не имеют значения для этого теста. Фамилия — единственное, что мы проверяем. Мы можем вынести ненужные конструкторы в общий код:
Нижний тест более ясно показывает, что мы тестируем.
13. Централизованное управление пакетами
Проблема, которую решает централизованное управление пакетами, в том, что у вас может быть один пакет в нескольких проектах в решении, и может быть раздражающим поддерживать их синхронизацию. В .NET 6 добавили эту функцию, с помощью которой вы можете создать directory.packages.props в корне и определить там пакеты и версии, которые вам нужны, а затем вы удаляете все версии пакетов в файлах .csproj проектов, и все проекты используют одну версию пакета (хотя, её можно переопределить в любом проекте при необходимости).
Источник: https://youtu.be/SvcRvolP2NE
Вещи, Которые Я Делаю в Каждом Проекте .NET. Окончание
1-2
3
4-6
7-9
10. HTTP-заголовки безопасности
Они сообщают браузеру о необходимости применения дополнительных правил. Это означает, что вы можете предотвратить определённые типы атак, такие как «человек посередине», кликджекинг, кросс-скриптинг и многие другие. Однако будьте осторожны. Можно, например, запретить загрузку JavaScript отовсюду, кроме вашего домена, но если вы при этом используете CDN, то ваше приложение сломается. Есть стратегии по постепенному внедрению HTTP-заголовков в существующее приложение. Если ваше приложение сканируется на безопасность внешними аналитиками, они всегда проверяют это. Есть публичные сайты, вроде securityheaders.com, которые просканируют ваш сайт и сообщат о состоянии дел. Примерно половина веб-сайтов вообще не имеет заголовков безопасности. И только у 10% рейтинг A или A+. У Эндрю Лока есть NuGet-пакет для реализации некоторых заголовков безопасности.
11. Валидация при сборке
Эта функция проверяет, что ваш DI контейнер настроен правильно. Синглтоны зависят только от синглтонов, а не принимают scoped-объекты. Иначе возникает проблема захвата зависимости. ASP.NET Core обнаруживает это, в режиме локальной разработки, но не в других средах. Но вы можете заставить это работать всегда:
builder.Host.UseDefaultHostProvider(config =>
{
config.ValidateOnBuild = true;
});
12. Автоматизированные тесты
Надеюсь, не нужно объяснять, почему нужно писать тесты? Здесь рассмотрим один нюанс. Слышали выражение «чеховское ружьё»? Если кратко, Чехов писал, что если в главе 1 у вас на стене висит ружьё, то в последующих главах оно должно выстрелить. Иначе нечего его изначально «вешать». То есть, у всего должно быть предназначение. Вот как это касается тестов.
[Fact]
public void EmptyLastNameValidationTest()
{
var customer = new Customer
{
FirstName = "John",
LastName = "",
Address = "123 1st Street",
Born = new DateOnly(2000, 1, 1)
}
var result = new CustomerValidator()
.Validate(customer);
result.Errors.Should().Contain(
e => e.ErrorMessage == "Last Name is required");
}
Что здесь не так? Имя, дата рождения и адрес не имеют значения для этого теста. Фамилия — единственное, что мы проверяем. Мы можем вынести ненужные конструкторы в общий код:
[Fact]
public void EmptyLastNameValidationTest()
{
_customer.LastName = "";
var result = _customerValidator
.Validate(_customer);
result.Errors.Should().Contain(
e => e.ErrorMessage == "Last Name is required");
}
Нижний тест более ясно показывает, что мы тестируем.
13. Централизованное управление пакетами
Проблема, которую решает централизованное управление пакетами, в том, что у вас может быть один пакет в нескольких проектах в решении, и может быть раздражающим поддерживать их синхронизацию. В .NET 6 добавили эту функцию, с помощью которой вы можете создать directory.packages.props в корне и определить там пакеты и версии, которые вам нужны, а затем вы удаляете все версии пакетов в файлах .csproj проектов, и все проекты используют одну версию пакета (хотя, её можно переопределить в любом проекте при необходимости).
Источник: https://youtu.be/SvcRvolP2NE
👍17
День 2425. #ЗаметкиНаПолях
Тестирование Текущего Времени с Помощью TimeProvider и FakeTimeProvider
В тестировании сложно использовать то, что зависит от конкретных данных. Представьте себе файловую систему: для корректной работы тестов необходимо убедиться, что файловая система структурирована именно так, как вы ожидаете. Похожая проблема возникает и с датами: если вы создаёте тесты, основанные на текущей дате, при следующем запуске они не пройдут. Нужно найти способ абстрагировать эти функции, чтобы сделать их пригодными для использования в тестах. Сегодня рассмотрим класс TimeProvider, как его использовать и как его имитировать.
Раньше: интерфейс вручную
Раньше самым простым способом абстрагирования управления датами было ручное создание интерфейса или абстрактного класса для доступа к текущей дате:
И стандартная его реализация, использующая дату и время в UTC:
Или аналогичный подход с абстрактным классом:
Затем нужно просто добавить его экземпляр в движок DI, и всё готово. Единственная проблема - придётся делать это для каждого проекта, над которым вы работаете.
Сейчас: класс TimeProvider
Вместе с .NET 8 команда .NET выпустила абстрактный класс TimeProvider. Помимо предоставления абстракции для локального времени, он предоставляет методы для работы с высокоточными временными метками и часовыми поясами. Важно отметить, что даты возвращаются как DateTimeOffset, а не как экземпляры DateTime. TimeProvider поставляется «из коробки» с консольным приложением .NET:
А если вам нужно использовать внедрение зависимостей, нужно внедрить его как синглтон:
Тестируем TimeProvider
Мы можем использовать NuGet-пакет Microsoft.Extensions.TimeProvider.Testing, который предоставляет класс FakeTimeProvider, выступающий в качестве заглушки для абстрактного класса TimeProvider. Используя класс FakeTimeProvider, вы можете установить текущее время UTC и местное время, а также настроить другие параметры, предоставляемые TimeProvider:
На самом деле TimeProvider предоставляет гораздо больше функциональных возможностей, чем просто возврат UTC и местного времени. См. документацию
Источник: https://www.code4it.dev/csharptips/timeprovider-faketimeprovider/
Тестирование Текущего Времени с Помощью TimeProvider и FakeTimeProvider
В тестировании сложно использовать то, что зависит от конкретных данных. Представьте себе файловую систему: для корректной работы тестов необходимо убедиться, что файловая система структурирована именно так, как вы ожидаете. Похожая проблема возникает и с датами: если вы создаёте тесты, основанные на текущей дате, при следующем запуске они не пройдут. Нужно найти способ абстрагировать эти функции, чтобы сделать их пригодными для использования в тестах. Сегодня рассмотрим класс 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);
А если вам нужно использовать внедрение зависимостей, нужно внедрить его как синглтон:
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 и местного времени. См. документацию
Источник: https://www.code4it.dev/csharptips/timeprovider-faketimeprovider/
🔥23👍1
День 2426. #TipsAndTricks
Перемещение Файлов и Папок в Корзину в .NET
При работе с файлами и папками в приложениях .NET в Windows может потребоваться перемещать элементы в корзину вместо их безвозвратного удаления (File.Delete, Directory.Delete). Это позволит пользователям восстановить случайно удалённые элементы. Вот как это можно сделать с помощью API Windows Shell:
Теперь можно использовать этот метод для перемещения файлов и папок в Корзину:
Источник: https://www.meziantou.net/moving-files-and-folders-to-recycle-bin-in-dotnet.htm
Перемещение Файлов и Папок в Корзину в .NET
При работе с файлами и папками в приложениях .NET в Windows может потребоваться перемещать элементы в корзину вместо их безвозвратного удаления (File.Delete, Directory.Delete). Это позволит пользователям восстановить случайно удалённые элементы. Вот как это можно сделать с помощью API Windows Shell:
static void MoveToRecycleBin(string path)
{
if (!OperatingSystem.IsWindows()) return;
var shellType = Type.GetTypeFromProgID(
"Shell.Application", throwOnError: true)!;
dynamic shellApp =
Activator.CreateInstance(shellType)!;
// https://learn.microsoft.com/en-us/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants?WT.mc_id=DT-MVP-5003978
var recycleBin = shellApp.Namespace(0xa);
// https://learn.microsoft.com/en-us/windows/win32/shell/folder-movehere?WT.mc_id=DT-MVP-5003978
recycleBin.MoveHere(path);
}
Теперь можно использовать этот метод для перемещения файлов и папок в Корзину:
MoveToRecycleBin(@"C:\path\to\file.txt");
MoveToRecycleBin(@"C:\path\to\directory");
Источник: https://www.meziantou.net/moving-files-and-folders-to-recycle-bin-in-dotnet.htm
👍26
День 2427. #ЗаметкиНаПолях
Особенность ToDictionaryAsync в Entity Framework
В Entity Framework Core ToDictionaryAsync (и, конечно же, его синхронный аналог ToDictionary) извлекает весь объект из БД.
Вот определение ToDictionaryAsync:
Мы видим, что он принимает лямбды для выбора ключей и значений, а также видим, что этот выбор выполняется на клиенте после получения всего объекта! Поэтому, если у вас есть сущность вроде такой:
И вы выполняете какой-то такой запрос:
Тогда вы извлекаете из базы данных не только автора и заголовок, но и, описание (Description), и содержание (Content). Решение проблемы описано в этом тикете: используем Select для выбора только нужных полей, так как он выполняется на стороне сервера:
Источник: https://steven-giesel.com/blogPost/1af57355-7978-40e6-a0f1-3d0ba2c6e1bc/todictionaryasync-retrieves-the-whole-object-from-the-database-in-entity-framework
Особенность ToDictionaryAsync в Entity Framework
В Entity Framework Core ToDictionaryAsync (и, конечно же, его синхронный аналог ToDictionary) извлекает весь объект из БД.
Вот определение ToDictionaryAsync:
public static async Task<Dictionary<TKey, TElement>> ToDictionaryAsync<TSource, TKey, TElement>(
this IQueryable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey>? comparer,
CancellationToken ct = default)
where TKey : notnull
{
Check.NotNull(keySelector);
Check.NotNull(elementSelector);
var d = new Dictionary<TKey, TElement>(comparer);
await foreach (var element in
source.AsAsyncEnumerable()
.WithCancellation(ct)
.ConfigureAwait(false))
{
d.Add(keySelector(element), elementSelector(element));
}
return d;
}
Мы видим, что он принимает лямбды для выбора ключей и значений, а также видим, что этот выбор выполняется на клиенте после получения всего объекта! Поэтому, если у вас есть сущность вроде такой:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Content { get; set; }
public string Author { get; set; }
}
И вы выполняете какой-то такой запрос:
return await dbContext.BlogPosts
.Where(...)
.ToDictionaryAsync(k => k.Author, v => v.Title);
Тогда вы извлекаете из базы данных не только автора и заголовок, но и, описание (Description), и содержание (Content). Решение проблемы описано в этом тикете: используем Select для выбора только нужных полей, так как он выполняется на стороне сервера:
return await dbContext.BlogPosts
.Where(...)
.Select(s => new { Author = s.Author, Title = s.Title })
.ToDictionaryAsync(k => k.Author, v => v.Title);
Источник: https://steven-giesel.com/blogPost/1af57355-7978-40e6-a0f1-3d0ba2c6e1bc/todictionaryasync-retrieves-the-whole-object-from-the-database-in-entity-framework
👍41
День 2428. #Оффтоп
Странная Ошибка Компиляции
Пишу экспорт отчёта в CSV. До этого использовали библиотеку EPPlus (её же используем для экспорта в Excel), но из-за большого размера отчёта оно периодически падает с Out of memory.
Свойства объектов экспортируемой коллекции помечены атрибутами EPPlus для форматирования. Типа
Понятно, что EPPlus при экспорте сам эти атрибуты обрабатывает, а мне в самодельном экспорте надо их ручками вытягивать через рефлексию. См. вторую картинку. Обращаюсь через
или
И вот когда в
В обоих файлах ссылка на нугет есть, юзинг неймспейса есть. И
Предложения по исправлению ничем не помогают. Там только что-то в духе «Создать новый класс 'EpplusTableColumn'…».
Есть предположения, в чём проблема?
Чуть позже выложу ответ.
UPD: довольно быстро догадались, ответ в комментариях.
Странная Ошибка Компиляции
Пишу экспорт отчёта в CSV. До этого использовали библиотеку EPPlus (её же используем для экспорта в Excel), но из-за большого размера отчёта оно периодически падает с Out of memory.
Свойства объектов экспортируемой коллекции помечены атрибутами EPPlus для форматирования. Типа
[EpplusIgnore] для игнорирования свойства, [EpplusTableColumn(Header="Заголовок", Order = 1)] - для имени заголовка колонки и порядка. См. первую картинку.Понятно, что EPPlus при экспорте сам эти атрибуты обрабатывает, а мне в самодельном экспорте надо их ручками вытягивать через рефлексию. См. вторую картинку. Обращаюсь через
Attribute.GetCustomAttribute(prop, typeof(…))
или
prop.CustomAttributes.Any(a => a.AttributeType == typeof(…))
И вот когда в
typeof(…) кидаю EpplusIgnore, всё работает, а когда кидаю EpplusTableColumn - пишет The type or namespace name 'EpplusTableColumn' could not be found (are you missing a using directive or an assembly reference?)
В обоих файлах ссылка на нугет есть, юзинг неймспейса есть. И
EpplusIgnore, и EpplusTableColumn находятся в одной сборке, в одном неймспейсе, и как атрибуты модели работают прекрасно.Предложения по исправлению ничем не помогают. Там только что-то в духе «Создать новый класс 'EpplusTableColumn'…».
Есть предположения, в чём проблема?
Чуть позже выложу ответ.
UPD: довольно быстро догадались, ответ в комментариях.
👍2
День 2429. #ЗаметкиНаПолях
Управляем Временем Жизни DbContext. Начало
DbContext — это сердце EF Core, но его легко использовать неправильно.
Основные правила:
- DbContext представляет собой единицу работы и должен существовать недолго.
- Он не является потокобезопасным; никогда не используйте один экземпляр совместно для параллельных операций.
- В ASP.NET Core стандартным и, как правило, правильным выбором является DbContext с ограниченной (scoped) областью действия для каждого запроса. Для задач вне области действия запроса (фоновые сервисы, одиночные объекты, UI-приложения) используйте фабрику для создания новых контекстов по требованию.
Рассмотрим, как правильно подключать DbContext, когда выбирать каждый вид регистрации и каких ловушек следует избегать.
Виды регистрации
1. AddDbContext (Scoped – по умолчанию, для каждого запроса)
Когда использовать: контроллеры MVC/минимальные API, Razor Pages, хабы SignalR — всё, что находится внутри веб-запроса.
Почему: Вы автоматически получаете отдельный контекст на каждый запрос; EF Core эффективно обрабатывает использование подключений.
2. AddDbContextFactory (Transient - фабрика для контекстов по требованию)
Использование:
Когда использовать:
- Фоновые сервисы (IHostedService, BackgroundService),
- Любые синглтон-сервисы, которым требуется DbContext,
- Десктопные/Blazor-приложения, где требуется свежий контекст для каждой операции.
Почему: Фабрики создают чистые, кратковременные контексты, не полагаясь на внешние области видимости.
3. AddDbContextPool (Scoped с пулингом)
Когда использовать: Высокопроизводительные API, когда конфигурация контекста стабильна и не сохраняет состояние.
Зачем: Повторное использование экземпляров DbContext из пула для снижения затрат на выделение ресурсов.
Внимание: не сохраняйте состояние запросов в контексте; экземпляры в пуле сбрасываются и используются повторно.
Замечания по производительности
- Пулинг уменьшает выделение памяти при высокой нагрузке; оцените свою рабочую нагрузку.
- Проверки потокобезопасности: EF Core может обнаруживать некоторые злоупотребления многопоточностью; вы можете отключить проверки для повышения производительности, но только если вы абсолютно уверены, что в одном контексте нет параллельных запросов. Обычно отключать проверки не рекомендуется.
Окончание следует…
Источник: https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
Управляем Временем Жизни DbContext. Начало
DbContext — это сердце EF Core, но его легко использовать неправильно.
Основные правила:
- DbContext представляет собой единицу работы и должен существовать недолго.
- Он не является потокобезопасным; никогда не используйте один экземпляр совместно для параллельных операций.
- В ASP.NET Core стандартным и, как правило, правильным выбором является DbContext с ограниченной (scoped) областью действия для каждого запроса. Для задач вне области действия запроса (фоновые сервисы, одиночные объекты, UI-приложения) используйте фабрику для создания новых контекстов по требованию.
Рассмотрим, как правильно подключать DbContext, когда выбирать каждый вид регистрации и каких ловушек следует избегать.
Виды регистрации
1. AddDbContext (Scoped – по умолчанию, для каждого запроса)
// Program.cs
builder.Services
.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));
Когда использовать: контроллеры MVC/минимальные API, Razor Pages, хабы SignalR — всё, что находится внутри веб-запроса.
Почему: Вы автоматически получаете отдельный контекст на каждый запрос; EF Core эффективно обрабатывает использование подключений.
2. AddDbContextFactory (Transient - фабрика для контекстов по требованию)
builder.Services
.AddDbContextFactory<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));
Использование:
public sealed class ReportService(
IDbContextFactory<AppDbContext> factory)
{
public async Task<IReadOnlyList<OrderDto>>
GetAsync(CancellationToken ct)
{
await using var db = await
factory.CreateDbContextAsync(ct);
return await db.Orders
.Where(…)
.Select(…)
.ToListAsync(ct);
}
}
Когда использовать:
- Фоновые сервисы (IHostedService, BackgroundService),
- Любые синглтон-сервисы, которым требуется DbContext,
- Десктопные/Blazor-приложения, где требуется свежий контекст для каждой операции.
Почему: Фабрики создают чистые, кратковременные контексты, не полагаясь на внешние области видимости.
3. AddDbContextPool (Scoped с пулингом)
builder.Services
.AddDbContextPool<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));
Когда использовать: Высокопроизводительные API, когда конфигурация контекста стабильна и не сохраняет состояние.
Зачем: Повторное использование экземпляров DbContext из пула для снижения затрат на выделение ресурсов.
Внимание: не сохраняйте состояние запросов в контексте; экземпляры в пуле сбрасываются и используются повторно.
Замечания по производительности
- Пулинг уменьшает выделение памяти при высокой нагрузке; оцените свою рабочую нагрузку.
- Проверки потокобезопасности: EF Core может обнаруживать некоторые злоупотребления многопоточностью; вы можете отключить проверки для повышения производительности, но только если вы абсолютно уверены, что в одном контексте нет параллельных запросов. Обычно отключать проверки не рекомендуется.
Окончание следует…
Источник: https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
1👍36
День 2430. #ЗаметкиНаПолях
Управляем Временем Жизни DbContext. Окончание
Начало
Примеры из реального мира
1. Контроллер / Минимальные API (Scoped)
DI даёт вам scoped-контекст, привязанный к запросу. Одна единица работы, никаких утечек:
2. Фоновые сервисы
Фоновые сервисы не имеют области видимости запроса, вы должны создать область видимости:
Либо использовать фабрику:
3. Синглтон-сервис, которому нужна БД
Синглтоны не должны захватывать DbContext. Создание нового при каждом вызове помогает избежать проблем:
4. Высокопроизводительные APIs (пулинг)
Совет: Пулинг улучшает пропускную способность. Он не делает DbContext потокобезопасным. По-прежнему один контекст на запрос.
Распространённые ошибки
1. Совместное использование одного DbContext между потоками
Симптом: "A second operation started on this context before a previous operation was completed." (Вторая операция началась в этом контексте до завершения предыдущей операции).
Решение: один контекст на единицу работы; не выполняйте параллельные запросы в одном и том же контексте.
2. Внедрение DbContext в синглтоны
Решение: внедрение IDbContextFactory или IServiceScopeFactory и создание контекстов по требованию.
3. Долгоживущие контексты
Решение: делайте контексты короткоживущими, иначе раздуваются трекеры изменений и удерживаются соединения.
4. Использование пула с состоянием запросов
Решение: не добавляйте пользовательское состояние к контексту (например, поле CurrentUserId). Контексты в пуле переиспользуются.
5. Попытка «ускорения» путём одновременного выполнения нескольких запросов в одном контексте.
Решение: либо сериализуйте работу, либо создайте несколько контекстов. DbContext не является потокобезопасным.
Итого
Думайте о DbContext как о листке-черновике: вы берёте лист, используете для коротких записей, и выбрасываете его, когда закончите. Не передавайте его по всему офису и не пытайтесь писать на нём двумя ручками одновременно.
Источник: https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
Управляем Временем Жизни DbContext. Окончание
Начало
Примеры из реального мира
1. Контроллер / Минимальные API (Scoped)
DI даёт вам scoped-контекст, привязанный к запросу. Одна единица работы, никаких утечек:
app.MapGet("/orders/{id:int}",
async (int id,
AppDbContext db,
CancellationToken ct) =>
{
var order = await db
.Orders.FindAsync([id], ct);
return order is null
? Results.NotFound()
: Results.Ok(order);
});2. Фоновые сервисы
Фоновые сервисы не имеют области видимости запроса, вы должны создать область видимости:
public sealed class CleanupService(
IServiceScopeFactory scopes,
ILogger<CleanupService> log)
: BackgroundService
{
protected override async Task
ExecuteAsync(CancellationToken stop)
{
while (!stop.IsCancellationRequested)
{
using var scope = scopes.CreateScope();
var db = scope
.ServiceProvider
.GetRequiredService<AppDbContext>();
// Одна короткоживущая единица работы
// … используем db …
await db.SaveChangesAsync(stop);
await Task.Delay(TimeSpan.FromHours(6), stop);
}
}
}
Либо использовать фабрику:
public sealed class CleanupService(
IDbContextFactory<AppDbContext> factory,
ILogger<CleanupService> log)
: BackgroundService
{
protected override async Task
ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await using var db = await
factory.CreateDbContextAsync(ct);
// … та же логика, что и выше …
}
}
}
3. Синглтон-сервис, которому нужна БД
Синглтоны не должны захватывать DbContext. Создание нового при каждом вызове помогает избежать проблем:
csharp
public sealed class PricingCache(
IDbContextFactory<AppDbContext> factory,
IMemoryCache cache)
{
public async Task<decimal>
GetPriceAsync(int id, CancellationToken ct)
{
if (cache.TryGetValue(id, out var price))
return price;
await using var db = await
factory.CreateDbContextAsync(ct);
price = await db.Products
.Where(p => p.Id == id)
.Select(p => p.Price)
.FirstAsync(ct);
cache.Set(id, price, TimeSpan.FromMinutes(10));
return price;
}
}
4. Высокопроизводительные APIs (пулинг)
builder.Services
.AddDbContextPool<AppDbContext>(o =>
{
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default"));
// Не используйте состояние в опциях
// Избегайте изменяемого состояния
});
Совет: Пулинг улучшает пропускную способность. Он не делает DbContext потокобезопасным. По-прежнему один контекст на запрос.
Распространённые ошибки
1. Совместное использование одного DbContext между потоками
Симптом: "A second operation started on this context before a previous operation was completed." (Вторая операция началась в этом контексте до завершения предыдущей операции).
Решение: один контекст на единицу работы; не выполняйте параллельные запросы в одном и том же контексте.
2. Внедрение DbContext в синглтоны
Решение: внедрение IDbContextFactory или IServiceScopeFactory и создание контекстов по требованию.
3. Долгоживущие контексты
Решение: делайте контексты короткоживущими, иначе раздуваются трекеры изменений и удерживаются соединения.
4. Использование пула с состоянием запросов
Решение: не добавляйте пользовательское состояние к контексту (например, поле CurrentUserId). Контексты в пуле переиспользуются.
5. Попытка «ускорения» путём одновременного выполнения нескольких запросов в одном контексте.
Решение: либо сериализуйте работу, либо создайте несколько контекстов. DbContext не является потокобезопасным.
Итого
Думайте о DbContext как о листке-черновике: вы берёте лист, используете для коротких записей, и выбрасываете его, когда закончите. Не передавайте его по всему офису и не пытайтесь писать на нём двумя ручками одновременно.
Источник: https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
👍26
День 2431. #ЗаметкиНаПолях
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Начало
Если вы уже какое-то время работаете в мире .NET, то знаете, что эта среда выполнения полна скрытых жемчужин. Некоторые из них отточены и дружелюбны, разработаны для того, чтобы новички чувствовали себя в безопасности. Другие — сырые, но мощные инструменты, которым «не обучают в школе», но они дают вам невероятную мощь, если вы знаете, когда и как их использовать. Разберём 5 малоизвестных API, которые могут помочь вам выжать гораздо больше производительности из кода, интенсивно использующего память. Эти функции имеют реальное значение в высокопроизводительных сервисах, конвейерах с малой задержкой и любых приложениях, которые используют много памяти.
1. CollectionsMarshal.AsSpan() для доступа к списку
Обычно, работая с List<T>, вы представляете его себе как удобную и безопасную абстракцию над массивом. Добавляете данные, проходите по списку. Но что, если вам нужен прямой доступ к базовому массиву без копирования?
Стандартный способ:
ToArray() создаёт полную копию содержимого списка, тем самым удваивая размер используемой памяти.
Способ лучше:
Почему: здесь мы работаем непосредственно с внутренним хранилищем списка. Никакого копирования, никаких лишних выделений памяти. Есть одна проблема: если список изменит размер (например, при добавлении нового элемента), span станет недействительным. Именно поэтому этот API находится в System.Runtime.InteropServices.
Когда использовать: при обработке больших буферов или выполнении коротких циклов. Это избавляет от ненужных копий, снижает нагрузку на сборщик мусора и повышает производительность.
Выигрыш: в 2 раза по памяти.
2. CollectionsMarshal.GetValueRefOrNullRef для доступа к словарю
Словари повсюду в приложениях .NET. И чаще всего мы просто делаем что-то вроде этого:
Выглядит безобидно, но мы ищем ключ дважды: в TryGetValue и в индексаторе.
Способ лучше:
Почему: С помощью GetValueRefOrNullRef вы получаете ссылку прямо на хранилище словаря. Без повторного поиска, без избыточного вычисления хэша и без лишних записей.
Когда использовать: при работе с большими словарями или циклами, критически важными для производительности (например, с таблицей символов компилятора или кэшем), эти ненужные поиски накапливаются. Этот подход быстрее, но, опять же, слово Unsafe разработчиками языка используется не просто так. Обращайтесь с ref’ами осторожно.
Выигрыш: примерно в 2 раза.
См. также Альтернативный Доступ к Коллекциям в C#13
Окончание следует…
Источник: https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Начало
Если вы уже какое-то время работаете в мире .NET, то знаете, что эта среда выполнения полна скрытых жемчужин. Некоторые из них отточены и дружелюбны, разработаны для того, чтобы новички чувствовали себя в безопасности. Другие — сырые, но мощные инструменты, которым «не обучают в школе», но они дают вам невероятную мощь, если вы знаете, когда и как их использовать. Разберём 5 малоизвестных API, которые могут помочь вам выжать гораздо больше производительности из кода, интенсивно использующего память. Эти функции имеют реальное значение в высокопроизводительных сервисах, конвейерах с малой задержкой и любых приложениях, которые используют много памяти.
1. CollectionsMarshal.AsSpan() для доступа к списку
Обычно, работая с List<T>, вы представляете его себе как удобную и безопасную абстракцию над массивом. Добавляете данные, проходите по списку. Но что, если вам нужен прямой доступ к базовому массиву без копирования?
Стандартный способ:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Нужен Span? Придётся делать копию
Span<int> span = numbers.ToArray().AsSpan();
// вариант 2, но всё равно копия
Span<int> span1 = [.. numbers];
// … работа со span’омToArray() создаёт полную копию содержимого списка, тем самым удваивая размер используемой памяти.
Способ лучше:
using System.Runtime.InteropServices;
…
// Span напрямую над массивом
Span<int> span = CollectionsMarshal.AsSpan(numbers);
…
Почему: здесь мы работаем непосредственно с внутренним хранилищем списка. Никакого копирования, никаких лишних выделений памяти. Есть одна проблема: если список изменит размер (например, при добавлении нового элемента), span станет недействительным. Именно поэтому этот API находится в System.Runtime.InteropServices.
Когда использовать: при обработке больших буферов или выполнении коротких циклов. Это избавляет от ненужных копий, снижает нагрузку на сборщик мусора и повышает производительность.
Выигрыш: в 2 раза по памяти.
2. CollectionsMarshal.GetValueRefOrNullRef для доступа к словарю
Словари повсюду в приложениях .NET. И чаще всего мы просто делаем что-то вроде этого:
if (dict.TryGetValue("foo", out var value))
{
value++;
dict["foo"] = value;
}Выглядит безобидно, но мы ищем ключ дважды: в TryGetValue и в индексаторе.
Способ лучше:
using System.Runtime.InteropServices;
…
ref int valueRef =
ref CollectionsMarshal
.GetValueRefOrNullRef(dict, "foo");
if (!Unsafe.IsNullRef(ref valueRef))
valueRef++;
Почему: С помощью GetValueRefOrNullRef вы получаете ссылку прямо на хранилище словаря. Без повторного поиска, без избыточного вычисления хэша и без лишних записей.
Когда использовать: при работе с большими словарями или циклами, критически важными для производительности (например, с таблицей символов компилятора или кэшем), эти ненужные поиски накапливаются. Этот подход быстрее, но, опять же, слово Unsafe разработчиками языка используется не просто так. Обращайтесь с ref’ами осторожно.
Выигрыш: примерно в 2 раза.
См. также Альтернативный Доступ к Коллекциям в C#13
Окончание следует…
Источник: https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
1👍33
День 2432. #ЗаметкиНаПолях
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Окончание
Начало
3. GC.AllocateUninitializedArray<T> для более быстрой работы с массивами
При создании нового массива в .NET среда выполнения обнуляет его. Обычно это хорошо, предсказуемое поведение, никаких мусорных данных. Но что, если вам не нужно изначальное содержимое, ведь вы всё равно перезапишете каждый элемент? Обнуление — пустая трата времени.
Стандартный способ:
Способ лучше:
Почему: здесь среда выполнения пропускает очистку памяти. Это ускоряет выделение памяти, особенно для больших массивов.
Когда использовать: требуется дисциплина. Если вы читаете массив перед записью, вы получите мусорные значения. Но в таких сценариях, как сериализация, повторное использование буфера или краткие циклы с вычислениями, это может сэкономить время.
Выигрыш: ~15% для 1000 элементов.
4. ArrayPool<T>.Shared и IMemoryOwner
Если вы многократно выделяете большие массивы в цикле, сборщик мусора вас возненавидит. Именно здесь на помощь приходит ArrayPool<T> — общий пул массивов, который можно арендовать и вернуть.
Стандартный способ:
Это приведёт к тысяче аллокаций, которые захламят кучу.
Способ лучше:
Почему: мы арендуем буфер из пула, используем его и возвращаем при уничтожении IMemoryOwner<T>. Никакие выделения памяти не перегружают сборщик мусора.
Когда использовать: особенно актуально в сценариях с высокой пропускной способностью, таких как сетевые серверы, конвейеры или обработка видео. Кроме того, это безопаснее, чем просто арендовать и возвращать массивы, поскольку IMemoryOwner<T> гарантирует корректную очистку.
Выигрыш: более чем в 2 раза быстрее и 1 аллокация вместо 1000.
5. ObjectPool<T> для дорогостоящих объектов
Создание некоторых объектов требует больших затрат. Если вы постоянно создаёте и уничтожаете их, вы тратите не только память, но и ресурсы процессора. Поможет ObjectPool<T>.
Без пулинга:
Это создаст 100 экземпляров StringBuilder. Они будут собраны GC, но зачем создавать столько?
Пулинг:
Почему: мы переиспользуем небольшой пул объектов StringBuilder. Выделение памяти значительно сокращается, и мы избегаем постоянного обращения к GC.
Когда использовать: фреймворки логирования, библиотеки сериализации или любые другие рабочие нагрузки, требующие создания множества временных объектов.
Выигрыш: примерно в 4 раза быстрее и в 50 раз меньше по памяти.
Итого
Если вы когда-нибудь заглядывали в свой профилировщик и задавались вопросом, почему так много времени тратится на сборку мусора или копирование памяти, эти приёмы помогут вам это исправить. Они подходят не для всех ситуаций, но, когда вы гонитесь за реальной производительностью, именно эти инструменты отличают «неплохое» приложение от действительно эффективного.
Источник: https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Окончание
Начало
3. GC.AllocateUninitializedArray<T> для более быстрой работы с массивами
При создании нового массива в .NET среда выполнения обнуляет его. Обычно это хорошо, предсказуемое поведение, никаких мусорных данных. Но что, если вам не нужно изначальное содержимое, ведь вы всё равно перезапишете каждый элемент? Обнуление — пустая трата времени.
Стандартный способ:
// Автоматически обнуляется
int[] arr = new int[1000];
for (int i = 0; i < arr.Length; i++)
arr[i] = i; // Перезаписываем всё
Способ лучше:
int[] arr = GC.AllocateUninitializedArray<int>(1000);
for (int i = 0; i < arr.Length; i++)
arr[i] = i;
Почему: здесь среда выполнения пропускает очистку памяти. Это ускоряет выделение памяти, особенно для больших массивов.
Когда использовать: требуется дисциплина. Если вы читаете массив перед записью, вы получите мусорные значения. Но в таких сценариях, как сериализация, повторное использование буфера или краткие циклы с вычислениями, это может сэкономить время.
Выигрыш: ~15% для 1000 элементов.
4. ArrayPool<T>.Shared и IMemoryOwner
Если вы многократно выделяете большие массивы в цикле, сборщик мусора вас возненавидит. Именно здесь на помощь приходит ArrayPool<T> — общий пул массивов, который можно арендовать и вернуть.
Стандартный способ:
for (int i = 0; i < 1000; i++)
{
// аллокации каждый раз
var buffer = new byte[1024];
DoSomething(buffer);
}
Это приведёт к тысяче аллокаций, которые захламят кучу.
Способ лучше:
using System.Buffers;
for (int i = 0; i < 1000; i++)
{
using IMemoryOwner<byte> owner =
MemoryPool<byte>.Shared.Rent(1024);
var memory = owner.Memory;
DoSomething(memory.Span);
}
Почему: мы арендуем буфер из пула, используем его и возвращаем при уничтожении IMemoryOwner<T>. Никакие выделения памяти не перегружают сборщик мусора.
Когда использовать: особенно актуально в сценариях с высокой пропускной способностью, таких как сетевые серверы, конвейеры или обработка видео. Кроме того, это безопаснее, чем просто арендовать и возвращать массивы, поскольку IMemoryOwner<T> гарантирует корректную очистку.
Выигрыш: более чем в 2 раза быстрее и 1 аллокация вместо 1000.
5. ObjectPool<T> для дорогостоящих объектов
Создание некоторых объектов требует больших затрат. Если вы постоянно создаёте и уничтожаете их, вы тратите не только память, но и ресурсы процессора. Поможет ObjectPool<T>.
Без пулинга:
for (int i = 0; i < 100; i++)
{
var sb = new StringBuilder(1024);
sb.Append("Hello ").Append(i);
Console.WriteLine(sb.ToString());
}
Это создаст 100 экземпляров StringBuilder. Они будут собраны GC, но зачем создавать столько?
Пулинг:
using Microsoft.Extensions.ObjectPool;
var provider = new DefaultObjectPoolProvider();
var pool = provider.CreateStringBuilderPool();
for (int i = 0; i < 100; i++)
{
var sb = pool.Get();
sb.Clear();
sb.Append("Hello ").Append(i);
Console.WriteLine(sb.ToString());
pool.Return(sb);
}
Почему: мы переиспользуем небольшой пул объектов StringBuilder. Выделение памяти значительно сокращается, и мы избегаем постоянного обращения к GC.
Когда использовать: фреймворки логирования, библиотеки сериализации или любые другие рабочие нагрузки, требующие создания множества временных объектов.
Выигрыш: примерно в 4 раза быстрее и в 50 раз меньше по памяти.
Итого
Если вы когда-нибудь заглядывали в свой профилировщик и задавались вопросом, почему так много времени тратится на сборку мусора или копирование памяти, эти приёмы помогут вам это исправить. Они подходят не для всех ситуаций, но, когда вы гонитесь за реальной производительностью, именно эти инструменты отличают «неплохое» приложение от действительно эффективного.
Источник: https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
👍47
День 2433. #SystemDesign101 #Microservices
Как Выглядит Типичная Микросервисная Архитектура?
Балансировщик нагрузки: устройство или приложение, которое распределяет сетевой или прикладной трафик между несколькими серверами.
CDN (Сеть Доставки Контента): группа географически распределённых серверов, которые обеспечивают быструю доставку статического и динамического контента. С CDN пользователям не нужно загружать контент (музыку, видео, файлы, изображения и т. д.) с исходного сервера. Вместо этого контент кэшируется на узлах CDN по всему миру, и пользователи могут загружать его с ближайших узлов CDN.
API-шлюз: обрабатывает входящие запросы и направляет их соответствующим сервисам. Он взаимодействует с провайдером идентификации и выполняет обнаружение сервисов. См. подробнее.
Провайдер идентификации: отвечает за аутентификацию и авторизацию пользователей.
Регистрация и обнаружение сервисов (Service Registry и Service Discovery): Service Registry — это база данных, которая хранит информацию о сервисах и их экземплярах, а Service Discovery — это механизм, использующий этот реестр для автоматического обнаружения, регистрации и отслеживания доступности сервисов в распределенной системе, что особенно важно для микросервисных архитектур, где сервисы могут динамически масштабироваться. API-шлюз ищет соответствующие сервисы в этом компоненте для взаимодействия с ними.
Менеджмент: этот компонент отвечает за мониторинг сервисов.
Микросервисы: микросервисы разрабатываются и развёртываются в разных доменах. Каждый домен имеет свою собственную базу данных. API-шлюз взаимодействует с микросервисами через REST API или другие протоколы, а микросервисы в пределах одного домена взаимодействуют друг с другом с помощью RPC (удалённого вызова процедур).
Преимущества микросервисов
- Их можно быстро проектировать, развёртывать и горизонтально масштабировать.
- Каждый домен может независимо поддерживаться выделенной командой.
- Бизнес-требования можно настраивать в каждом домене, что обеспечивает лучшую поддержку.
Источник: https://blog.bytebytego.com
Как Выглядит Типичная Микросервисная Архитектура?
Балансировщик нагрузки: устройство или приложение, которое распределяет сетевой или прикладной трафик между несколькими серверами.
CDN (Сеть Доставки Контента): группа географически распределённых серверов, которые обеспечивают быструю доставку статического и динамического контента. С CDN пользователям не нужно загружать контент (музыку, видео, файлы, изображения и т. д.) с исходного сервера. Вместо этого контент кэшируется на узлах CDN по всему миру, и пользователи могут загружать его с ближайших узлов CDN.
API-шлюз: обрабатывает входящие запросы и направляет их соответствующим сервисам. Он взаимодействует с провайдером идентификации и выполняет обнаружение сервисов. См. подробнее.
Провайдер идентификации: отвечает за аутентификацию и авторизацию пользователей.
Регистрация и обнаружение сервисов (Service Registry и Service Discovery): Service Registry — это база данных, которая хранит информацию о сервисах и их экземплярах, а Service Discovery — это механизм, использующий этот реестр для автоматического обнаружения, регистрации и отслеживания доступности сервисов в распределенной системе, что особенно важно для микросервисных архитектур, где сервисы могут динамически масштабироваться. API-шлюз ищет соответствующие сервисы в этом компоненте для взаимодействия с ними.
Менеджмент: этот компонент отвечает за мониторинг сервисов.
Микросервисы: микросервисы разрабатываются и развёртываются в разных доменах. Каждый домен имеет свою собственную базу данных. API-шлюз взаимодействует с микросервисами через REST API или другие протоколы, а микросервисы в пределах одного домена взаимодействуют друг с другом с помощью RPC (удалённого вызова процедур).
Преимущества микросервисов
- Их можно быстро проектировать, развёртывать и горизонтально масштабировать.
- Каждый домен может независимо поддерживаться выделенной командой.
- Бизнес-требования можно настраивать в каждом домене, что обеспечивает лучшую поддержку.
Источник: https://blog.bytebytego.com
👍8