День 2399. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Продолжение
Начало
Продолжение
3. Пакет ServiceDefaults
При создании нескольких веб-API или рабочих процессов вам может потребоваться повторять одну и ту же конфигурацию для каждого проекта. Например, потребуется добавить Azure AppConfiguration, настроить OpenTelemetry, логирование, проверки работоспособности, и т.п. Вместо того, чтобы дублировать эту конфигурацию во всех проектах, вы можете создать общий пакет ServiceDefaults, содержащий общую конфигурацию, и использовать его в каждом проекте. Главное преимущество — возможность лёгкого обновления с течением времени. Если вам нужно изменить конфигурацию OpenTelemetry, вы можете сделать это в пакете, и все проекты, которые на него ссылаются, обновятся.
Если вы знакомы с шаблоном .NET Aspire, вы, возможно, видели проект ServiceDefaults. Он предоставляет набор общих сервисов и конфигураций, которые можно использовать повторно в нескольких проектах в рамках одного решения. Но можно ведь создать NuGet-пакет, который можно использовать в любом проекте.
Пример проекта пакета на GitHub.
4. Задачи MSBuild
MSBuild — это расширяемая система сборки, позволяющая создавать пользовательские задачи и таргеты. На сайте nuget.org представлено несколько пакетов для добавления задач MSBuild в ваш проект. Например, вы можете автоматически устанавливать версию своего проекта с помощью MinVer, GitVersion.MsBuild или Nerdbank.GitVersioning. А, например, пакет Workleap.OpenApi.MSBuild интегрируется в процесс сборки (dotnet build), чтобы гарантировать актуальность файла спецификации OpenAPI, соответствие коду, стандартам компании и отсутствие критических изменений в API.
5. Фрагменты файлов
Иногда удалить шаблонный код невозможно. В этом случае стратегия заключается в том, чтобы поместить его в «область», которую можно легко идентифицировать и обновить. Версионирование и обновление общей части файла можно осуществлять с помощью такого инструмента, как Renovate.
Общие части файлов заключаются в комментарии
6. Конвейеры CI/CD
Конвейеры — ещё один хороший вариант для совместного использования между проектами. Существуют различные стратегии совместного использования конвейеров.
Комплаенс-конвейеры (например, SAST, DAST, сканирование зависимостей и т.д.) можно перенести на уровень организации. Это гарантирует, что все проекты будут иметь одинаковые проверки соответствия, и вы сможете обновлять их в одном месте. Это также избавит от необходимости обновлять каждый проект при изменении проверок соответствия.
GitHub предоставляет гибкие способы совместного использования частей конвейеров CI/CD:
- композитные действия
- настраиваемые действия
- повторно используемые рабочие процессы
Окончание следует…
Источник: https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
  Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Продолжение
