TL; DR
Множество авторов предлагают лучше инкапсулировать коллекции навигации с общедоступными IEnumerable
проекциями частных ICollection
проекций, которые являютсяподдерживается в EF Core. Я попытался использовать похожую технику с проекциями только для чтения для обхода отношений многие-ко-многим, но это заставляет Linq возвращаться к оценке на стороне клиента при прохождении (что, кроме того, что не исполняется, запрещено в EF Core 3.0). Если это мой выбранный общедоступный API, есть ли способ сделать эти свойства, доступные только для чтения, такими, чтобы Linq мог превратить их в SQL без изменения API?
Полный вопрос
Я согласенпопытка создать модель данных с помощью EF Core со следующими целями проектирования:
- Как можно большее невежество в постоянстве - по крайней мере с точки зрения общедоступного API
- Ограничить коллекции навигацииот непосредственного изменения через
ICollection
интерфейс - Сохраняйте мой общедоступный интерфейс, чтобы показывать только то, что выглядит как прямые отношения многие ко многим
- Скрыть технические идентификаторы, поскольку они не имеют значения домена (это может быть отброшено)
Сейчас у EF Core есть большое ограничение - отсутствие отношений «многие ко многим» без объекта объединения, но это должно произойти, поэтому я пытаюсьчтобы скрыть объекты объединения, и я следую методикам, изложенным в этом сообщении в блоге ajcvickers (и других).
Так, например, у меня есть эти (очевидно, неполные) сущности 1-го класса:
public class Conversation
{
private IEnumerable<Tag> Tags => _joinToTags.Select(jt => jt.Tag);
public ICollection<ConversationTag> _joinToTags = new HashSet<ConversationTag>();
}
public class Tag
{
public string Label { get; private set; } = null!;
// No `Conversations` navigation property if I can help it
}
(Предположим, что существует код отображения EF и тривиальный ConversationTag
объект объединения, иони правильные. Кажется, они работают правильно, за исключением проблемы, описанной ниже.)
Обратите внимание, что Tag
s может быть связано с 5+ другими объектами ... поэтому, пока я может у меня есть свойства навигации обратно к этим сущностям из Tag
, я пытаюсь избежать загромождения API. Но я должен иметь возможность запросить это направление - например, найти, что Conversations
имеет данное Tag
.
Моя проблемаэто: этот дизайн не позволяет мне эффективно выполнять запросы и пытается вернуться к выполнению на стороне клиента всякий раз, когда я пересекаю свойство Tags
.
По большей части это работало хорошо. Тем не менее, я только что перешел на EF Core 3.0, который услужливо показал некоторые места, где мои запросы к этим свойствам навигации приводят к оценке на стороне клиента (что может привести к очень низкой производительности).
Например, в EF Core 3.0 этокод интеграционного теста (с использованием потрясающей библиотеки Shouldly):
context.Conversations.Count(c => c.Tags.Any(t => t == context.Find<Tag>("In-Person"))).ShouldBe(2);
, который работал под EF Core 2.1, теперь взрывается с:
System.InvalidOperationException: 'Processing of the LINQ expression 'Any<Tag>(
source: NavigationTreeExpression
Value: EntityReferenceConversation
Expression: (Unhandled parameter: c).Tags,
predicate: (t) => t == (Unhandled parameter: __Find_0))' by
'NavigationExpandingExpressionVisitor' failed. This may indicate either
a bug or a limitation in EF Core. See
https://go.microsoft.com/fwlink/?linkid=2101433
for more detailed information.'
Что, вероятно, хорошо, потому что в противном случае ядумаю, что я буду извлекать все разговоры из БД в рабочем состоянии - причина этого критического изменения в EF Core 3.0.
Но как я могу заставить этот «запрос» работать так, чтобы онэффективно переводит в SQL - не раскрывая объединяемые сущности и вообще не затрагивая вышеуказанные цели проектирования?
Этот кажется таким, каким он должен быть выполнимым. Некоторые вещи, которые, как я думал, могут помочь, не имеют:
// Making it IQueryable instead of IEnumerable...maybe the expression tree will bubble up? Nope...
public IQueryable<Tag> Tags => _joinToTags.AsQueryable().Select(jt => jt.Tag);
// Added this IEnumberable<ConversationTag> extension method:
public static class ConversationTagExtensions
{
public static IEnumerable<Tag> Tags(this IEnumerable<ConversationTag> cts)
{
return cts.Select(ct => ct.Tag);
}
}
// And changed the Tags property to:
public IEnumerable<Tag> Tags => _joinToTags.Tags(); // Nope!
Примечательно, что эти do работают:
// Changing _joinToTags to public:
context.Conversations.Count(c => c._joinToTags.Any(ct => ct.Tag == context.Find<Tag>("In-Person"))).ShouldBe(2);
// Added Conversation property:
// public IEnumerable<ConversationTag> ConversationTags => _joinToTags.AsEnumerable();
context.Conversations.Count(c => c.ConversationTags.Any(ct => ct.Tag == context.Find<Tag>("In-Person"))).ShouldBe(2);
// ^ that surprised me a little bit.
Наконец, этот подход работает работает - большое спасибо этому посту за указание мне в правильном направлении - но требует, чтобы я знал, чтобы перейти к выражению для запроса:
public class Tag
{
public string Label { get; private set; } = null!;
// Added EF mapping for _joinToConversations, but at least it's not part of the public API...
private ICollection<ConversationTag> _joinToConversations = new HashSet<ConversationTag>();
public static Expression<Func<Tag, IEnumerable<Conversation>>> ConversationsExpression
{
get { return t => t._joinToConversations.Select(jt => jt.Conversation); }
}
}
// And now the unit test code is translated to:
context.Tags.Where(t => t.Label == "In-Person").SelectMany(Tag.ConversationsExpression).Count().ShouldBe(2);
Этот последний производит этот хороший, правильный SQL-запрос:
[Parameters=[@__entity_equality_Find_0_Label='In-Person' (Size = 9)], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*)
FROM "Conversations" AS "c"
WHERE EXISTS (
SELECT 1
FROM "ConversationTag" AS "c0"
INNER JOIN "Tags" AS "t" ON "c0"."TagLabel" = "t"."Label"
WHERE (("c"."Id" = "c0"."ConversationId1") AND "c0"."ConversationId1" IS NOT NULL)
AND (("t"."Label" = @__entity_equality_Find_0_Label) AND @__entity_equality_Find_0_Label IS NOT NULL))
Это решение или другое решение, которое включает в себя Expressions или, возможно, предоставление технических идентификаторов, будет приемлемым, но оно не очень "свободно" для разработчиков клиентов, и кажется, что должно существовать лучшее. Кажется, что этот подход с выражением может быть применен в другом направлении и напрямую связан со стандартным свойством, которое возвращает что-то, непосредственно представляющее результат проекции ... но я не могу понятьправильное заклинание. Скорее, каждый раз, когда я использую стандартное свойство Tags
, независимо от того, что оно возвращает, оно возвращается к оценке на стороне клиента.
Есть ли способ создать свойство Tags
, которое болееестественным образом применимый, который возвращает что-то, что Linq может обработать как дерево выражений?
Приложение: здесь есть несколько других целей проектирования, которые не имеют прямого отношения, но могут вызвать вопросы. Например, context
выше - это на самом деле не EF DbContext
, а объект Unit of Work, интерфейс которого обеспечивает IQueryable
s для каждого типа сущности ... тонкая оболочка над EF ... которая может не оказатьсябыть практичным, но до сих пор служил мне достаточно хорошо. Так что, если вам интересно, учитывая мои цели проектирования, почему мой тестовый код использует EF напрямую, на самом деле это не так - хотя он предназначен для использования уровня постоянства, чтобы найти проблемы в точности как тот, о котором я спрашиваю.
Обновление дня 2
Это дико ... но это другой дизайн, который работает или, по крайней мере, успешно создает код SQL:
public class Conversation
{
internal IEnumerable<Tag> Tags => _joinToTags.Select(jt => jt.Tag);
// ^-- NOTE: now internal instead of private... not ideal
public ICollection<ConversationTag> _joinToTags = new HashSet<ConversationTag>();
}
public static class ConversationExtensions
{
public static IQueryable<Conversation> WhereTag(this IQueryable<Conversation> conversations, Expression<Func<Tag, bool>> expression)
{
return conversations.Where(c => c._joinToTags.AsQueryable().Select<ConversationTag, Tag>(ct => ct.Tag).Any<Tag>(expression));
}
}
// With test code:
context.Conversations.WhereTag(t => t == context.Find<Tag>("In-Person") ).Count().ShouldBe(2);
Я получил тамрасширение концепции IQueryable
методов расширения ( найдено много мест , и я не могу вспомнить, где я впервые это увидел) с идеей предоставления фильтравыражение как параметр ... который, к моему большому удивлению, работал довольно легко. Все еще не для моей цели, но может быть предпочтительнее, чем обходной путь, приведенный выше. (Чтобы быть справедливым, после Я понял это, я нашел статью, которая делает что-то подобное .)
Я также сейчас изучаю некоторые дополнительные инструменты, которые янайдено - особенно DelegateDecompiler , который, я согласен, является умопомрачительным ! Похоже, что этот инструмент может быть лучшей надеждой на ответ с использованием Plain Old Properties: Decompile()
получатель моего свойства возвращается в выражение - но я пока не могу заставить его работать с коллекцией ... все еще пытаюсь.
Я также теперь лучше понимаю корень проблемы, как объяснено здесь :
Проблема возникает из-за того, что эти свойства не могут быть переведены и отправлены всервер, так как они были скомпилированы в промежуточный язык (IL), а не в деревья выражений LINQ, которые требуются для перевода реализациями IQueryable. В .NET нет ничего, что позволило бы нам перепроектировать IL обратно в методы и синтаксис, которые позволили бы нам преобразовать предполагаемую операцию в удаленный запрос.
... За исключением того, что это былонаписанный в 2009 году, и теперь существует DelegateDecompiler, который звучит как то, что он делает.
Я также нашел некоторые другие инструменты в том же духе, такие как Linq.Translations , PropertyTranslator (хотя это выглядит скорее всего несуществующим), ExpressMapper (и соответствующее сообщение в блоге ), или, может быть, даже AutoMapper, как я раньше не знал о Запрашиваемые расширения функция. Все еще не нашел волшебные слова все же.