Chapter 9 - LINQ Operators

Filtering

Simple Local Filter

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
  names.Where (name => name.EndsWith ("y"));

query.Dump ("In fluent syntax");

query =
  from n in names
  where n.EndsWith ("y")
  select n;
  
query.Dump ("In query syntax");

Multiple Where Clauses

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

var query =
  from n in names
  where n.Length > 3
  let u = n.ToUpper()
  where u.EndsWith ("Y")
  select u;
  
query.Dump();

Indexed Filtering

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.Where ((n, i) => i % 2 == 0).Dump ("Skipping every second element");

Contains and LIKE when querying a database

Customers.Where (c => c.Name.Contains ("a"))  
  .Dump ("Notice the SQL translation uses LIKE");

Customers.Where (c => c.Name.StartsWith ("J"))  
  .Dump ("StartsWith and EndsWith also translate to LIKE");

Customers.Where (c => EF.Functions.Like (c.Name, "_ar%y"))
  .Dump ("A more complex use of LIKE");

IN and NOT IN when querying a database

string[] chosenOnes = { "Tom", "Jay" };

Customers.Where (c => chosenOnes.Contains (c.Name))
  .Dump ("This translates to SQL WHERE ... IN");
  
Customers.Where (c => !chosenOnes.Contains (c.Name))
  .Dump ("This translates to SQL WHERE NOT ... IN");

Extra - Where-based Subqueries

// We saw how to construct a basic subquery in Chapter 8:

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();
(
  from n in names
  where n.Length == names.Min (n2 => n2.Length)
  select n
)
.Dump ("Basic subquery");

// The same principle works well when querying a database:
(
  from c in Customers
  where c.Name.Length == Customers.Min (c2 => c2.Name.Length)
  select c
)
.Dump ("Basic subquery, EF Core");

// We can construct similar subqueries across association properties:
(
  from c in Customers
  where c.Purchases.Any (p => p.Price > 1000)
  select c
)
.Dump ("Customers who have purchased at least one item > $1000");

Extra - Where-based Subqueries with let

// The let keyword in query expressions comes in useful with subqueries: it lets
// you re-use the subquery in the projection:

from c in Customers
let highValuePurchases = c.Purchases.Where (p => p.Price > 1000)
where highValuePurchases.Any()
select new
{
  c.Name,
  highValuePurchases
}

// We'll see more examples of this in the following section, "Projecting".

Take and Skip

// We need a long list of names for this example, so will indulge in a little
// reflection (using LINQ, of course!) The following query extracts all type 
// names in the System.CoreLib assembly:

string[] typeNames =
  (from t in typeof (int).Assembly.GetTypes() select t.Name).ToArray();

typeNames
  .Where (t => t.Contains ("Exception"))
  .OrderBy (t => t)
  .Take (20)
  .Dump ("The first 20 matches");

typeNames
  .Where   (t => t.Contains ("Exception"))
  .OrderBy (t => t)
  .Skip (20)
  .Take (20)
  .Dump ("Matches 21 through 40");

Take and Skip when querying a database

// The following skips the first 5 purchases (ordered by price) and takes the next 3:

Purchases.OrderBy (p => p.Price).Skip (5).Take(3)

// Take a look at the SQL.  So much easier with LINQ!

TakeWhile and SkipWhile

int[] numbers = { 3, 5, 2, 234, 4, 1 };

numbers.TakeWhile (n => n < 100).Dump ("TakeWhile");
numbers.SkipWhile (n => n < 100).Dump ("SkipWhile");

Distinct

"HelloWorld".Distinct()

Distinct querying a database

Purchases.Select (p => p.Description).Distinct()
Projecting - Select

Simple Local Select

var query =
  from f in FontFamily.Families
  select f.Name;

query.Dump ("In query syntax");


FontFamily.Families.Select (f => f.Name)   .Dump ("In lambda syntax");

Projecting into Anonymous Type

from f in FontFamily.Families.AsQueryable() 
select new 
{
  f.Name,
  LineSpacing = f.GetLineSpacing (FontStyle.Bold)
}

Select - No Transformation

from f in FontFamily.Families.AsQueryable() 
where f.IsStyleAvailable (FontStyle.Strikeout)
select f

Select - Indexed

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.Select ((s,i) => i + "=" + s)  .Dump();

Select-Subqueries and Object Hierarchies

string sampleDirectory = Environment.GetFolderPath  (Environment.SpecialFolder.MyDocuments);

DirectoryInfo[] dirs = new DirectoryInfo (sampleDirectory).GetDirectories();

var query =
  from d in dirs  
  where (d.Attributes & FileAttributes.System) == 0
  select new
  {
    DirectoryName = d.FullName,
    Created = d.CreationTime,
    Files = from f in d.GetFiles()
      where (f.Attributes & FileAttributes.Hidden) == 0
      select new { FileName = f.Name, f.Length, }
  };

