Свободный синтаксис для метода расширения IQueryable?

Я позволю коду говорить самому за себя:

public interface ISoftDeletable {
  bool IsDeleted {get; set;}
}

public static class Extensions {
  public IQueryable<T> Active<T>(this IQueryable<T> q) where T : ISoftDeletable {
    return q.Where(t => !t.IsDeleted);
  }
}

public partial class Thing : ISoftDeletable {
  ...
}

...
var query = from tc in db.ThingContainers
            where tc.Things.Active().Any(t => t.SatisfiesOtherCondition)
            select new { ... }; // throws System.NotSupportedException

Ошибка:

LINQ to Entities не распознает метод System.Collections.Generic.IQueryable`1[Thing] ActiveThing, и этот метод нельзя преобразовать в выражение хранилища.

Вы уловили идею: мне нужен свободный способ выражения этого, чтобы для любого ISoftDeletable я мог добавить предложение «где» с помощью простого, многократно используемого фрагмента кода. Пример здесь не работает, потому что Linq2Entities не знает, что делать с моим методом Active().

Пример, который я привел здесь, прост, но в моем реальном коде расширение Active() содержит гораздо более сложный набор условий, и я не хочу копировать и вставлять его по всему коду.

Какие-либо предложения?


person Shaul Behr    schedule 29.08.2013    source источник
comment
Вы не сказали какое исключение, которое вы получаете, что затрудняет вам помощь. Я ожидаю, что то, что вы сделали до сих пор, будет в порядке, но более сложные примеры не будут, например. используя Active в соединении.   -  person Jon Skeet    schedule 29.08.2013
comment
@JonSkeet — добавлено сообщение об ошибке. Но да, вы правы, мое фактическое использование включает соединение.   -  person Shaul Behr    schedule 29.08.2013
comment
И воспроизвели ли вы эту ошибку с помощью вашего простого кода? Я подозреваю, что нет - было бы полезно иметь пример кода, который действительно воспроизводит ошибку.   -  person Jon Skeet    schedule 29.08.2013
comment
@JonSkeet - отредактировано, чтобы показать что-то более близкое к моему реальному коду. Очевидно, я не могу скомпилировать, чтобы увидеть, действительно ли это не работает, но это лучше, чем навязывать вам мой реальный код!   -  person Shaul Behr    schedule 29.08.2013
comment
@JonSkeet, кстати, вы заметили, что я использую то же соглашение для фрагментов кода, что и вы в C# in Depth? :-) Я люблю книгу - спасибо!   -  person Shaul Behr    schedule 29.08.2013
comment
У меня не было, но я рад, что это завоевывает популярность. Теперь, когда вы показали код, он имеет гораздо больше смысла, потому что теперь у вас фактически есть дерево выражений, содержащее вызов Active, что не то же самое, что вызывать Active самостоятельно (как раньше).   -  person Jon Skeet    schedule 29.08.2013
comment
@JonSkeet, конечно, теперь это имеет больше смысла, но у вас есть решение? :-)   -  person Shaul Behr    schedule 29.08.2013
comment
К сожалению, не без переписывания деревьев выражений :(   -  person Jon Skeet    schedule 29.08.2013


Ответы (2)


У вас есть две несвязанные проблемы в коде. Во-первых, Entity Framework не может обрабатывать приведения в выражениях, что делает ваш метод расширения.

Решение этой проблемы состоит в том, чтобы добавить ограничение class к вашему методу расширения. Если вы не добавите это ограничение, выражение будет скомпилировано для включения приведения:

.Where (t => !((ISoftDeletable)t.IsDeleted))

Приведение выше сбивает с толку Entity Framework, поэтому вы получаете ошибку времени выполнения.

Когда ограничение добавлено, выражение становится простым доступом к свойству:

 .Where (t => !(t.IsDeleted))

Это выражение можно легко проанализировать с помощью Entity Framework.

Вторая проблема заключается в том, что вы не можете применять пользовательские методы расширения в синтаксисе запроса, но вы можете использовать их в синтаксисе Fluent:

db.ThingContainers.SelectMany(tc => tc.Things).Active()
    .Any(t => t.SatisfiesOtherCondition); // this works

Чтобы увидеть проблему, мы должны посмотреть, каким будет фактически сгенерированный запрос:

 db.ThingContainers
       .Where(tc => tc.Things.Active().Any(t => t.StatisfiesOtherCondition))
       .Select(tc => new { ... });

Вызов Active() никогда не выполняется, а генерируется как выражение для анализа EF. Разумеется, EF не знает, что делать с такой функцией, поэтому выручает.

Очевидным обходным решением (хотя и не всегда возможным) является запуск запроса с Things вместо ThingContainers:

db.Things.Active().SelectMany(t => t.Container);

Другой возможный обходной путь — использовать функции, определяемые моделью, но это более сложный процесс. См. это, это и эти статьи MSDN для получения дополнительной информации.

person felipe    schedule 29.08.2013
comment
Тем не менее, это помогло мне в связанном с этим вопросе, так что +1 за это, спасибо! - person Shaul Behr; 29.08.2013
comment
@ Шауль, я расширил ответ. По сути, смешивание синтаксиса запроса с методами расширения — это боль. - person felipe; 30.08.2013
comment
Очень хорошо, но что бы вы сделали, если бы ThingContainer также было ISoftDeletable, и я хочу применить к этому тоже .Active()? :-) - person Shaul Behr; 30.08.2013
comment
db.Things.Active().SelectMany(t => t.Container).Active() ;). Это, безусловно, может привести к тому, что вам придется повторять выражение, если запрос достаточно сложный. - person felipe; 30.08.2013
comment
Взгляните на мой ответ, что я сделал в конце. Спасибо за вашу помощь! - person Shaul Behr; 30.08.2013

Хотя @felipe получил признание за ответ, я подумал, что также опубликую свой собственный ответ в качестве альтернативы, хотя он и похож:

var query = from tc in db.ThingContainers.Active() // ThingContainer is also ISoftDeletable!
            join t in db.Things.Active() on tc.ID equals t.ThingContainerID into things
            where things.Any(t => t.SatisfiesOtherCondition)
            select new { ... };

Преимущество этого заключается в том, что структура запроса остается более или менее неизменной, хотя вы теряете беглость неявной связи между ThingContainer и Thing. В моем случае сделка заключается в том, что лучше явно указать отношения, чем явно указывать Active() критерии.

person Shaul Behr    schedule 29.08.2013