Обхідне рішення "містить ()" за допомогою Linq для сутностей?


86

Я намагаюся створити запит, який використовує список ідентифікаторів у реченні where, використовуючи API клієнта Silverlight ADO.Net Data Services (і, отже, Linq To Entities). Хто-небудь знає, як вирішити проблему, пов’язану з вмістом, що не підтримується?

Я хочу зробити щось подібне:

List<long?> txnIds = new List<long?>();
// Fill list 

var q = from t in svc.OpenTransaction
        where txnIds.Contains(t.OpenTransactionId)
        select t;

Спробував це:

var q = from t in svc.OpenTransaction
where txnIds.Any<long>(tt => tt == t.OpenTransactionId)
select t;

Але отримав "Метод" Будь-який "не підтримується".


35
Примітка: Entity Framework 4 (у .NET 4) має метод "Містить", на випадок, якщо хтось читає це, що не знає про нього. Я знаю, що OP використовував EF1 (.NET 3.5).
Даррелл Нортон

7
@Darrell Я просто витратив півгодини, бо пропустив ваш коментар. Мені б хотілося, щоб ваш коментар блимав і вимальовував по всьому екрану.
Chris Dwyer

Відповіді:


97

Оновлення: EF ≥ 4 підтримує Containsбезпосередньо (Оформлення замовлення Any), тому вам не потрібні обхідні шляхи.

public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    IEnumerable<TValue> collection
  )
{
  if (selector == null) throw new ArgumentNullException("selector");
  if (collection == null) throw new ArgumentNullException("collection");
  if (!collection.Any()) 
    return query.Where(t => false);

  ParameterExpression p = selector.Parameters.Single();

  IEnumerable<Expression> equals = collection.Select(value =>
     (Expression)Expression.Equal(selector.Body,
          Expression.Constant(value, typeof(TValue))));

  Expression body = equals.Aggregate((accumulate, equal) =>
      Expression.Or(accumulate, equal));

  return query.Where(Expression.Lambda<Func<TEntity, bool>>(body, p));
}

//Optional - to allow static collection:
public static IQueryable<TEntity> WhereIn<TEntity, TValue>
  (
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, TValue>> selector,
    params TValue[] collection
  )
{
  return WhereIn(query, selector, (IEnumerable<TValue>)collection);
}

ВИКОРИСТАННЯ:

public static void Main()
{
  using (MyObjectContext context = new MyObjectContext())
  {
    //Using method 1 - collection provided as collection
    var contacts1 =
      context.Contacts.WhereIn(c => c.Name, GetContactNames());

    //Using method 2 - collection provided statically
    var contacts2 = context.Contacts.WhereIn(c => c.Name,
      "Contact1",
      "Contact2",
      "Contact3",
      "Contact4"
      );
  }
}

6
Увага; коли arg - це велика колекція (у мене було 8500 елементів у списку), стек переповнюється. Можливо, вам здається божевільним передавати такий список, але, я думаю, це виявляє недолік у цьому підході, тим не менше.
dudeNumber4

2
Виправте мене, якщо я помиляюся. але це означає, що коли передана колекція (фільтр) є порожнім набором, це в основному призведе до всіх даних, оскільки вона щойно повернула параметр запиту. Я очікував, що він відфільтрує все значення, чи є спосіб зробити це?
Дрімота

1
Якщо ви маєте на увазі, що коли порожня колекція порожня, вона не повинна повертати результатів, у наведеному вище фрагменті замініть дію if (!collection.Any()) //action;- replace на просто повернення порожнього запиту запитуваного типу для кращої продуктивності - або просто видаліть цей рядок.
Shimmy Weitzhandler

1
повернути WhereIn (запит, селектор, колекція); слід замінити return WhereIn (запит, селектор, (IEnumerable <TValue>) колекція); щоб уникнути небажаної рекурсії.
Антуан Обрі

1
Я вважаю, що в коді є помилка. Якщо наданий список значень порожній, правильною поведінкою повинно бути повернення результатів - тобто / у колекції не існує об’єктів у запиті. Однак код робить прямо протилежне - повертаються всі значення, а не жодне з них. Я вважаю, що ви хочете "if (! Collection.Any ()) return query.Where (e => false)"
ShadowChaser

18

Ви можете повернутися до кодування деяких e-sql (зверніть увагу на ключове слово "it"):

return CurrentDataSource.Product.Where("it.ID IN {4,5,6}"); 

Ось код, який я використовував для створення деякого e-sql із колекції, YMMV:

string[] ids = orders.Select(x=>x.ProductID.ToString()).ToArray();
return CurrentDataSource.Products.Where("it.ID IN {" + string.Join(",", ids) + "}");

1
Чи є у вас більше інформації про "це"? Префікс "it" відображається у зразках MSDN, але ніде я не можу знайти пояснення про те, коли / чому "це" потрібно.
Роберт Клейпул,

1
Використовуваний в динамічному запиті Entity Framework, погляньте на geekswithblogs.net/thanigai/archive/2009/04/29/… , там пояснює Танігагатан Сіранджеві.
Shimmy Weitzhandler

13

З MSDN :

static Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
    Expression<Func<TElement, TValue>> valueSelector, IEnumerable<TValue> values)
{
    if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
    if (null == values) { throw new ArgumentNullException("values"); }
    ParameterExpression p = valueSelector.Parameters.Single();

    // p => valueSelector(p) == values[0] || valueSelector(p) == ...
    if (!values.Any())
    {
        return e => false;
    }

    var equals = values.Select(
             value => (Expression)Expression.Equal(valueSelector.Body, Expression.Constant(value, typeof(TValue))));

    var body = equals.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

    return Expression.Lambda<Func<TElement, bool>>(body, p);
} 

і запит стає:

var query2 = context.Entities.Where(BuildContainsExpression<Entity, int>(e => e.ID, ids));

3
Якщо ви хочете зробити "Не містить", просто внесіть наступні зміни в метод BuildContainsExpression: - Expression.Equal стає Expression.NotEqual - Expression.Or стає Expression.And
Мерріт

2

Я не впевнений у Silverligth, але у linq до об’єктів я завжди використовую any () для цих запитів.

var q = from t in svc.OpenTranaction
        where txnIds.Any(t.OpenTransactionId)
        select t;

5
Будь-який не бере об'єкт типу послідовності - він або не має параметрів (у цьому випадку це просто "це пусто чи ні"), або він бере предикат.
Джон Скіт,

Я надзвичайно рада, що знайшла цю відповідь:) +1 Дякую AndreasN
SDReyes

1

Щоб завершити запис, ось код, який я нарешті використав (перевірка помилок опущена для ясності) ...

// How the function is called
var q = (from t in svc.OpenTransaction.Expand("Currency,LineItem")
         select t)
         .Where(BuildContainsExpression<OpenTransaction, long>(tt => tt.OpenTransactionId, txnIds));



 // The function to build the contains expression
   static System.Linq.Expressions.Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
                System.Linq.Expressions.Expression<Func<TElement, TValue>> valueSelector, 
                IEnumerable<TValue> values)
        {
            if (null == valueSelector) { throw new ArgumentNullException("valueSelector"); }
            if (null == values) { throw new ArgumentNullException("values"); }
            System.Linq.Expressions.ParameterExpression p = valueSelector.Parameters.Single();

            // p => valueSelector(p) == values[0] || valueSelector(p) == ...
            if (!values.Any())
            {
                return e => false;
            }

            var equals = values.Select(value => (System.Linq.Expressions.Expression)System.Linq.Expressions.Expression.Equal(valueSelector.Body, System.Linq.Expressions.Expression.Constant(value, typeof(TValue))));
            var body = equals.Aggregate<System.Linq.Expressions.Expression>((accumulate, equal) => System.Linq.Expressions.Expression.Or(accumulate, equal));
            return System.Linq.Expressions.Expression.Lambda<Func<TElement, bool>>(body, p);
        }


0

Дуже дякую. WhereIn методу розширення мені було достатньо. Я профілював його і створив ту саму команду SQL для бази даних, що і e-sql.

public Estado[] GetSomeOtherMore(int[] values)
{
    var result = _context.Estados.WhereIn(args => args.Id, values) ;
    return result.ToArray();
}

Створено це:

SELECT 
[Extent1].[intIdFRLEstado] AS [intIdFRLEstado], 
[Extent1].[varDescripcion] AS [varDescripcion]
FROM [dbo].[PVN_FRLEstados] AS [Extent1]
WHERE (2 = [Extent1].[intIdFRLEstado]) OR (4 = [Extent1].[intIdFRLEstado]) OR (8 = [Extent1].[intIdFRLEstado])

0

Я думаю, що приєднання до LINQ може бути обхідним шляхом.

Однак я не тестував код. Сподіваюся, це допоможе. На ура :-)

List<long?> txnIds = new List<long?>();
// Fill list 

var q = from t in svc.OpenTransaction
        join tID in txtIds on t equals tID
        select t;

Приєднуйтесь до LINQ:

http://weblogs.asp.net/salimfayad/archive/2008/07/09/linq-to-entities-join-queries.aspx


0

Вибачте, новий користувач, я б прокоментував фактичну відповідь, але, схоже, я ще не можу цього зробити?

У будь-якому випадку, що стосується відповіді із зразком коду для BuildContainsExpression (), майте на увазі, що якщо ви використовуєте цей метод у Суб’єктах бази даних (тобто не в об’єктах пам’яті) і використовуєте IQueryable, він насправді повинен перейти до бази даних оскільки він в основному робить багато умов SQL "або" для перевірки речення "де в" (запустіть його за допомогою SQL Profiler, щоб побачити).

Це може означати, що якщо ви вдосконалюєте IQueryable за допомогою декількох BuildContainsExpression (), це не перетворить його на один оператор SQL, який запускається в кінці, як ви очікуєте.

Обхідним шляхом для нас було використання декількох об’єднань LINQ, щоб зберегти його в одному виклику SQL.


0

На додаток до обраної відповіді.

Замінити Expression.Orз Expression.OrElseвикористовувати з NHibernate і виправити Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseOr' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'виняток.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.