query.Dump();

// Here's how to enumerate the results manually:

foreach (var dirFiles in query)
{
  Console.WriteLine ("Directory: " + dirFiles.DirectoryName);
  foreach (var file in dirFiles.Files)
    Console.WriteLine ("    " + file.FileName + "Len: " + file.Length);
}

Select-Subqueries querying a database

// Requires .ToList() because EF Core 3 cannot create Queryables in the select result.
// Punted for version 3 but may be resolved in the future. 
// Issue tracked at https://github.com/aspnet/EntityFrameworkCore/issues/16314

from c in Customers
select new
{
  c.Name,
  Purchases = (
    from p in Purchases
    where p.CustomerID == c.ID && p.Price > 1000
    select new { p.Description, p.Price }).ToList()
}

Select-Subqueries using Assocations

from c in Customers
select new
{
  c.Name,
  Purchases =
    from p in c.Purchases
    where p.Price > 1000
    select new { p.Description, p.Price }
}

Select-Subqueries Filtered

from c in Customers
where c.Purchases.Any (p => p.Price > 1000)
select new
{
  c.Name,
  Purchases =
    from p in c.Purchases
    where p.Price > 1000
    select new { p.Description, p.Price }
}

Select-Subqueries Filtered with let

from c in Customers
let highValueP =
  from p in c.Purchases
  where p.Price > 1000
  select new { p.Description, p.Price }
where highValueP.Any()
select new 
{
  c.Name,
  Purchases = highValueP
}
Projecting - SelectMany

Simple SelectMany

var fullNames = new[] { "Anne Williams", "John Fred Smith", "Sue Green" }.AsQueryable();

IEnumerable<string[]> query1 = fullNames.Select     (name => name.Split());
IEnumerable<string>   query2 = fullNames.SelectMany (name => name.Split());

query1.Dump ("A simple Select gives us a hierarchical result set");
query2.Dump ("SelectMany gives us a flat result set");

IEnumerable<string> query3 = 
  from fullName in fullNames
  from name in fullName.Split()     // Translates to SelectMany
  select name;

query3.Dump ("Same SelectMany query, but in query syntax");

Outer Range Variables

var fullNames = new[] { "Anne Williams", "John Fred Smith", "Sue Green" }.AsQueryable();

IEnumerable<string> query =
  from fullName in fullNames                       // fullName = outer variable
  from name in fullName.Split()                    // name = iteration variable
  select name + " came from " + fullName;
  
query.Dump ("Both variables are in scope");

Try Translating this to Fluent Syntax!

// (Without cheating, by looking at the lambda tab!)

var fullNames = new[] { "Anne Williams", "John Fred Smith", "Sue Green" }.AsQueryable();

IEnumerable<string> query =
  from fullName in fullNames
  from name in fullName.Split()
  orderby fullName, name
  select name + " came from " + fullName;
  
query.Dump();

The Solution

string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };

IEnumerable<string> query1 =
  from fullName in fullNames
  from x in fullName.Split().Select (name => new { name, fullName } ) 
  orderby x.fullName, x.name
  select x.name + " came from " + x.fullName;
  
query1.Dump ("The intermediate solution");

IEnumerable<string> query2 = fullNames
  .SelectMany (fName => fName.Split().Select (name => new { name, fName } ))
  .OrderBy (x => x.fName)
  .ThenBy  (x => x.name)
  .Select  (x => x.name + " came from " + x.fName);

query2.Dump ("Final translation to fluent syntax");

NOTE - Another way to call SelectMany

// SelectMany is overloaded to help you (slightly!) with queries that perform a Select
// within SelectMany's lambda expression. To illustrate, consider the following query:

string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" };

var query1 =
  fullNames
  .SelectMany (fName => fName.Split().Select (name => new { name, fName } ));

// We can re-write this as follows, and get the same result:

var query2 =
  fullNames
  .SelectMany (fName => fName.Split(), (fName, name) => new { name, fName } );
  
query1.Dump ("Using SelectMany+Select");
query2.Dump ("Using SelectMany with a collection selector + result selector");

// Instead of performing a Select inside the SelectMany, we supply SelectMany with two
// lambda expressions:
//   (a) the collection selector (which would otherwise come just before the .Select)
//   (b) the result selector (which would otherwise be fed into the .Select).
//
// C#, in the current release, favours this version of SelectMany when translating
// query syntax queries. Functionally, it has the same effect as performing
// a Select within a SelectMany.

Expanding and Flattening

from c in Customers
from p in c.Purchases
select c.Name + " bought a " + p.Description

Cross Product

var numbers = new[] { 1, 2, 3 }.AsQueryable();
var letters = new[] { "a", "b" }.AsQueryable();

IEnumerable<string> query =
  from n in numbers
  from l in letters
  select n.ToString() + l;
  
query.Dump();

Cross Product Join

var players = new[] { "Tom", "Jay", "Mary" }.AsQueryable();

IEnumerable<string> query =
  from name1 in players
  from name2 in players
  select name1 + " vs " + name2;

query.Dump();

Cross Product Join - Filtered

var players = new[] { "Tom", "Jay", "Mary" }.AsQueryable();

IEnumerable<string> query =
  from name1 in players
  from name2 in players
  where name1.CompareTo (name2) < 0
  orderby name1, name2
  select name1 + " vs " + name2;

query.Dump();

Database - Cross Join

from c in Customers
from p in Purchases
select c.Name + " might have bought a " + p.Description

Database - Standard (equi) Inner Join

var query1 =
  from c in Customers
  from p in Purchases
  where c.ID == p.CustomerID
  select c.Name + " bought a " + p.Description;

var query2 =
  from c in Customers
  from p in c.Purchases
  select c.Name + " bought a " + p.Description;

query1.Dump ("Using SelectMany to manually join");
query2.Dump ("Using an Association to achieve the same result");

Database - Filtered Association

from c in Customers
where c.Name.StartsWith ("T")
from p in c.Purchases
select new { c.Name, p.Description }

Database - Joining to the Parent

// Don't make this harder than it needs to be!

from p in Purchases
select new
{
   p.Description,
   CustomerName = p.Customer.Name
}

Database - Outer Joins with SelectMany

(
  from c in Customers
  from p in c.Purchases
  where p.Price > 1000
  select new { c.Name, p.Description, p.Price }
)
.Dump ("An inner join");

(
  from c in Customers
  from p in c.Purchases.DefaultIfEmpty()
  select new { c.Name, p.Description, Price = (decimal?) p.Price }
)
.Dump ("An outer join (without the predicate)");

Local query - Outer Joins with SelectMany

Customer[] localCustomerCollection = Customers.ToArray();

var query =
  from c in localCustomerCollection
  from p in c.Purchases.DefaultIfEmpty()
  select new
  {
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?) null : p.Price
  };

query.Dump();

Database - Filtered Outer Joins

from c in Customers
from p in c.Purchases.Where (p => p.Price > 1000).DefaultIfEmpty()
select new
{
  c.Name,
  p.Description,
  Price = (decimal?)p.Price
}

// We could do this instead:

//var dbQuery =
//from c in Customers
//select new
//{
//  c.Name,
//  Purchases =
//    from p in c.Purchases
//    where p.Price > 1000
//    select new { p.Description, p.Price }
//};
//
//dbQuery.Dump();

// It achieves the same result (outer join), except that you end up with shaped data 
// instead of flat data. Shaped data is usually easier to work with, anyway, and
// if you really need flat data, you could always flatten it via a local query at
// the end:

//var flat =
//  from result in dbQuery.AsEnumerable()
//  from purchase in result.Purchases.DefaultIfEmpty()
//  select new
//  {
//    result.Name,
//    purchase?.Description,
//    purchase?.Price
//  };
//  
//flat.Dump ("If you really need flat data");

// Yet another option is to check out whether the joining operators can be
// used to achieve this result in EFCore.

Extra - Database - Manual Outer Joins

// You can also use a Where subquery to join manually, rather than 
// through Association properties:

from c in Customers
from p in Purchases.Where (p => p.CustomerID == c.ID).DefaultIfEmpty()
select new
{
  c.Name,
  p.Description,
  Price = (decimal?) p.Price
}

// You can use a similar strategy to perform nonequi-outer joins.
Joining

Simple Join

// Note: before delving into this section, make sure you've read the preceding two
// sections: Select and SelectMany. The Join operators are actually unnecessary
// when querying a database, and the equivalent of SQL inner and outer joins is 
// most easily achieved using Select/SelectMany and subqueries!

from c in Customers
join p in Purchases on c.ID equals p.CustomerID
select c.Name + " bought a " + p.Description

Join vs SelectMany

Customer[] customers = Customers.ToArray();
Purchase[] purchases = Purchases.ToArray();

var slowQuery =
  from c in customers
  from p in purchases where c.ID == p.CustomerID
  select c.Name + " bought a " + p.Description;

var fastQuery =
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  select c.Name + " bought a " + p.Description;
                
slowQuery.Dump ("Slow local query with SelectMany");                
fastQuery.Dump ("Fast local query with Join");

Join - Reversing Sequences

var query1 =
  from c in Customers
  join p in Purchases on c.ID equals p.CustomerID
  select c.Name + " bought a " + p.Description;
  
var query2 =
  from p in Purchases
  join c in Customers on p.CustomerID equals c.ID
  select c.Name + " bought a " + p.Description;

query1.Dump();
query2.Dump();

Joining Multiple Sequences

