3

In T-SQL you can use CROSS APPLY to get all possible variations between the table left and right from the statement. Now I have the following situation in C# and I hope there is a way to solve my problem using LINQ-to-Objects.

I have a list with TestData objects (like below) which is similar to the KeyValuePair<string, object> object (Just a Key and a Value property): The key can be everything and there can be multiple objects with the same key.

IList<KeyValuePair<String, Object>> objects;
// Content of list
// # | Key  | Value
// 1 | "A"  | 1
// 2 | "A"  | 2
// 3 | "A"  | 3
// 4 | "B"  | 4
// 5 | "B"  | 5
// 6 | "C"  | 6
// 7 | "D"  | 7
// 8 | "D"  | 8

I have also a list of requested keys:

IList<String> requestedKeys = new List<string>() { "A", "D" };

Now I want to have all possible combinations of KeyValuePair objects between the keys in the requestedKeys list.

IList<IList<KeyValuePair<String, Object>>> result = ...
// Content of 'result' will be in this example 6 lists with each 2 KeyValuePair objects
// #  | "A" | "D" | (If there are more in the requestedKey list then there are more KeyValuePair items in the innerlist.)
// 1  |  1  |  7  |
// 2  |  2  |  7  |
// 3  |  3  |  7  |
// 4  |  1  |  8  |
// 5  |  2  |  8  |
// 6  |  3  |  8  |

Is it possible to solve my problem using LINQ-to-Objects. If not can you tell me the most efficient way to build it anyway.


EDIT 1:
To make more clear what the result should be:
I want to have a LINQ-to-Objects query something like this:
@Joanna thanks for the tip about multiple froms but the problem is: With this syntax you cannot have a dynamic amount of froms. In my case I need as many froms as items in the requestedKeys list

var result =    
   from listA in objects.Where(m => m.Key == "A")
   from listD in objects.Where(m => m.Key == "D")
   // from .....
   // from .....
   // overhere as many froms as items in 'requestedKeys' list   
select new [] { listA, listD /*, All other lists */ }
hwcverwe
  • 5,287
  • 7
  • 35
  • 63
  • http://blogs.msdn.com/b/ericlippert/archive/2010/06/28/computing-a-cartesian-product-with-linq.aspx – Dave Bish May 22 '12 at 16:09
  • So what is wrong with @Joanna's answer? (and your edit?) – leppie May 23 '12 at 12:29
  • @leppie it is not dynamic enough. for example what if I request all possible combinations with A B and C. Then you need to have a third `from` and a third KeyValuePair in the result list. So I want to know how to make it dynamic. – hwcverwe May 23 '12 at 13:04

4 Answers4

3

Something along these lines should work:

var filtered = objects
        .Where(o => requestedKeys.Contains(o.Key));

var crossJoined = from el1 in filtered
                    from el2 in filtered
                    select new [] {el1, el2};

The cross join is achieved by chaining multiple from clauses.

EDIT:

In this case I can't think of an easier way of doing this than what you started in your edit. The only missing thing is to select the values:

var result =    
   from listA in objects.Where(m => m.Key == "A").Select(m => m.Value)
   from listD in objects.Where(m => m.Key == "D").Select(m => m.Value)
   // from .....
   // from .....
   // overhere as many froms as items in 'requestedKeys' list  
select new [] { listA, listD /*, All other lists */ }
Joanna Derks
  • 4,033
  • 3
  • 26
  • 32
  • Thanks for your answer. I am glad to hear I can make use of multiple from clauses but now you perform a cross join between all values in `filtered`. But I want to have a separate list for each item in `requestedKeys` to cross apply. Look at my last edit. – hwcverwe May 23 '12 at 07:05
  • @hwcverwe - I edited the code you posted to select values - not sure if that is already what you wanted ? – Joanna Derks May 23 '12 at 12:27
  • the problem is that you have a dynamic amount of `from`s and a result innerlist with a dynamic amount of items. (amount of `from` and the amount of items in the innerlist are equal to the amount of requestedKeys). I have solved the problem today. Look at my answer. It is way more complex than I thought in the beginning. Thanks for your help – hwcverwe May 23 '12 at 12:50
