Можно ли объединить предложения SELECT и WHERE LINQ?

#c# #linq

#c# #linq

Вопрос:

Вот что я сделал для Select пользователей в своей модели, а затем удалил все null записи:

         model.Users = users
            .Select(u =>
        {
            var membershipUser = Membership.GetUser(u.UserName);
            return membershipUser != null
                ? new UserBriefModel
                {
                    Username = u.UserName,
                    Fullname = u.FullName,
                    Email = membershipUser.Email,
                    Roles = u.UserName.GetRoles()
                }
                : null;
        })
            .Where(u => u != null)
            .ToList();
  

Интересно, есть ли способ объединить SELECT WHERE предложение and .

Я пытался:

         model.Users = users
            .Select(u =>
            {
                var membershipUser = Membership.GetUser(u.UserName);
                if (membershipUser != null)
                    return new UserBriefModel
                    {
                        Username = u.UserName,
                        Fullname = u.FullName,
                        Email = membershipUser.Email,
                        Roles = u.UserName.GetRoles()
                    };
            })
            .ToList();
  

Но intellisense предлагает синтаксическую ошибку. Что заставляет меня добавить return null оператор:

         model.Users = users
            .Select(u =>
            {
                var membershipUser = Membership.GetUser(u.UserName);
                if (membershipUser != null)
                    return new UserBriefModel
                    {
                        Username = u.UserName,
                        Fullname = u.FullName,
                        Email = membershipUser.Email,
                        Roles = u.UserName.GetRoles()
                    };
                return null;
            })
            .ToList();
  

Итак, как правильно написать это SELECT утверждение, чтобы в моей модели были выбраны только допустимые записи?

Комментарии:

1. Какую ошибку сборки вы получаете? Также я бы посоветовал выполнить проверку null, прежде чем пытаться использовать объект, иначе вы получите нулевые ссылки внутри вашего linq, что затрудняет диагностику

2. Почему? В чем проблема с отделением where от select ?

3. Просто хочу знать, возможно ли объединить их. Сначала заполнять модель некоторыми бесполезными записями, а затем отфильтровывать их, кажется немного некрасивым. Не лучше ли было бы отказаться null от s в SELECT предложении?

4. Во втором примере кода ваша лямбда неверна — что она возвращает, когда членство равно нулю? Лямбда, анонимные методы — это те же методы — они должны быть правильными методами для использования.

5. Иногда оператор foreach более чистый, чем LINQ. Я думаю, что это может быть одним из таких случаев.

Ответ №1:

Концептуально здесь фактически есть три операции:

  1. спроецируйте имя пользователя на пользователя-участника
  2. отфильтровывать пользователей с нулевым членством
  3. спроецируйте пользователей-членов на модель

Именно так должен выглядеть ваш запрос. Ваш самый первый запрос уже пытался объединить шаги 1 и 3 вместе, но вы испытываете трудности, потому что второй шаг действительно должен быть посередине двух, и препятствия, которые вам нужно преодолеть, чтобы обойти, не очень хороши.

Запрос на самом деле становится более простым и читаемым (и становится идиоматическим кодом LINQ), когда вы представляете все три операции по отдельности.

 model.Users = users
    .Select(user => new
    {
        user,
        membershipUser = Membership.GetUser(user.UserName)
    })
    .Where(pair => pair.membershipUser != null)
    .Select(pair => new UserBriefModel
    {
        Username = pair.user.UserName,
        Fullname = pair.user.FullName,
        Email = pair.membershipUser.Email,
        Roles = pair.user.UserName.GetRoles()
    })
    .ToList();
  

Это запрос, который также может быть более эффективно написан в синтаксисе запроса:

 model.Users = from user in users
                let membershipUser = Membership.GetUser(user.UserName)
                where membershipUser != null
                select new UserBriefModel
                {
                    Username = user.UserName,
                    Fullname = user.FullName,
                    Email = membershipUser.Email,
                    Roles = user.UserName.GetRoles()
                };
  