from c in Customers
join p in Purchases on c.ID equals p.CustomerID           // first join
join pi in PurchaseItems on p.ID equals pi.PurchaseID     // second join
select new
{
  c.Name, p.Description, pi.Detail
}

Joining on Multiple Keys

PropertyInfo[] stringProps = typeof (string).GetProperties().Dump ("String Props");
PropertyInfo[] builderProps = typeof (StringBuilder).GetProperties().Dump ("StringBuilder Props");

var query =
  from s in stringProps
  join b in builderProps
    on new { s.Name, s.PropertyType } equals new { b.Name, b.PropertyType }
  select new
  {
    s.Name,
    s.PropertyType,
    StringToken = s.MetadataToken,
    StringBuilderToken = b.MetadataToken
  };
  
query.Dump ("Join query");

Joining in Fluent Syntax

var querySyntax =
  from c in Customers
  join p in Purchases on c.ID equals p.CustomerID
  select new
  {
    c.Name, p.Description, p.Price
  };

var fluentSyntax =
  Customers.Join (         // outer collection
    Purchases,             // inner collection
    c => c.ID,             // outer key selector
    p => p.CustomerID,     // inner key selector
    (c, p) => new          // result selector 
    {
      c.Name, p.Description, p.Price
    }    
  );
  
querySyntax.Dump ("Query syntax");
fluentSyntax.Dump ("Same query in fluent syntax");

Joining in Fluent Syntax with OrderBy

var querySyntax =
  from c in Customers
  join p in Purchases on c.ID equals p.CustomerID
  orderby p.Price
  select c.Name + " bought a " + p.Description + " for $" + p.Price;
  
var fluentSyntax =
  Customers.Join (           // outer collection
    Purchases,               // inner collection
    c => c.ID,               // outer key selector
    p => p.CustomerID,       // inner key selector
    (c, p) => new { c, p }   // result selector 
  )
  .OrderBy (x => x.p.Price)
  .Select  (x => x.c.Name + " bought a " + x.p.Description + " for $" + x.p.Price);
  
querySyntax.Dump ("Query syntax");
fluentSyntax.Dump ("Same query in fluent syntax");

Simple GroupJoin

// NOTE: EF Core currently requires GroupBy without an aggregate expression to be explicitly performed client-side. This is because
//       the query translates into a GroupJoin, which SQL databases don't natively support unless an aggregate expression appears in
//       the SELECT list. The EF Core team takes the position that implicit, rather than explicit, client-side evaluation can mask
//       performance pitfalls. Subsequently, the developer must explicitly force client-side evaluation. This results in two queries
//       to the database, where Linq to SQL generated a single, more efficient query. The EF Core team is tracking the issue and
//       may revisit it at a future date.
//       https://github.com/aspnet/EntityFrameworkCore/issues/17068
from c in Customers.AsEnumerable()
join p in Purchases on c.ID equals p.CustomerID
into custPurchases
select custPurchases

GroupJoin with Outer Range Variable

Customer[] customers = Customers.ToArray();
Purchase[] purchases = Purchases.ToArray();

var groupJoinQuery =
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  select new
  {
    CustName = c.Name,
    custPurchases
  };

var selectEquivalent =
  from c in customers
  select new
  {
    CustName = c.Name,
    custPurchases = purchases.Where (p => c.ID == p.CustomerID)
  };

@"The GroupJoin query is more efficient in this case, because we're querying
arrays (i.e. local collections).".Dump();
  
groupJoinQuery.Dump ("Group Join Query");
selectEquivalent.Dump ("Equivalent with Select");

Same Query to a database

// Requires .ToList() because EF Core 3 cannot create Queryables in the select result.
// Punted for version 3 but may be resolved in the future. 
// Issue tracked at https://github.com/aspnet/EntityFrameworkCore/issues/16314

var groupJoinQuery =
  from c in Customers.AsEnumerable()
  join p in Purchases.AsEnumerable() on c.ID equals p.CustomerID
  into custPurchases
  select new
  {
    CustName = c.Name,
    custPurchases
  };

var selectEquivalent =
  from c in Customers 
  select new
  {
    CustName = c.Name,
    custPurchases = Purchases.Where (p => c.ID == p.CustomerID).ToList()
  };

@"Notice in the SQL results pane, that there's no difference between these two queries.
The second query, however, is more flexibile.".Dump();  

groupJoinQuery.Dump ("Group Join Query");
selectEquivalent.Dump ("Equivalent with Select");

GroupJoin with Inner Join Filter

// NOTE: EF Core currently requires GroupBy without an aggregate expression to be explicitly performed client-side. This is because
//       the query translates into a GroupJoin, which SQL databases don't natively support unless an aggregate expression appears in
//       the SELECT list. The EF Core team takes the position that implicit, rather than explicit, client-side evaluation can mask
//       performance pitfalls. Subsequently, the developer must explicitly force client-side evaluation. This results in two queries
//       to the database, where Linq to SQL generated a single, more efficient query. The EF Core team is tracking the issue and
//       may revisit it at a future date.
//       https://github.com/aspnet/EntityFrameworkCore/issues/17068