Начало
Продолжение
3. Пакет ServiceDefaults
При создании нескольких веб-API или рабочих процессов вам может потребоваться повторять одну и ту же конфигурацию для каждого проекта. Например, потребуется добавить Azure AppConfiguration, настроить OpenTelemetry, логирование, проверки работоспособности, и т.п. Вместо того, чтобы дублировать эту конфигурацию во всех проектах, вы можете создать общий пакет ServiceDefaults, содержащий общую конфигурацию, и использовать его в каждом проекте. Главное преимущество — возможность лёгкого обновления с течением времени. Если вам нужно изменить конфигурацию OpenTelemetry, вы можете сделать это в пакете, и все проекты, которые на него ссылаются, обновятся.
Если вы знакомы с шаблоном .NET Aspire, вы, возможно, видели проект ServiceDefaults. Он предоставляет набор общих сервисов и конфигураций, которые можно использовать повторно в нескольких проектах в рамках одного решения. Но можно ведь создать NuGet-пакет, который можно использовать в любом проекте.
Пример проекта пакета на GitHub.
4. Задачи MSBuild
MSBuild — это расширяемая система сборки, позволяющая создавать пользовательские задачи и таргеты. На сайте nuget.org представлено несколько пакетов для добавления задач MSBuild в ваш проект. Например, вы можете автоматически устанавливать версию своего проекта с помощью MinVer, GitVersion.MsBuild или Nerdbank.GitVersioning. А, например, пакет Workleap.OpenApi.MSBuild интегрируется в процесс сборки (dotnet build), чтобы гарантировать актуальность файла спецификации OpenAPI, соответствие коду, стандартам компании и отсутствие критических изменений в API.
5. Фрагменты файлов
Иногда удалить шаблонный код невозможно. В этом случае стратегия заключается в том, чтобы поместить его в «область», которую можно легко идентифицировать и обновить. Версионирование и обновление общей части файла можно осуществлять с помощью такого инструмента, как Renovate.
Общие части файлов заключаются в комментарии
# reference:<URL> и # endreference. Таким образом, вы можете легко найти общую часть файла и обновить её при необходимости. Версию общей части файла можно контролировать с помощью тегов и обновлять с помощью Renovate. Обратите внимание, что Renovate позволяет запускать пользовательские инструменты после обновления ссылки.
# reference:https://…/.editorconfig
root = true
[*]
indent_style = space
trim_trailing_whitespace = true
end_of_line = lf
…
# endreference
# Настройки специфичные для проекта
[*.cs]
dotnet_diagnostic.CA1008.severity = none
…
6. Конвейеры CI/CD
Конвейеры — ещё один хороший вариант для совместного использования между проектами. Существуют различные стратегии совместного использования конвейеров.
Комплаенс-конвейеры (например, SAST, DAST, сканирование зависимостей и т.д.) можно перенести на уровень организации. Это гарантирует, что все проекты будут иметь одинаковые проверки соответствия, и вы сможете обновлять их в одном месте. Это также избавит от необходимости обновлять каждый проект при изменении проверок соответствия.
GitHub предоставляет гибкие способы совместного использования частей конвейеров CI/CD:
- композитные действия
- настраиваемые действия
- повторно используемые рабочие процессы
Окончание следует…
Источник: https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
День 2400. #ЗаметкиНаПолях
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Окончание
Начало
Продолжение 1-2
Продолжение 3-5
7. Dockerfile
Вам действительно нужен Dockerfile? Некоторые технологии, такие как .NET, позволяют создавать образы Docker без Dockerfile. Например, можно использовать dotnet publish для создания образа и его отправки в реестр. Это не только избавляет от шаблонного кода, но и повышает производительность. Это также позволяет избежать многих распространённых проблем с Dockerfile, таких как кэширование.
8. Helm-чарты
Поскольку вы можете устанавливать стандарты с помощью пакетов MSBuild или SDK, вы можете повторно использовать эти стандарты в Helm-чартах. Например, вы можете настроить проверки готовности и жизнеспособности в общем чарте. Вы также можете добавить параметры, специфичные для вашей инфраструктуры, такие как сертификаты, автоматическое масштабирование, идентификацию пода Azure и т.д. Таким образом, вы можете удалить шаблонный код из Helm-чартов и сделать их более пригодными для повторного использования в разных проектах. В большинстве проектов требуется задать имя образа, ограничения на ресурсы ЦП и памяти. Остальные параметры должны быть общими для большинства проектов.
9. Модули PowerShell
PowerShell — мощный язык сценариев, который можно использовать для автоматизации задач и управления системами. Он очень распространён в непрерывной интеграции (CI) или для локальных операций. Модули PowerShell — отличный способ совместного использования скриптов и функций в разных проектах. Вы можете создать модуль, содержащий общие функции и скрипты, которые можно использовать повторно в нескольких проектах, используя команду
Модули имеют версии и публикуются как NuGet-пакеты, поэтому вы можете легко обновлять их при необходимости:
- Publish-Module
- Install-Module
Обновление существующих проектов
Предоставление общих библиотек и конфигурации — это здорово, но для их использования также необходимо обновить существующие проекты. Это может быть непросто, поскольку вам, возможно, придётся обновлять сотни проектов. Лучший способ сделать это — создать инструмент, который будет автоматически это делать. Вот пример кода такого инструмента. Он клонирует все репозитории и применяет к ним миграцию. Затем создаёт пулл-реквест с изменениями. Некоторые миграции просты и могут быть написаны детерминированным способом. Для более сложных миграций скрипт миграции может использовать ИИ для помощи.
Обратите внимание, что вы можете применить стратегию ограничения изменений, чтобы избежать перегрузки не только сервера Git слишком большим количеством пулл-реквестов одновременно, но и системы сборки слишком большим количеством задач. Это помешает команде разработчиков работать над другими задачами во время выполнения миграции.
Итого
Просмотрите свои проекты и определите, есть ли что-то ещё, что можно использовать по ссылке. Например:
- Конфигурациями IDE (например, .vscode, vsconfig для Visual Studio)
- Git-хуки
- Инструменты разработки, необходимые для запуска проекта (например, dev-контейнеры, GitHub Codespaces, devbox и т. д.). Фактически, обратите внимание на всё, что не является бизнес-кодом, и подумайте, можно ли удалить это из проекта.
Источник: https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
Сокращаем Шаблонный Код и Поддерживаем Согласованность Проекта. Окончание
Начало
Продолжение 1-2
Продолжение 3-5
7. Dockerfile
Вам действительно нужен Dockerfile? Некоторые технологии, такие как .NET, позволяют создавать образы Docker без Dockerfile. Например, можно использовать dotnet publish для создания образа и его отправки в реестр. Это не только избавляет от шаблонного кода, но и повышает производительность. Это также позволяет избежать многих распространённых проблем с Dockerfile, таких как кэширование.
# Dockerfile не нужен
dotnet publish -p:PublishProfile=DefaultContainer
8. Helm-чарты
Поскольку вы можете устанавливать стандарты с помощью пакетов MSBuild или SDK, вы можете повторно использовать эти стандарты в Helm-чартах. Например, вы можете настроить проверки готовности и жизнеспособности в общем чарте. Вы также можете добавить параметры, специфичные для вашей инфраструктуры, такие как сертификаты, автоматическое масштабирование, идентификацию пода Azure и т.д. Таким образом, вы можете удалить шаблонный код из Helm-чартов и сделать их более пригодными для повторного использования в разных проектах. В большинстве проектов требуется задать имя образа, ограничения на ресурсы ЦП и памяти. Остальные параметры должны быть общими для большинства проектов.
9. Модули PowerShell
PowerShell — мощный язык сценариев, который можно использовать для автоматизации задач и управления системами. Он очень распространён в непрерывной интеграции (CI) или для локальных операций. Модули PowerShell — отличный способ совместного использования скриптов и функций в разных проектах. Вы можете создать модуль, содержащий общие функции и скрипты, которые можно использовать повторно в нескольких проектах, используя команду
Import-Module -Name <ModuleName> -RequiredVersion <ModuleVersion>
Модули имеют версии и публикуются как NuGet-пакеты, поэтому вы можете легко обновлять их при необходимости:
- Publish-Module
- Install-Module
Обновление существующих проектов
Предоставление общих библиотек и конфигурации — это здорово, но для их использования также необходимо обновить существующие проекты. Это может быть непросто, поскольку вам, возможно, придётся обновлять сотни проектов. Лучший способ сделать это — создать инструмент, который будет автоматически это делать. Вот пример кода такого инструмента. Он клонирует все репозитории и применяет к ним миграцию. Затем создаёт пулл-реквест с изменениями. Некоторые миграции просты и могут быть написаны детерминированным способом. Для более сложных миграций скрипт миграции может использовать ИИ для помощи.
Обратите внимание, что вы можете применить стратегию ограничения изменений, чтобы избежать перегрузки не только сервера Git слишком большим количеством пулл-реквестов одновременно, но и системы сборки слишком большим количеством задач. Это помешает команде разработчиков работать над другими задачами во время выполнения миграции.
Итого
Просмотрите свои проекты и определите, есть ли что-то ещё, что можно использовать по ссылке. Например:
- Конфигурациями IDE (например, .vscode, vsconfig для Visual Studio)
- Git-хуки
- Инструменты разработки, необходимые для запуска проекта (например, dev-контейнеры, GitHub Codespaces, devbox и т. д.). Фактически, обратите внимание на всё, что не является бизнес-кодом, и подумайте, можно ли удалить это из проекта.
Источник: https://www.meziantou.net/reduce-boilerplate-and-maintain-project-consistency.htm
👍4
  День 2401. #ЗаметкиНаПолях