Что касается буквального вопроса о том, можете ли вы объединить проецирование и фильтрацию в одну операцию LINQ, это, безусловно, возможно. Это было бы неподходящим решением проблемы, но использование SelectMany может позволить вам фильтровать и проецировать одновременно. Это можно сделать, проецируя элемент либо на последовательность из одного элемента, содержащую значение, на которое вы хотите его спроецировать, либо на пустую последовательность, основанную на предикате.

 model.Users = users
    .SelectMany(u =>
    {
        var membershipUser = Membership.GetUser(u.UserName);
        return membershipUser != null
            ? new[]{ new UserBriefModel
            {
                Username = u.UserName,
                Fullname = u.FullName,
                Email = membershipUser.Email,
                Roles = u.UserName.GetRoles()
            }}
            : Enumerable.Empty<UserBriefModel>();
    }).ToList();
  

Конечно, каждый раз, когда вы используете этот код, котенок погибает. Не убивайте котят; вместо этого используйте предыдущий запрос.

Комментарии:

1. Почему я получил -2, а парень, который скопировал мой пост, получил 3.

2. @LIUFA Ну, я даже не видел ваш пост, пока вы не упомянули об этом, и не голосовал за него, так что я могу только догадываться, но несколько вещей выделяются. Во-первых, у вас был довольно заметно отличающийся ответ, когда вы впервые опубликовали и отредактировали его позже. Я не уверен, когда поступили голоса, но они, возможно, были основаны на вашей первой редакции. Далее, и что более важно, ваш ответ ничего не объясняет . Просто публикация некоторого кода без объяснения того, что вы изменили и почему вы его изменили, радикально влияет на ценность публикации. Наконец, код в моем ответе не совпадает с вашим, хотя и похож.

3. Я только что потратил около 20 минут, выясняя, как это сделать, только SelectMany для того, чтобы исправить мой ответ, и вы все равно победили меня… Черт бы тебя побрал, Серви

4. @KyleGobel Теперь вы можете попробовать вызов Эрика и реализовать (неэффективный) Join с точки зрения SelectMany .

Ответ №2:

Я не думаю, что это возможно, Select сопоставлю все 1-1, насколько я знаю … если вы пытаетесь отфильтровать, вам понадобится Where .

редактировать редактировать: я больше не верю SelectMany , что могу это сделать (как показал Серви).

Комментарии:

1. Тогда вы совершенно уверены, что считаете неправильно. SelectMany обычно позволяет сглаживать списки списков и приводит к большему количеству элементов, а не к меньшему. Это select, но никоим образом не фильтр.

2. @Blaise ничего не может сказать SelectMany , но я почти уверен, что первая часть ответа верна. К сожалению, я не думаю, что вы сможете втиснуть их в 1 метод.

3. @TzahMama: SelectMany это невероятно полезный метод, который позволяет превратить древовидную структуру в список для работы: вы могли бы сделать что-то вроде users.SelectMany(user => user.UserName.GetRoles()) , и результатом будет список всех ролей, которые есть у любого пользователя, вероятно, с дубликатами. Это ценно, просто здесь неуместно.

4. Мой плохой, я был более или менее под впечатлением, что SelectMany мог бы сделать что угодно с помощью небольшого волшебства и хитрости

5. @KyleGobel: Но теперь, в результате, вы сможете использовать его правильно! Этот ответ привел к чистому получению знаний.

Ответ №3:

Я не знаю ни о каком методе Linq, который позволит вам произвольно добавлять или не добавлять значение в результирующий IEnumerable.

Для этого лямбда (селектор, предикат, фильтр …) должен иметь возможность управлять этим добавлением. И только предикаты (Where ) могут это сделать. В вашем случае вам нужно будет выполнить предикат (Where) и выбрать. Не существует комбинационного метода, который будет выполнять оба для вас одновременно, за исключением одного непрямого метода, описанного в конце ответа.

 model.Users = users
   .Where(u => Membership.GetUser(u.UserName) != null)
   .Select(u =>
    {
       return new UserBriefModel
         {
             Username = u.UserName,
             Fullname = u.FullName,
             Email = Membership.GetUser(u.UserName).Email,
             Roles = u.UserName.GetRoles()
          };
    })
    .ToList();
  

Мы либо получаем два Membership.getUser(u.UserName) с такой предварительной фильтрацией, либо заканчиваем вашей первоначальной постфильтрацией.

