views:

1076

answers:

7

Consider the requirement to change a data member on one or more properties of an object that is 5 or 6 levels deep.

There are sub-collections that need to be iterated through to get to the property that needs inspection & modification.

Here we're calling a method that cleans the street address of a Employee. Since we're changing data within the loops, the current implementation needs a for loop to prevent the exception:

Cannot assign to "someVariable" because it is a 'foreach iteration variable'

Here's the current algorithm (obfuscated) with nested foreach and a for.

foreach (var emp in company.internalData.Emps)
{
    foreach (var addr in emp.privateData.Addresses)
    {
        int numberAddresses = addr.Items.Length;

        for (int i = 0; i < numberAddresses; i++)
        {
            //transform this street address via a static method
            if (addr.Items[i].Type =="StreetAddress")
            addr.Items[i].Text = CleanStreetAddressLine(addr.Items[i].Text);
        }
    }
}

Question: Can this algorithm be reimplemented using LINQ? The requirement is for the original collection to have its data changed by that static method call.

Update: I was thinking/leaning in the direction of a jQuery/selector type solution. I didn't specifically word this question in that way. I realize that I was over-reaching on that idea (no side-effects). Thanks to everyone! If there is such a way to perform a jQuery-like selector, please let's see it!

+6  A: 
foreach(var item in company.internalData.Emps
                        .SelectMany(emp => emp.privateData.Addresses)
                        .SelectMany(addr => addr.Items)
                        .Where(addr => addr.Type == "StreetAddress"))
     item.Text = CleanStreetAddressLine(item.Text);
Mehrdad Afshari
+1 ahh man, you always beat me and your code is always so much better :(
Stan R.
+1  A: 

LINQ does not provide the option of having side effects. however you could do:

company.internalData.Emps.SelectMany(emp => emp.Addresses).SelectMany(addr => Addr.Items).ToList().ForEach(/*either make an anonymous method or refactor your side effect code out to a method on its own*/);
Rune FS
+1  A: 
var dirtyAddresses = company.internalData.Emps.SelectMany( x => x.privateData.Addresses )
                                              .SelectMany(y => y.Items)
                                              .Where( z => z.Type == "StreetAddress");

  foreach(var addr in dirtyAddresses)
    addr.Text = CleanStreetAddressLine(addr.Text);
Stan R.
+1  A: 

LINQ is not intended to modify sets of objects. You wouldn't expect a SELECT sql statement to modify the values of the rows being selected, would you? It helps to remember what LINQ stands for - Language Integrated Natural Query. Modifying objects within a linq query is, IMHO, an anti-pattern.

Stan R.'s answer would be a better solution using a foreach loop, I think.

Josh E
reason for the downvote?
Josh E
i don't know why someone downvoted, i tend to agree with you on a general basis.
Stan R.
thanks. yours and Merhdad's answers were functionally identical, but I saw yours first :) I figured it would be pointless to retype that code, but I felt it was important to make the point about side effects
Josh E
A: 

You can do this, but you don't really want to. Several bloggers have talked about the functional nature of Linq, and if you look at all the MS supplied Linq methods, you will find that they don't produce side effects. They produce return values, but they don't change anything else. Search for the arguments over a Linq ForEach method, and you'll get a good explanation of this concept.

With that in mind, what you probaly want is something like this:

var addressItems = company.internalData.Emps.SelectMany(
    emp => emp.privateData.Addresses.SelectMany(
           addr => addr.Items
    )
);
foreach (var item in addressItems)
{
   ...
}

However, if you do want to do exactly what you asked, then this is the direction you'll need to go:

var addressItems = company.internalData.Emps.SelectMany(
    emp => emp.privateData.Addresses.SelectMany(
           addr => addr.Items.Select(item =>
           { 
              // Do the stuff
              return item;
           }) 
    )
);
John Fisher
+1  A: 

I don't like mixing "query comprehension" syntax and dotted-method-call syntax in the same statement.

I do like the idea of separating the query from the action. These are semantically distinct, so separating them in code often makes sense.

var addrItemQuery = from emp in company.internalData.Emps
                    from addr in emp.privateData.Addresses
                    from addrItem in addr.Items
                    where addrItem.Type == "StreetAddress"
                    select addrItem;

foreach (var addrItem in addrItemQuery)
{
    addrItem.Text = CleanStreetAddressLine(addrItem.Text);
}

A few style notes about your code; these are personal, so I you may not agree:

  • In general, I avoid abbreviations (Emps, emp, addr)
  • Inconsistent names are more confusing (addr vs. Addresses): pick one and stick with it
  • The word "number" is ambigious. It can either be an identity ("Prisoner number 378 please step forward.") or a count ("the number of sheep in that field is 12."). Since we use both concepts in code a lot, it is valuable to get this clear. I use often use "index" for the first one and "count" for the second.
  • Having the type field be a string is a code smell. If you can make it an enum your code will probably be better off.
Jay Bazuzi
Thanks for your comments Jay. The code was not copy-pasted from the actual project, and was obfuscated for public consumption.
p.campbell
A: 

Dirty one-liner.

company.internalData.Emps.SelectMany(x => x.privateData.Addresses)
    .SelectMany(x => x.Items)
    .Where(x => x.Type == "StreetAddress")
    .Select(x => { x.Text = CleanStreetAddressLine(x.Text); return x; });
Matajon