9

Lets say I have a collection of Messages which has the properties "UserID" (int) and "Unread" (bool).

How can I use LINQ extension methods to set Unread = false, for any Message in the collection in whose UserID = 5?

So, I know I can do something like:

messages.Any(m => m.UserID == 5);

But, how do I set the Unread property of each of those with an extension method as well?

Note: I know I should not do this in production code. I'm simply trying to learn some more LINQ-fu.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
KingNestor
  • 65,976
  • 51
  • 121
  • 152
  • 1
    Is there any reason why you can't use a regular for-each iterating over the filtered collection? – helios Dec 30 '09 at 16:41
  • @helios, no. This isn't production code. I'm just having fun and I was curious if I could iterate over the items using LINQ – KingNestor Dec 30 '09 at 16:42
  • 2
    Just for convention and general sanity sake, call it read, not Unread. if(m.Read) is so much easier to understand than if (!m.Unread). – John Kraft Dec 30 '09 at 16:50

7 Answers7

6

Actually, this is possible using only the built-in LINQ extension methods without ToList.
I believe that this will perform very similarly to a regular for loop. (I haven't checked)

Don't you dare do this in real code.

messages.Where(m => m.UserID == 5)
        .Aggregate(0, (m, r) => { m.Unread = false; return r + 1; });

As an added bonus, this will return the number of users that it modified.

SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
5

messages.Where(m => m.UserID == 5).ToList().ForEach(m => m.Unread = false);

Then submit the changes.

David
  • 12,451
  • 1
  • 22
  • 17
  • `Any` returns a bool, so that's not correct- you'd probably want `Where` – Factor Mystic Dec 30 '09 at 16:39
  • Any is not correct because it checks if there is at least one message with UserID 5 in the collection and returns true or false. You need to use Where instead of any. Also be aware of the fact that creating a list can be expensive when the collection is really big. See also my answer which suggests MoreLinq's extension method ForEach. – Oliver Hanappi Dec 30 '09 at 16:40
  • 3
    Select isn't right - that's going to return an `IEnumerable`. You mean `Where`. – Jon Skeet Dec 30 '09 at 16:41
  • Yeah, sorry about that, just not awake yet. Fixed it now. – David Dec 30 '09 at 16:42
4

Standard LINQ extension methods doesn't include side effects aimed methods. However you can either implement it yourself or use from Reactive Extensions for .NET (Rx) like this:

messages.Where(m => m.UserID == 5).Run(m => m.Unread = false);
Dzmitry Huba
  • 4,493
  • 20
  • 19
4

As there is no explicit extension method that does a ForEach, you are stuck with either using a secondary library, or writing the foreach statement on your own.

foreach (Message msg in messages.Where(m => m.UserID == 5))
{
    msg.Unread = false;
}

If you really want to use a Linq statement to accomplish this, create a copy the collection using the ToList() method, accessing the ForEach() method of the List type:

messages.Where(m => m.UserID == 5).ToList().ForEach(m => m.Unread = false);

or place the side-effect in a Where() statement:

messages.Where(m =>
{
    if (m.UserID == 5) { m.Unread = false; return true; }
    return false;
});

In either case, I prefer to use the explicit foreach loop as it doesn't make unnecessary copies and is clearer than the Where hack.

Steve Guidi
  • 19,700
  • 9
  • 74
  • 90
  • Calling `Select` won't actually do anything until the result is enumerated. You should call `.Count()` on `Select`'s return value to force the entire collection to be enumerated. Also, your first `Select` should actually be `Where`. – SLaks Dec 30 '09 at 16:54
  • Caught that immediately after posting. – Steve Guidi Dec 30 '09 at 16:56
  • The last method still won't work. Calling `Where` won't actually do anything until its return value is enumerated. You need to call `.Count()`. Also, you missed a closing parenthesis. – SLaks Dec 30 '09 at 16:56
2

With LINQ you can't because LINQ is a query language/extension. There is however a project called MoreLinq, which defines an extension method called ForEach which allows you to pass an action which will be performed on every element.

So, you could do with MoreLinq:

messages.Where(m => m.UserID == 5).ForEach(m => m.Unread = false);

Best Regards,
Oliver Hanappi

Oliver Hanappi
  • 12,046
  • 7
  • 51
  • 68
  • 1
    This is not strictly true. The reason that LINQ can't do this has nothing to do with the fact that it's a query language/extension. (Besides the fact that LINQ can do this) – SLaks Dec 30 '09 at 16:48
0

This answer is in the spirit of providing a solution. On could create an extension which does both the predicate (Where extension) to weed out the items and the action needed upon those items.

Below is an extension named OperateOn which is quite easy to write:

public static void OperateOn<TSource>(this List<TSource> items, 
                                      Func<TSource, bool> predicate, 
                                      Action<TSource> operation)
{
    if ((items != null) && (items.Any()))
    {
        items.All (itm =>
        {
            if (predicate(itm))
                operation(itm);

            return true;
        });

    }
}

Here is it in action:

var myList = new List<Item>
                   { new Item() { UserId = 5, Name = "Alpha" },
                     new Item() { UserId = 5, Name = "Beta", UnRead = true },
                     new Item() { UserId = 6, Name = "Gamma", UnRead = false }
                   };


myList.OperateOn(itm => itm.UserId == 5, itm => itm.UnRead = true);

Console.WriteLine (string.Join(" ",
                               myList.Select (itm => string.Format("({0} : {1})",
                                                                   itm.Name,
                                                                   itm.UnRead ))));

/* Outputs this to the screen

(Alpha : True) (Beta : True) (Gamma : False)

*/

...

public class Item
{
    public bool UnRead { get; set; }
    public int UserId { get; set; }
    public string Name { get; set; }
}
ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
0

You should be able to just do it in a Select(), remember the lambda is a shortcut for a function, so you can put as much logic in there as you want, then return the current item being enumerated. And... why exactly wouldn't you do this in production code?

messages = messages
    .Select(m => 
    {
        if (m.UserId == 5) 
            m.Unread = true;
        return m;
    });
Alex Fairchild
  • 1,025
  • 11
  • 11