Losing foreach with Linq queries

190 views Asked by At

I was wondering if I could replace this foreach with LINQ queries somehow (if possible):

Online playpen: https://ideone.com/PQEytf

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
    public static void Main()
    {
        // dummy inputs for test sake
        var keys = new[] { "key1", "key2" };

        var services = new Dictionary<string, Service>
        {
            {"key1", new Service {Components = new Dictionary<string, string> {{"comp1", "value1"}}}},
            {"key2", new Service {Components = new Dictionary<string, string> {{"comp2", "value2"}}}}
        };


        var serviceComponents = GetServiceComponents(keys, services);
        // do something with it
    }

    public static IEnumerable<ServiceComponent> GetServiceComponents(string[] keys, Dictionary<string, Service> services)
    {
        var serviceComponents = new List<ServiceComponent>();

        // this is the foreach that I want to lose
        foreach (
            var key in
                keys.Select(
                    name =>
                        services.FirstOrDefault(
                            x => x.Key.Equals(name))))
        {
            serviceComponents.AddRange(key.Value.Components.Select(component => new ServiceComponent
            {
                Name = key.Key,
                Service = component.Key,
                Value = component.Value,
            }));
        }

        return serviceComponents.ToArray();

    }
}

public class Service
{
    public Dictionary<string, string> Components { get; set; }
}

public class ServiceComponent
{
    public string Name { get; set; }
    public string Service { get; set; }
    public string Value { get; set; }
}
3

There are 3 answers

1
Zack Butcher On BEST ANSWER

What you need is SelectMany, however, by using FirstOrDefault you're opening yourself up to a NullReferenceException (as the default value for any reference type is null). If you intend to have a NullReferenceException thrown when an element of keys is not a key in services then you can use other answers which leverage SelectMany. However, is you do not intend a NullReferenceException, then you should use something like the following:

return services.Where(pair => keys.Contains(pair.Key))
               .SelectMany(pair => 
                           pair.Value.Components
                                .Select(component => new ServiceComponent
                                                         {
                                                             Name = pair.Key,
                                                             Service = component.Key,
                                                             Value = component.Value
                                                          }))
               .ToArray();

This statement removes all key-value pairs from services whose key is not in the keys array, then turns each element of each pair's Components into a new ServiceComponent object, with the SelectMany making a single flat list out of the new ServiceComponents.

1
Servy On

Yes, what you're looking for is SelectMany. That allows you to turn each item in the sequence into another sequence, and then flatten all of those sequences into a single sequence. (You're accomplishing the same thing, without the deferred execution, by putting all of the sequences into a list.)

return keys.SelectMany(name => services.FirstOrDefault(x => x.Key.Equals(name))
    .Value.Components
    .Select(component => new ServiceComponent
    {
        Name = name.Key,
        Service = component.Key,
        Value = component.Value,
    }))
    .ToArray();

Having said that, what this query is doing is taking each of your keys, finding the corresponding item in services using a linear search, and then mapping the result. Rather than doing a linear search using FirstOrDefault, you can use the dictionary's native ability to effectively and efficiently find values for each key:

return keys.SelectMany(key => services[key].Components
    .Select(component => new ServiceComponent
    {
        Name = key,
        Service = component.Key,
        Value = component.Value,
    }))
    .ToArray();
6
Jim Wooley On

To extend @Servy's example, I often find the LINQ expression syntax to be easier to read than the lambda SelectMany (it translates to the same thing). Here is his query using query expressions:

return from key in keys
       from component in services[key].Components
       select new ServiceComponent
       {
          Name = key,
          Service = component.Key,
          Value = component.Value,
       }))
       .ToArray();