from c in Customers.AsEnumerable()
join p in Purchases on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any()
select new
{
  CustName = c.Name,
  custPurchases
}

GroupJoin with Pre-Filter

// NOTE: EF Core currently requires GroupBy without an aggregate expression to be explicitly performed client-side. This is because
//       the query translates into a GroupJoin, which SQL databases don't natively support unless an aggregate expression appears in
//       the SELECT list. The EF Core team takes the position that implicit, rather than explicit, client-side evaluation can mask
//       performance pitfalls. Subsequently, the developer must explicitly force client-side evaluation. This results in two queries
//       to the database, where Linq to SQL generated a single, more efficient query. The EF Core team is tracking the issue and
//       may revisit it at a future date.
//       https://github.com/aspnet/EntityFrameworkCore/issues/17068

from c in Customers.AsEnumerable()
join p in Purchases.Where (p2 => p2.Price > 1000)
  on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any()
select new
{
  CustName = c.Name,
  custPurchases
}

Outer Joins

from c in Customers
join p in Purchases on c.ID equals p.CustomerID into custPurchases
from cp in custPurchases.DefaultIfEmpty()
select new
{
  CustName = c.Name,
  Price = cp == null ? (decimal?) null : cp.Price
}

Joining with Lookups

Customer[] customers = Customers.ToArray();
Purchase[] purchases = Purchases.ToArray();

ILookup<int?, Purchase> purchLookup = purchases.ToLookup (p => p.CustomerID, p => p);

var inner =
  from c in customers
  from p in purchLookup [c.ID]
  select new { c.Name, p.Description, p.Price };

inner.Dump ("Inner join equivalent");

var outer =
  from c in customers
  from p in purchLookup [c.ID].DefaultIfEmpty()
  select new
  {
    c.Name,
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?) null : p.Price
  };
  
outer.Dump ("Outer join equivalent");

var groupJoin =
  from c in customers
  select new 
  {
     CustName = c.Name,
     CustPurchases = purchLookup [c.ID]
  };

groupJoin.Dump ("GroupJoin equivalent");

Extra - Weights Puzzle

// Luke Hoban, from Microsoft, has found a way to solve a puzzle with a LINQ query!
// His solution is dramatically faster with Join than SelectMany:
//
// http://blogs.msdn.com/lukeh/archive/2007/03/19/using-linq-to-solve-puzzles.aspx
//
// Here's the query in LINQPad: 

from a in Enumerable.Range(1, 13)
join b in Enumerable.Range(1, 13) on 4 * a equals b
from c in Enumerable.Range(1, 13)
join d in Enumerable.Range(1, 13) on 5 * c equals d
from e in Enumerable.Range(1, 13)
join f in Enumerable.Range(1, 13) on 3 * e equals 2 * f  
join g in Enumerable.Range(1, 13) on 2 * (c + d) equals 3 * g
from h in Enumerable.Range(1, 13)
join i in Enumerable.Range(1, 13) on 3 * h - 2 * (e + f) equals 3 * i
from j in Enumerable.Range(1, 13)
join k in Enumerable.Range(1, 13) on 3 * (a + b) + 2 * j - 2 * (g + c + d) equals k
from l in Enumerable.Range(1, 13)
join m in Enumerable.Range(1, 13) on (h + i + e + f) - l equals 4 * m
where (4 * (l + m + h + i + e + f) == 3 * (j + k + g + a + b + c + d))
select new
{
  a, b, c, d, e, f, g, h, i, j, k, l, m,
    Total = a + b + c + d + e + f + g + h + i + j + k + l + m
}
Ordering

Simple Ordering

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.OrderBy (s => s).Dump ("Ordered alphabetically");
names.OrderBy (s => s.Length).Dump ("Ordered by length");

Ordering on Multiple Expressions

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

names.OrderBy (s => s.Length).ThenBy (s => s)
  .Dump ("By length, then alphabetically");
  
names.OrderBy (s => s.Length).ThenBy (s => s[1]).ThenBy (s => s[0])
  .Dump ("By length, then second character, then first character");

(
  from s in names
  orderby s.Length, s[1], s[0]
  select s
)
.Dump ("Same query in query syntax");

Descending

Purchases
  .OrderByDescending (p => p.Price)
  .ThenBy (p => p.Description)
  .Dump ("In fluent syntax");

(
  from p in Purchases
  orderby p.Price descending, p.Description
  select p
)
.Dump ("In query syntax");

Comparers and Collations

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.OrderBy (n => n, StringComparer.CurrentCultureIgnoreCase)
  .Dump ("Case insensitive ordering");

