Practical .NET

How to Update Members of a Collection with LINQ

Sometimes it's just cool to take a walk through some technology to find all the different ways you can solve a problem -- sort of "Fun with the .NET Framework." Here's a look at all the ways that Peter could think of to update an object in a collection...some of which may be foolish.

Let's say you'd like to make a change to some items in a collection ... and you'd like to do it with LINQ. There are several ways to do this (you can even get it down to the proverbial one line of code). In this column, I'll show you all the variations that I know of. You'll have to decide which way you think works best.

The Obvious Code
For this exercise, I'll assume a collection of Customer objects -- I want to retrieve all the valid customers in the collection and set their credit limit property to $1,000. The obvious way to do this is to write a foreach loop, check each item as it goes by, and update the credit limit property for the customers you like:

foreach( Customer cust in customers)
{
   if (cust.IsValid) 
   {
      cust.CreditLimit = 1000;
    }
}

Converting this code to use LINQ isn't hard to do:

var validCustomers = from c in customers
                     where c.IsValid
                     select c;
foreach( Customer cust in validCustomers)
{
      cust.CreditLimit = 1000;
}

The code is terser if you'd prefer to use LINQ's Where method and a lambda expression:

var validCustomers = customers.Where(c => c.IsValid);
foreach( Customer cust in validCustomers)
{
    cust.CreditLimit = 1000;
}

Getting to One Line of Code
If you're willing to use the ToList method to convert the collection into a List, you can get an even terser statement by using the List object's ForEach method. That code looks like this:

var ValidCustomers = customers.Where(c => c.IsValid).ToList();
ValidCustomers.ForEach(c => c.CreditLimit = 1000);

The ForEach method doesn't return a value so I don't need a variable to catch anything.

The reason you have to convert your collection to a List is because the only thing you know about the collection returned by LINQ's Where clause is that it produces a collection that implements the IEnumerable interface. Unfortunately, there is no ForEach extension method that attaches itself to the IEnumerable interface so you can't use ForEach without calling ToList.

But we've already wandered into an area of some controversy: There's a couple of strong arguments against using a method like ForEach even if it is provided. For the philosophical argument of why ForEach LINQ is a bad idea see Eric Lippert's blog from way back in 2009.

From a more practical point of view, I'm not clear how Entity Framework would deal with converting a LINQ expression that included ForEach into SQL (I suspect it would fail). In my example, the ToList method forced Entity Framework to build and execute the SQL statement before the ForEach method was called.

Another option is to use LINQ's Select method. Normally, all we ask the Select method to do is return the object that will make up the new collection -- in fact, the Select method insists that the lambda expression passed to it return an object. However, if I'm willing to flesh out the lambda expression with all the optional stuff that I normally omit (curly braces, semicolons and the return keyword), I can give my lamdba expression an actual body. In that body, I can update a property on each object in in one statement and then make the Select method happy by returning that object. Here's that version:

customers.Where(c => c.IsValid).Select(c => { c.CreditLimit = 1000; return c; }).ToList();

There's a couple of things to notice about this version.

First, I've stopped accepting a new collection just as I did in the ForEach example. While the LINQ methods always return a new collection, they don't create a new set of objects: Both the input collection (customers, in my example) and the output collection (validCustomers, in my previous example) are just sets of pointers to the same objects. Since all I want to do is change the value on some of those objects, I don't need a reference to the collection this method returns. If I had additional lines of code after my previous example, I'm perfectly happy to use my original customers collection to refer to my Customer objects.

Second: I'm still using the ToList method, but this time it's there to make LINQ happy. If I omit the ToList, none of the objects in my customers collection will have had their CreditLimit updated. If I want, I can omit the ToList method if I catch the collection returned by my LINQ statement:

var newCustomers = customers.Where(c => c.IsValid).Select(c => { c.CreditLimit = 1000; return c; });

By the way: This code won't work with Entity Framework because Entity Framework can't, in a Select statement, convert a lambda expression with a body into SQL. To work with Entity Framework, I'd need to move the ToList() up to before the Select statement, like this:

db.Customers.Where(c => c.IsValid).ToList().Select(c => { c.CreditLimit = 1000; return c; });

This code will work with Entity Framework and the objects in the Customers collection will have their CreditLimit property updated.

Custom Code
Finally, if I'm unhappy with beating up on that defenseless Select method, I can write an extension method of my own that will attach itself to any collection that implements the IEnumerable interface. My extension method will, in turn, accept a lambda expression that accepts an object out of the collection. Within my extension method, I can loop through the collection my method has been called from. Within that loop, I'll call the lambda expression that's been passed in to my method, passing the lambda expression each object from the collection. Finally, I can return the updated collection:

public static class PHVExtensions
{
    public static IEnumerable<T> SetValue<T>(this IEnumerable<T> items, Action<T>
         updateMethod)
    {
        foreach (T item in items)
        {
            updateMethod(item);
        }
        return items;
    }
}

With that method in place, I can write my statement like this:

customers.Where(c => c.IsValid).SetValue(c => c.CreditLimit = 1000).ToList();

Or like this:

var newCustomers = customers.Where(c => c.IsValid).SetValue(c => c.CreditLimit = 1000);

Or like this for Entity Framework:

db.Customers.Where(c => c.IsValid).ToList().SetValue(c => c.CreditLimit = 1000);

And let me be clear: I'm not advocating for any of these solutions. I am, as economists say, relatively indifferent to all of them. But I'd be keenly interested in your opinion. Which do you like and (more importantly) why?

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube