Entity Framework обладает достаточно богатым встроенным языком запросов, позволяющим решать широкий спектр задач. Но бывают ситуации, когда либо сгенерированные с его помощью запросы оказываются недостаточно оптимальными, либо возможности EF покрывают не весь спектр функционала базы.
В этом случае приходится либо генерировать запросы вручную (напрямую, либо с помощью хранимых процедур), либо прибегать к дополнительной обработке сгенерированных запросов с помощью механизма интерсепторов. Рассмотрим работу этого механизма подробнее.
Немного предыстории
Мне пришлось разбираться с интерсепторами, когда возникла необходимость применить запрос вида SELECT … FOR UPDATE в Postgres. Хотя похожий функционал есть и в SQL Server, EF не поддерживает его «из коробки».
Концепт FOR UPDATE прост, несмотря на сложные механизмы «под капотом». Мы просто говорим, что хотим выбрать какие-либо записи для последующего изменения. Начиная с этого момента, и пока мы не отпустим тем или иным способом транзакцию, их нельзя менять, или выбирать в другом SELECT … FOR UPDATE за пределами текущей транзакции. При этом обычные SELECT работают вполне успешно, равно как и изменение других записей.
На уровне кода всё это достигается добавлением FOR UPDATE в конец запроса, но, как я уже сказал, в Entity Framework нет такого функционала. Зато есть функционал интерсепторов, с помощью которого можно решить поставленную задачу.
Интерсепторы
Принцип работы интерсепторов в EF следующий. Интерсепторы перехватывают и позволяют модифицировать сгенерированный SQL запрос перед его отправкой в базу.

По сути, нужно сделать три действия:
- Объявить класс-интерсептор, который будет модифицировать наш SQL-запрос нужным образом 
- Зарегистрировать интерсептор в системе для указанного контекста работы с базой 
- Как-то передать в него информацию о необходимости активации (если мы не хотим все запросы делать с этой модификацией) 
В последнем хорошо помогает механизм тегов. Мы можем промаркировать любой запрос дополнительным тегом, который трансформируется в запросе в комментарий SQL. В результате можем, например, создать класс с расширяющим методом вида:
public static class CommandExtensions
{
    public static IQueryable<T> ForUpdate<T>(this IQueryable<T> set) where T : class
    {
        return set.TagWith("FOR UPDATE");
    }
}И использовать его примерно следующим образом:
using (ApplicationContext db = new ApplicationContext())
{
	var trans = db.Database.BeginTransaction();
	try
    {
        var lists = db.Metadata
            .Where(p => p.ListId == id)
            .ForUpdate()
            .ToList();
		
		// код модификации списка
		trans.Commit();
    }
    catch
    {
	    trans.Rollback()
	    throw;
    }
}Код интресептора будет выглядеть так. Нам лишь надо убедиться в наличии тега-комментария и применить необходимую трансформацию.
public class ForUpdateInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
      DbCommand command,
      CommandEventData eventData,
      InterceptionResult<DbDataReader> result)
    {
        if (command.CommandText.Contains("-- FOR UPDATE", StringComparison.InvariantCulture))
        {
            command.CommandText += " FOR UPDATE";
        }
        return result;
    }
}Класс можно расширить, написав трансформации и для других типов команд ADO.NET. А теперь остаётся только зарегистрировать перехватчик в нашем классе контекста.
public class ApplicationContext : DbContext
{
    public DbSet<ListMeta> Metadata { get; set; }
    public ApplicationContext()
    {
        Database.EnsureCreated();
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
	    optionsBuilder.UseNpgsql(/* connection sting */)
            .AddInterceptors(new ForUpdateInterceptor());
    }
}Вывод
Хотя интерсепторы являются в EF узкоспециализированным инструментом, в некоторых случаях они позволяют существенно экономить время и продолжать использовать EF. Особенно в ситуации, где без них пришлось бы откатываться на написание чистого SQL-кода в самом EF или в хранимых процедурах на стороне базы.
Комментарии (12)
 - ALexKud18.11.2024 13:14- Запросы тюнить лучше в нативном SQL. Иначе двойная работа, если перепроверять запрос из ЕF.  - alexander_kuznetsov Автор18.11.2024 13:14- Зависит от. Понятно, что если структура базы такая, что нагенерированный EF код надо постоянно перепроверять, то ну его нафиг. Тут, да, никакие интерсепторы не помогут - только RawSql, а если кривых запросов ещё и достаточно солидный процент от общего числа, то стоит в принципе подумать на тему отказа от EF. - Но сделать интерсептор для того же For Update вместо переписывания в 3-х местах кода на нативный SQL - вполне себе годное решение, как мне кажется. И да, я полностью согласен, что хранимка в данном случае работала бы немного быстрее. Нюанс в том, что при ожидаемой частоте апдейтов потратили бы больше времени разработчика на переписывание, чем сэкономили бы машинного времени при такой оптимизации. 
 
 - S-type18.11.2024 13:14- Хорошая статья. Коротко, без разглагольствований и лишних отступлений. И, реальный, а не высосанный из пальца пример. По больше бы таких статей. Плюсанул. 
 - kemsky18.11.2024 13:14- Выглядит как очень хрупкий солюшен, потому что неизвестно, что нагенерит еф.  - alexander_kuznetsov Автор18.11.2024 13:14- Применять FOR UPDATE на какой-нибудь сложный кросс-джойн по нескольким таблицам с агрегаторами и группировкой - не надо так. Он там просто не сработает, причём EF там будет ни при чём. Ну а что нагенерирует EF на более-менее типовой селект по одной таблице с WHERE в качестве условия - вариантов там не особо много. - Плюс сам по себе механизм интерсепторов подразумевает наличие некоторых скилов в EF и базе у человека, который пишет сам интерсептор. А дальнейшее использование ничем не отличается от каких-нибудь агрегаторов. Их точно также не везде можно использовать, и можно ошибку словить, если сделаешь это криво.  - kemsky18.11.2024 13:14- Я и не говорю что надо использовать какие-то сложные запросы, смысл в том, что еф не предсказуем в генерации даже казалось бы простых вещей, поэтому вставка чего-либо просто в конец выражения рисковое дело.  - alexander_kuznetsov Автор18.11.2024 13:14- А можно, пожалуйста, практический пример того, как более-менее современная версия EF одно выражение Where трансформирует во что-то кроме SELECT … FROM … WHERE …? Спрашиваю без малейшего стёба, так как я такой риск сознательно откидывал как несущественный. - Я прекрасно понимаю, как можно «положить» этот запрос, но честно смог придумать только четыре сценария: - Применение ForUpdate к сложносоставному запросу, к которому в принципе нельзя применять эту команду на уровне SQL. Ну как бы сам себе злобный Буратино, не надо отвёрткой гвозди забивать. 