(
  from c in Customers
  orderby c.Name.ToUpper()
  select c.Name
)
.Dump ("Closest equivalent when querying a SQL Server database with default collation");

IOrderedEnumerable and IOrderedQueryable

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IOrderedEnumerable<string> query1 = names.OrderBy (s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy (s => s);

query2.Dump();

var query = names.OrderBy (s => s.Length).AsEnumerable();
query = query.Where (n => n.Length > 3);

query.Dump();
Grouping

Simple GroupBy

string[] files = Directory.GetFiles (Path.GetTempPath()).Take (100).ToArray();

files.GroupBy (file => Path.GetExtension (file))
  .Dump ("Your temporary files, grouped by extension.");

if (files.Length == 100) "(Maybe you need a cleanup!)".Dump();

Enumerating groupings

string[] files = Directory.GetFiles (Path.GetTempPath()).Take (100).ToArray();

IEnumerable<IGrouping<string,string>> query = 
  files.GroupBy (file => Path.GetExtension (file));

foreach (IGrouping<string,string> grouping in query)
{
  Console.WriteLine ("Extension: " + grouping.Key);
  
  foreach (string filename in grouping)
    Console.WriteLine ("   - " + filename);
}

Transforming the Input Elements

var files = Directory.GetFiles (Path.GetTempPath()).Take (100).ToArray().AsQueryable();

files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper())
  .Dump ("In Fluent Syntax");
  
(
  from file in files
  group file.ToUpper() by Path.GetExtension (file)
)
.Dump ("In query syntax");

Group with Query Continuation

var files = Directory.GetFiles (Path.GetTempPath()).Take (100).ToArray().AsQueryable();

(
  from file in files
  group file.ToUpper() by Path.GetExtension (file) into grouping
  orderby grouping.Key
  select grouping
)
.Dump();

Query Continuation - Filtered

var files = Directory.GetFiles (Path.GetTempPath()).AsQueryable();

(
  from file in files
  group file.ToUpper() by Path.GetExtension (file) into grouping
  where grouping.Count() < 5
  select grouping
)
.Dump ("Extensions with less then five files");

Group - Aggregation Only

string[] votes = { "Dogs", "Cats", "Cats", "Dogs", "Dogs" };

IEnumerable<string> query = from vote in votes
                            group vote by vote into g
                            orderby g.Count() descending
                            select g.Key;

string winner = query.First();    // Dogs
winner.Dump();

Grouping when querying a database

from p in Purchases
group p.Price by p.Date.Year into salesByYear
select new
{
  Year       = salesByYear.Key,
  TotalValue = salesByYear.Sum()
}

Extra - Filtered Grouping with database

// The following groups purchases by year, then returns only those groups where
// the average purchase across the year was greater than $1000:

from p in Purchases
group p.Price by p.Date.Year into salesByYear
where salesByYear.Average (x => x) > 1000
select new
{
  Year       = salesByYear.Key,
  TotalSales = salesByYear.Count(),
  AvgSale    = salesByYear.Average(),
  TotalValue = salesByYear.Sum()
}

Grouping by Multiple Keys

from n in new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable()
group n by new
{
  FirstLetter = n[0],
  Length = n.Length
}

Extra - Nested Grouping

// (The following example requires the NORTHWIND database to run. You can
//  download the Northwind database here:
//  https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/linq/downloading-sample-databases
//
//  Follow the installation instructions at that URL then set your connection in this query window to
//  your Northwind database.)
// 
// Suppose you want to project a nested grouping, for example, producing
// a hierarchical output of countries, cities and post codes as follows:
//
//   Brazil
//      Campinas
//         04876-786
//      Resende
//         08737-363
//      Rio de Janeiro
//         02389-673
//         02389-890
//         05454-876
//
// We'll assume the country, city and post code information is all in one table called Orders 
// (as is it in the NORTHWIND sample database).
//
// To write this query, you need to nest one "group by" within the projection of another:

var query =
  from o in Orders
  group o by o.ShipCountry into countryGroups
  select new
  {
    Country = countryGroups.Key,
    Cities =
      from cg in countryGroups
      group cg.ShipPostalCode by cg.ShipCity into cityGroups
      select new
      {
        City = cityGroups.Key,
        PostCodes = cityGroups.Distinct()
      }
};

query.Dump();

// In this case, at the bottom level we are interested only in a simple list of postal codes (not in
// any further information about those orders) so we can use a simple Distinct to get the desired list.
//
// Here's how to programmatically enumerate the result:

foreach (var countryGroup in query)
{
  Console.WriteLine (countryGroup.Country);
  foreach (var cityGroup in countryGroup.Cities)
  {
    Console.WriteLine ("   " + cityGroup.City);
    foreach (string postCode in cityGroup.PostCodes)
      Console.WriteLine ("      " + postCode);
  }
}
Set Operators

