Blaise
Blaise

Reputation: 22222

Can SELECT and WHERE LINQ clause be combined?

Here is what I have done to Select users into my model and then remove all the null records:

        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();

Wondering if there is a way to combine the SELECT and WHERE clause.

I tried:

        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();

But the intellisense suggest a syntax error. Which forces me to add a return null statement:

        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();

So what is the correct way to write this SELECT statement so only valid records are selected into my model?

Upvotes: 4

Views: 5821

Answers (5)

Thomas Johnson
Thomas Johnson

Reputation: 420

You can use a single method to accomplish this:

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()
            };
        }
    }
}

You would use it like this:

model.Users = SelectUserBriefModels(users);

Upvotes: 0

Eugene Podskal
Eugene Podskal

Reputation: 10401

I don't know about any Linq method which will allow you to arbitrary add or to not add the value into the resulting IEnumerable.

To do it the lambda(selector, predicate, filter...) should be able to control this addition. And only predicates(Where) are able to do it. In your case you will have to execute predicate(Where) and Select. There is no combinational method which will do both for you at the same time, except one non-direct method described in the end of the answer.

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();

We either get two Membership.GetUser(u.UserName) with such prefiltering or we will end with your original postfiltering.

That's just shifting the complexity. And it is difficult to say where the performance will be better. It depends on whether the Membership.GetUser is fast and there are a lot of non-membership users - for my example. Or if Membership.GetUser is resource-consuming and there are few non-membership users your example with postfilter is better.

As any performance based decision it should be thoroughly considered and checked. In most cases the difference is minimal.

As it was already shown in the another post and pointed by Mr. 'Servy' it is possible to do so using one call of SelectMany SelectMany selecting either empty IEnumerable or 1-element array. But I still consider the first statement to be technically correct, because SelectMany returns collection of elements (it does not exactly add or doesn't add single element directly):

 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();

Upvotes: 0

Kyle Gobel
Kyle Gobel

Reputation: 5750

I don't think this is possible, Select will map everything 1-1 as far as I know...if you're trying to filter you will need a Where.

edit edit: I no longer believe SelectMany can do it (As Servy has shown).

Upvotes: 1

Servy
Servy

Reputation: 203845

Conceptually you actually have three operations here:

  1. project the user name to a membership user
  2. filter out null membership users
  3. project the membership users to a model

That is how your query should be looking. Your very first query has already tried to combine steps 1 and 3 together, but you're struggling because step two really should be in the middle of the two, and the hoops that you need to jump through to get around that aren't pretty.

The query actually becomes simpler and readable (and becomes idiomatic LINQ code) when you represent all three operations individually.

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();

This is a query that can also be written more effectively in query syntax:

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()
                };

As for the literal question of whether or not you can combine the projecting an filtering into a single LINQ operation, it is certainly possible. It would be an inappropriate solution to the problem, but the use of SelectMany can allow you to filter and project at the same time. This can be done by projecting the item to either a one item sequence containing the value that you want to project it to or an empty sequence based on the predicate.

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();

Of course, every time you use this code, a kitten is killed. Don't kill kittens; use the earlier query instead.

Upvotes: 12

Christoph Fink
Christoph Fink

Reputation: 23113

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();

First filter, then select. For this solution you need to have a navigation property so you can do u.Membership.Email instead of the membershipUser.Email.

My users look something like:

public class UserProfile
{
    // other properties

    public virtual Membership Membership { get; set; }
}

where Membership is the entity representing the membership table and is mapped via:

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

Then you can select everything with one query. Some other solutions here also work fine, but every call to Membership.GetUser(u.UserName) results in one additional DB call.

Upvotes: -1

Related Questions