Разница Между Выражениями и Инициализаторами Коллекций
Вы когда-нибудь задумывались, есть ли разница между выражением коллекции:
и инициализатором коллекции:
Давайте выясним. Для этого посмотрим, во что преобразуется каждая строчка.
Выражение коллекции:
Инициализатор коллекции:
Таким образом, выражения для коллекций ([1,2,3]) «умнее» и быстрее в том смысле, что они заранее выделяют список с точным количеством элементов, которые мы хотим добавить. Инициализатор коллекции не делает этого по одной простой причине: компилятор, согласно своей спецификации, обязан вызывать метод Add для каждого элемента в инициализаторе.
Конечно, для 3х элементов особой разницы в производительности не будет. А вот, например, бенчмарк для 17 элементов:
Вопрос, почему здесь инициализатор тратит больше времени и памяти, любят задавать на собесах. Кто знает, пишите в комментариях.
Источник: https://steven-giesel.com/blogPost/fea0b033-ccf5-4197-b62c-ffd8ca6d79c7/quick-one-difference-between-collection-expressions-and-collection-initializer
Разница Между Выражениями и Инициализаторами Коллекций
Вы когда-нибудь задумывались, есть ли разница между выражением коллекции:
List<int> list = [1, 2, 3];
и инициализатором коллекции:
List<int> list2 = new() {1,2,3};Давайте выясним. Для этого посмотрим, во что преобразуется каждая строчка.
Выражение коллекции:
int num = 3;
List<int> list = new List<int>(num);
CollectionsMarshal.SetCount(list, num);
Span<int> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = 1;
num2++;
span[num2] = 2;
num2++;
span[num2] = 3;
num2++;
Инициализатор коллекции:
List<int> list2 = new List<int>();
list2.Add(1);
list2.Add(2);
list2.Add(3);
Таким образом, выражения для коллекций ([1,2,3]) «умнее» и быстрее в том смысле, что они заранее выделяют список с точным количеством элементов, которые мы хотим добавить. Инициализатор коллекции не делает этого по одной простой причине: компилятор, согласно своей спецификации, обязан вызывать метод Add для каждого элемента в инициализаторе.
Конечно, для 3х элементов особой разницы в производительности не будет. А вот, например, бенчмарк для 17 элементов:
| Method | Mean | Allocated |
|-----------|---------:|----------:|
|Expression | 18.09 ns | 128 B |
|Initializer| 72.88 ns | 368 B |
Вопрос, почему здесь инициализатор тратит больше времени и памяти, любят задавать на собесах. Кто знает, пишите в комментариях.
Источник: https://steven-giesel.com/blogPost/fea0b033-ccf5-4197-b62c-ffd8ca6d79c7/quick-one-difference-between-collection-expressions-and-collection-initializer
👍41
  День 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
  День 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
  День 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
  День 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
  