- В будущих версиях EF решат сломать обратную совместимость при генерации запросов и, например, начать явно прописывать ; в конце. Ок, это будет не первый на моей памяти случай, когда внешняя библиотека что-то ломает. Заранее это не предскажешь, так как могут и две ; подряд поставить, например. Тут, главное, при подъёме версий библиотек автотесты не забывать гонять. 
- Смена базы данных, при которой надо вместо FOR UPDATE начинать писать совсем другой код и по другим правилам. Тут тоже заранее хз, что делать, так как смена базы дело достаточно капитальное и не зная целевую базу сложно универсальный код написать. Тем более, что тут любой raw sql или вызов хранимую отломится точно также. 
- Другой интерсептор, написанный без учёта совместимости с уже имеющимися. Например, добавляющий в конец SELECT запроса ; по принципу «а для красоты». Такое только на ревью отслеживать, плюс, опять же, автотесты. 
 
 
 
 
 
           
 
qw1
Избыточно сравнивать с локалью
https://habr.com/en/companies/skbkontur/articles/854340/
alexander_kuznetsov Автор
В данном случае - однозначно. Скорее как устоявшаяся привычка.
qw1
Всмысле, если убрать 2-й параметр у Contains, будет ещё хуже (используется локаль от текущего потока). А надо
OrdinalIgroreCasealexander_kuznetsov Автор
Да, но лучше, думаю, просто Ordinal в этом случае использовать, так как смысла нет регистро-независимое сравнение делать. Тут жёстко прописанная константа, которую мы вряд ли просто так менять будем, а на проверку вариантов написания букв в разных регистрах тоже время тратится.