Schotime
Schotime

Reputation: 15997

Putting an order number on items in a linq query

I have the following Linq query. transactionData is an IEnumerable.

var totalTransactions = 0;
viewModel.GroupedTransactions = transactionData
    .GroupBy(x => new { DocumentId = x.DocumentId ?? "Un Documented" })
    .Select(x => new GroupedTransaction
    {
        DocumentId = x.Key.DocumentId,
        Transactions = x.Select(y => new Transaction
        {
            Amount = y.CommitAmount,
            ActivityType = y.ActivityType,
            Number = totalTransactions++
        })
    })
    .OrderBy(x => x.DocumentId);

where I'm trying to set the Number on the Transaction record to be an incremented number. This doesn't work, leaving gaps in the numbers.

I also tried the following after the query.

foreach (var item in viewModel.GroupedTransactions.SelectMany(x => x.Transactions))
{
     item.Number = totalTransactions;
     totalTransactions++;
}

This didn't even update the Number value. What am I doing wrong, or is there a simpler way, with a neat linq extension method?

Upvotes: 0

Views: 3096

Answers (2)

BrokenGlass
BrokenGlass

Reputation: 160942

The problem is that you are closing over the variable totalTransactions, you have to create a local copy to use. Check Closing over the loop variable considered harmful for a more detailed explanation.

Something like this should work:

var totalTransactions = 0;
viewModel.GroupedTransactions = transactionData
    .GroupBy(x => new { DocumentId = x.DocumentId ?? "Un Documented" })
    .Select(x => 
    {
      new GroupedTransaction()
      {
        DocumentId = x.Key.DocumentId,
        Transactions = x.Select(y => 
        {
          var currentTransactionId = totalTransactions;
          totalTransactions++;

          return new Transaction
          {
            Amount = y.CommitAmount,
            ActivityType = y.ActivityType,
            Number = currentTransactionId 
          }
        })
      }
    })
    .OrderBy(x => x.DocumentId);

For your second approach with the foreach loop - you are actually creating a new enumeration with SelectMany() that you subsequently just throw away:

foreach (var item in viewModel.GroupedTransactions.SelectMany(x => x.Transactions))
{
     item.Number = totalTransactions;
     totalTransactions++;
}

Instead you have to force eager evaluation of your collection by using ToList() to create a collection you can safely modify.

var transactions = viewModel.GroupedTransactions
                            .SelectMany(x => x.Transactions)
                            .ToList();
foreach (var item in transactions)
{
     item.Number = totalTransactions;
     totalTransactions++;
}

Upvotes: 1

Chris Pitman
Chris Pitman

Reputation: 13104

Another way to think about it is that you have two sequences:

  1. Transactions
  2. "auto incremented" index

And you want to get one sequence, transactions with ids. When we want to combine two sequences, we can use the Zip operator:

viewModel.GroupedTransactions = transactionData     
    .GroupBy(x => new { DocumentId = x.DocumentId ?? "Un Documented" })
    .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new GroupedTransaction     
    {         
        DocumentId = x.Key.DocumentId,         
        Transactions = x.Select(y => new Transaction         
        {             
            Amount = y.CommitAmount,             
            ActivityType = y.ActivityType,             
            Number = index         
        })     
    })     
    .OrderBy(x => x.DocumentId); 

Is this what you had in mind?

Zip combines two sequences until it reaches the end of one of the sequences. Thats why it is ok tu Enumberable.Range to get a much larger range of numbers than we actually need.

Upvotes: 1

Related Questions