Concat and Union

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

seq1.Concat (seq2).Dump ("Concat");
seq1.Union  (seq2).Dump ("Union");

Intersect and Except

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };

seq1.Intersect (seq2).Dump ("Intersect");
seq1.Except    (seq2).Dump ("seq1.Except (seq2)");
seq2.Except    (seq1).Dump ("seq2.Except (seq1)");

Extra - Concat and Union (database)

IQueryable<string> purchaseDescriptions = Purchases.Select (p => p.Description);
IQueryable<string> itemDescriptions = PurchaseItems.Select (pi => pi.Detail);

purchaseDescriptions.Union (itemDescriptions)
  .Dump ("Purchase and purchase item descriptions flattened with Union (notice the UNION in the SQL view)");
  
purchaseDescriptions.Concat (itemDescriptions)
  .Dump ("Purchase and purchase item descriptions flattened with Concat (notice the UNION ALL in the SQL view)");

Extra - Intersect and Except (database)

IQueryable<string> purchaseDescriptions = Purchases.Select (p => p.Description);
IQueryable<string> itemDescriptions = PurchaseItems.Select (pi => pi.Detail);

purchaseDescriptions.Intersect (itemDescriptions)
  .Dump ("Purchases that have an identical description in PurchaseItem");
  
purchaseDescriptions.Except (itemDescriptions)
  .Dump ("Purchases that have no corresponding description in PurchaseItem");
  
itemDescriptions.Except (purchaseDescriptions)
  .Dump ("PurchaseItems that have no corresponding description in Purchase");
Zip Operator

Zip operator

int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };
IEnumerable<string> zip = numbers.Zip (words, (n, w) => n + "=" + w);

zip.Dump();
Conversion Methods

Simple Cast

ArrayList classicList = new ArrayList();
classicList.AddRange ( new int[] { 3, 4, 5 } );

IEnumerable<int> sequence1 = classicList.Cast<int>();

sequence1.Dump ("Because sequence1 implements IEnumerable<int>, we can run queries on it");

Cast versus OfType

ArrayList classicList = new ArrayList();
classicList.AddRange ( new int[] { 3, 4, 5 } );

DateTime offender = DateTime.Now;
classicList.Add (offender);

IEnumerable<int>
  ofTypeSequence = classicList.OfType<int>(),
  castSequence = classicList.Cast<int>();
  
ofTypeSequence.Dump ("Notice that the offending DateTime element is missing");

try
{ 
  castSequence.Dump();
}
catch (InvalidCastException ex)
{
  ex.Message.Dump ("Notice what the offending DateTime element does to the Cast sequence");
}

Cast versus the C# Cast Operator

int[] integers = { 1, 2, 3 };

IEnumerable<long> test1 = integers.OfType<long>();
test1.Dump ("OfType returns no results");

IEnumerable<long> test2 = integers.Cast<long>();
test2.Dump ("Cast returns a sequence of three long integers!"); // Throws an exception. See Chapter 9, "OfType and Cast"

// Here's an alternative approach, using a projection:

integers.Select (s => (long) s).Dump ("The correct approach");

Cast in Query Syntax

// Query syntax provides a shortcut for using the Cast operator on the 
// input sequence. You simply include the type name directly after the from clause:

object[] untyped = { 1, 2, 3 };

var query1 =
  from i in untyped.Cast<int>()      // Without syntactic shortcut
  select i * 10;

var query2 =
  from int i in untyped       // Notice we've slipped in "int"
  select i * 10;

query1.Dump ("Explicitly calling Cast operator");
query2.Dump ("Syntactic shortcut for same query");

ToArray and ToList

IEnumerable<string> query = "one two two three".Split().Distinct();

string[] toArray = query.ToArray();
List<string> toList = query.ToList();

toArray.Dump(); 
toList.Dump();

ToDictionary

Dictionary<int, string> idToName = Customers.ToDictionary (c => c.ID, c => c.Name);

idToName.Dump();
Element Operators

First and Last

int[] numbers  = { 1, 2, 3, 4, 5 };

numbers.First().Dump ("First");
numbers.Last().Dump ("Last");

numbers.First (n => n % 2 == 0).Dump ("First even number");
numbers.Last (n => n % 2 == 0).Dump ("Last even number");

try 
{
  numbers.First (n => n > 10);
}
catch (Exception ex) 
{
  ex.Message.Dump ("The First number > 10");
}

FirstOrDefault

int[] numbers  = { 1, 2, 3, 4, 5 };

numbers.FirstOrDefault (n => n > 10)
  .Dump ("The FirstOrDefault number > 10");

Customers.FirstOrDefault (c => c.Name == "Harry")
  .Dump ("First customer called 'Harry'");
  