Это просто меняет сложность. И трудно сказать, где производительность будет лучше. Это зависит от того, является ли Membership.GetUser это быстрым, и есть много пользователей, не являющихся членами — для моего примера. Или, если Membership.GetUser это требует много ресурсов и мало пользователей, не являющихся членами, ваш пример с postfilter лучше.

Как и любое решение, основанное на производительности, оно должно быть тщательно рассмотрено и проверено. В большинстве случаев разница минимальна.

Как уже было показано в другом сообщении и указано г-ном ‘Servy’, это можно сделать, используя один вызов SelectMany SelectMany, выбирая либо пустой IEnumerable, либо 1-элементный массив. Но я по-прежнему считаю первое утверждение технически правильным, потому что SelectMany возвращает коллекцию элементов (он точно не добавляет или не добавляет один элемент напрямую):

  model.Users = users
       .SelectMany(u =>
       {
           var membership = Membership.GetUser(u.UserName);

           if (membership == null)
               return Enumerable.Empty<UserBriefModel>();

           return new UserBriefModel[]
           {
               new UserBriefModel()
               {
                   Username = u.UserName,
                   Fullname = u.FullName,
                   Email = membership.Email,
                   Roles = u.UserName.GetRoles()
               }
           };
       })
        .ToList();
  

Комментарии:

1. MembershipUser = Membership.getUser(u.UserName); если (MembershipUser != null) … превращается в . Где(u => Membership.getUser(u.UserName) != null). Я не вижу никакой ошибки. Извините, я ошибаюсь

2. Немного дублирования, но я не думаю, что там можно многое сделать. Мне все еще нравится, как это выглядит лучше, чем OP.

3. Да, LINQ выглядит лучше без условных выражений. Но дело не в этом.

4. Вопрос в том, можно ли объединить select и where, что рассматривается здесь. Этот ответ является правильным ответом.

5. @Magus Но их можно объединить. Неверный ответ указывает, что они не могут быть. Теперь их не следует объединять. Это было бы очень плохой практикой, но это абсолютно возможно.

Ответ №4:

Для этого можно использовать один метод:

 private IEnumerable<UserBriefModel> SelectUserBriefModels(IEnumerable<User> users)
{
    foreach (var user in users)
    {
        var membershipUser = Membership.GetUser(user.UserName);
        if (membershipUser != null)
        {
            yield return new UserBriefModel
            {
                Username = user.UserName,
                Fullname = user.FullName,
                Email = membershipUser.Email,
                Roles = user.UserName.GetRoles()
            };
        }
    }
}
  

Вы бы использовали его так:

 model.Users = SelectUserBriefModels(users);
  

Ответ №5:

 model.Users = users
    .Where(u => u.Membership != null)
    .Select(u => new UserBriefModel
            {
                Username = u.UserName,
                Fullname = u.FullName,
                Email = u.Membership.Email,
                Roles = u.UserName.GetRoles()
            })
    .ToList();
  

Сначала фильтруйте, затем выберите. Для этого решения вам необходимо иметь свойство навигации, чтобы вы могли делать u.Membership.Email вместо membershipUser.Email .

Мои пользователи выглядят примерно так:

 public class UserProfile
{
    // other properties

    public virtual Membership Membership { get; set; }
}
  

где Membership находится объект, представляющий таблицу членства и отображаемый через:

 modelBuilder.Entity<Membership>()
   .HasRequired<UserProfile>(m => m.User)
   .WithOptional(u => u.Membership);
  

Затем вы можете выбрать все одним запросом. Некоторые другие решения здесь также работают нормально, но каждый вызов Membership.GetUser(u.UserName) приводит к одному дополнительному вызову DB.

Комментарии:

1. Ваш код выбирает что-то совершенно другое. Куда это делается Membership.GetUser ?

2. @KlausByskovPedersen прав. Я проверяю Membership.GetUser(u.UserName) .

3. @KlausByskovPedersen: Теперь я изменил его — таким образом, он работает / работал в моем коде.

4. @chrfin Это не проблема OP

5. @TzahMama: это рабочее решение проблемы OP (выберите a UserBriefModel для каждого пользователя, у которого есть учетная запись участника) с дополнительным преимуществом гораздо меньшего количества запросов к БД — я использую это в своем приложении таким образом…