1

I found the solution myself:

It is a very complex join in LINQ because each item in the requestKeys list requires an extra cross join. Regarding to the given example list, the result should be objects.Count(m => m.Key == "A") * objects.Count(m => m.Key == "D") (result is 3 * 2 = 6). Each extra item in the list causes an extra multiply of the whole result set.

So this is the result:

// The result list
IEnumerable<IList<KeyValuePair<char, int>>> result;

// If there are no requestedKeys there is no result expected
if(requestedKeys.Count() > 0)
{
    // Loop through all request keys to cross join them together
    foreach (var key in requestedKeys)
    {
        if (result == null)
        {
            // First time the innerlist List<KeyValuePair<char, int>> will contain 1 item
            // Don't forget to use ToList() otherwise the expression will be executed to late.
            result = objects.Where(m => m.Key == key).Select(m => new List<KeyValuePair<char, int>>() { m }).ToList();
        }
        else
        {
            // Except for the first time the next subresult will be cross joined
            var subresult = objects.Where(m => m.Key == key).Select(m => new List<KeyValuePair<char, int>>() { m });
            result = result.Join(
                subresult,
                l1 => 0, // This and the next parameter does the cross join trick
                l2 => 0, // This and the previous parameter does the cross join trick
                (l1, l2) => l1.Concat(l2).ToList() // Concat both lists which causes previous list plus one new added item
                ).ToList(); // Again don't forget to 'materialize' (I don't know it is called materialization in LINQ-to-Objects 
                            // but it has simular behaviors because the expression needs to be executed right away)
        }
    }           
}
return result;

Unfortunately it is not completely LINQ so if someone know an better solution. Please comment me or answer my question :)

hwcverwe
  • 5,287
  • 7
  • 35
  • 63
  • I want to scratch my eyes out when I see weird solutions like this..! The query is very simple: requestedKeys.GroupJoin(objects, key => key, m => m.Key, (key, ms) => new {Key, ms}).Where(keygrp => keygrp.Any()) – Morten Gorm Madsen Oct 23 '17 at 07:59
  • @MortenGormMadsen. Your answer does not give the requested result. This answer was given in 2012 hopefully there are better ways now. I still hope you can find a better solution because I agree that this code is not really readable – hwcverwe Nov 03 '17 at 14:30
1

user this way can genreate sql cross apply:

    var comments = AppCommentRepository.Where(com => com.iAction > -1 && productIds.Contains(com.sProductId))
            .GroupBy(c => c.sProductId)
            .SelectMany(p => p.OrderByDescending(cc => cc.dAddTime).Take(commentNum)).ToList();

finally,the sql is :

    SELECT [t3].[iCommentId], .....FROM (
        SELECT [t0].[sProductId]
        FROM [dbo].[App_Comment] AS [t0]
        WHERE ([t0].[iAction] > -1) --AND ([t0].[sProductId] IN (@p1))
           GROUP BY [t0].[sProductId]
        ) AS [t1]
CROSS APPLY (
    SELECT TOP (2) [t2].[iCommentId],......
    FROM [dbo].[App_Comment] AS [t2]
    WHERE ([t1].[sProductId] = [t2].[sProductId]) AND ([t2].[iAction] > -1)
-- AND ([t2].sProductId] IN (@p1))
    ORDER BY [t2].[dAddTime] DESC
    ) AS [t3]
ORDER BY [t3].sProductId DESC
adu
  • 39
  • 3
0
objects
.Join(requestedKeys, o => o.Key, rk => rk, (o, rk) => o)
.SelectMany(o => requestedKeys.Select(k => new {Key = k, Value = o.Value}));
Brian
  • 395
  • 1
  • 3
  • 14