Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Distinct of Enumerable.Select of nested List in C#

public class Country
{
    public List<State> States { get; set; } = new List<State>();
}

public class State
{
    public List<City> Cities { get; set; } = new List<City>();
}

public class City
{
    public decimal IdSeaLevel { get; set; }
}

IdSeaLevel Has these possible expected values: 0, 1, 2.

Then is needed to check al values inserted by user to prevent some different value.

Suppose that the user send us an country (object of Country class) with its list filled (and nested too).

How to get all Distinct IdSeaLevel inserted value by the user?

I was thinking like:

List<decimal> AllowedIdSeaLevellist = new List<decimal>(new decimal[] { 0, 1, 2 });

Now, I get a Distict inserted Values

HashSet<decimal> SentIdSeaLevelSet = country.States
                .Select(s => s.Cities.IdSeaLevel).ToHashSet();

Check

bool badRequest= SentIdSeaLevelSet
    .Where(s => AllowedIdSeaLevellist.All(a => a != s)).Any();
like image 412
Anita Avatar asked Dec 23 '21 23:12

Anita


2 Answers

.SelectMany will map List of lists into single list (flattened)

var allSeaLevels = country.States
   .SelectMany(s => s.Cities)
   .Select(city => city.SeaLevelId)
   .ToHashSet();

To get "invalid" sea levels you can alternatively to gather them while looping through sealevels.

var validSeaLevels = new[] { 0, 1, 2 }.ToHashSet();

var invalidSeaLevels = country.States
   .SelectMany(s => s.Cities)
   .Select(city => city.SeaLevelId)
   .Where(level => validSeaLevels.Contains(level) == false)
   .ToArray();

if (invalidSeaLevels.Any())
{
    return BadRequest(invalidSeaLevels);
}  
like image 112
Fabio Avatar answered Oct 18 '22 21:10

Fabio


This type of deep linking is where SelectMany<T> becomes helpful:

HashSet<decimal> SentIdSeaLevelSet = country.States
                .SelectMany(s => s.Cities.Select(c => c.IdSeaLevel)).Distinct().ToHashSet()

We want to project the IdSeaLevel but Cities is a List, so at some point you need the inner Cities.Select() but that can be inside or after the SelectMany which effectively flattens the hierarchy so that all of the nested Cities become a single list, the following would also work:

HashSet<decimal> SentIdSeaLevelSet = country.States
               .SelectMany(s => s.Cities).Select(c => c.IdSeaLevel).Distinct().ToHashSet()

I prefer to use projection first inside the SelectMany it we never need any other properties from the City objects (the first example) but different applications and structures might dictate that the second expression performs better.

For the final comparison your logic looks ok, another way to compare lists is by using except:

bool badRequest= SentIdSeaLevelSet
    .Except(AllowedIdSeaLevellist).Any();

This is functionally equivalent to your previous comparison and works because the types of the collections being compared are the same, runtime might be marginally faster but at this level you base your decision on code readability, which is a subjective topic on its own, but I prefer the except version specifically when we are comparing lists.

like image 35
Chris Schaller Avatar answered Oct 18 '22 21:10

Chris Schaller