Customers.FirstOrDefault (c => c.Purchases.Any (p => p.Price > 1000))
  .Dump ("First customer with a purchase > $1000");
  
Customers.FirstOrDefault (c => c.Name == "Dylan")
  .Dump ("First customer called 'Dylan', or default (null) if no match");

Single

int[] numbers  = { 1, 2, 3, 4, 5 };

numbers.Single (n => n % 3 == 0).Dump ("The Single number divisible by 3");

try
{ 
  numbers.Single (n => n % 2 == 0);
}
catch (Exception ex)
{
  ex.Message.Dump ("The Single number divisible by 2");
}

SingleOrDefault

int[] numbers  = { 1, 2, 3, 4, 5 };

try
{
  numbers.Single (n => n > 10);
}
catch (Exception ex)
{
  ex.Message.Dump ("The Single number > 10");
}

numbers.SingleOrDefault (n => n > 10).Dump ("The SingleOrDefault number > 10");

try
{
  numbers.SingleOrDefault (n => n % 2 == 0);
}
catch (Exception ex)
{
  ex.Message.Dump ("The SingleOrDefault number divisible by two");
}

SingleOrDefault (database)

Customer cust = Customers.Single (c => c.ID == 3);

cust.Dump();

ElementAt

int[] numbers  = { 1, 2, 3, 4, 5 };

numbers.ElementAt (2).Dump ("ElementAt (2)");
try
{
  numbers.ElementAt (9); 
}
catch (Exception ex)
{
  ex.Message.Dump ("ElementAt (9)");
}

numbers.ElementAtOrDefault (9).Dump ("ElementAtOrDefault (9)");
Aggregation Methods

Count

// Note: refer to 'Grouping' for examples on using aggregations in groupby clauses.

new int[] { 5, 6, 7 }.Count()
  .Dump ("Simple Count");

"pa55w0rd".Count (c => char.IsDigit (c))
  .Dump ("Count with predicate");

Min and Max

int[] numbers = { 28, 32, 14 };

numbers.Min().Dump ("Min");
numbers.Max().Dump ("Max");

numbers.Max (n => n % 10).Dump ("Maximum remainder after dividing by ten");

Min and Max (database)

try
{
  Purchases.Min ();
}
catch (Exception ex)
{
  ex.Message.Dump ("Purchases.Min()");
}

Purchases.Min (p => p.Price).Dump ("Lowest price");

Purchases
  .Where (p => p.Price == Purchases.Min (p2 => p2.Price))
  .FirstOrDefault()
  .Dump ("The cheapest purchase");

Sum and Average

decimal[] numbers  = { 3, 4, 8 };

numbers.Sum()      .Dump ("Sum");
numbers.Average()  .Dump ("Average (mean)");

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
names.Sum (s => s.Length).Dump ("Combined string lengths");

Average Return Types

// The following generates a compile-time error:

int avg = new int[] { 3, 4 }.Average();

Average Upscaling

double avg1 = new int[] { 3, 4 }.Average(); 
double avg2 = new int[] { 3, 4 }.Average (n => (double) n);

avg1.Dump();
avg2.Dump();

Average (database)

from c in Customers
where c.Purchases.Average (p => p.Price) > 500
select c.Name

Aggregate

int[] numbers = { 1, 2, 3 };
numbers.Aggregate (0, (seed, n) => seed + n).Dump();
Quantifiers

Contains and Any

new int[] { 2, 3, 4 }.Contains (3)               .Dump ("Contains (3)");
new int[] { 2, 3, 4 }.Any (n => n == 3)          .Dump ("Any (n => n == 3)");
new int[] { 2, 3, 4 }.Any (n => n > 10)          .Dump ("Has a big number");
new int[] { 2, 3, 4 }.Where (n => n > 10).Any()  .Dump ("Has a big number");

All

Customers.Where (c => c.Purchases.All (p => p.Price < 100))

SequenceEqual

var query = "Hello".Distinct();

query.SequenceEqual ("Helo").Dump();
Generation Methods

Empty - Use Case

int[][] numbers = 
{
  new int[] { 1, 2, 3 },
  new int[] { 4, 5, 6 },
  null                     // this null makes the query below fail.
};

IEnumerable<int> flat = numbers.SelectMany (innerArray => innerArray);

flat.Dump();          // Throws a NullReferenceException

Empty - Solution

int[][] numbers = 
{
  new int[] { 1, 2, 3 },
  new int[] { 4, 5, 6 },
  null                     // this necessitates the null coalescing operator below
};

IEnumerable<int> flat = numbers
  .SelectMany (innerArray => innerArray ?? Enumerable.Empty <int>() );

flat.Dump();

Range and Repeat

Enumerable.Range (5, 5).Dump ("Range"); 
Enumerable.Repeat (true, 3).Dump ("Repeat");
C# 8.0 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via Safari subscription