Skip to content

5 - LINQ

Language-Integrated Query (LINQ) is the name for a set of technologies based on the integration of query capabilities directly into the C# language1. Traditionally, queries against data are expressed as simple strings without type checking at compile time or IntelliSense support. Furthermore, you have to learn a different query language for each type of data source: SQL databases, XML documents, various Web services, and so on. With LINQ, a query is a first-class language construct, just like classes, methods, and events.

When you write queries, the most visible "language-integrated" part of LINQ is the query expression. You write query expressions in a declarative query syntax. By using query syntax, you perform filtering, ordering, and grouping operations on data sources with a minimum of code. You use the same query expression patterns to query and transform data from any type of data source.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Specify the data source.
int[] scores = [97, 92, 81, 60];

// Define the query expression.
IEnumerable<int> scoreQuery =
    from score in scores
    where score > 80
    select score;

// Execute the query.
foreach (var i in scoreQuery)
{
    Console.Write(i + " ");
}

// Output: 97 92 81
  • Query expressions examine and transform data from any LINQ-enabled data source. For example, a single query can retrieve data from a SQL database and produce an XML stream as output.
  • Query expressions use many familiar C# language constructs, which make them easy to read.
  • The variables in a query expression are all strongly typed.
  • A query isn't executed until you iterate over the query variable, for example in a foreach statement.
  • At compile time, the compiler converts query expressions to standard query operator method calls according to the rules defined in the C# specification. You can express any query that uses query syntax by using method syntax. In some cases, query syntax is more readable and concise. In others, method syntax is more readable. There's no semantic or performance difference between the two different forms.
  • Some query operations, such as Count or Max, have no equivalent query expression clause and must be expressed as a method call. You can combine method syntax with query syntax in various ways.
  • Query expressions can be compiled to expression trees or to delegates, depending on the type that the query is applied to. The compiler compiles IEnumerable<T> queries to delegates. The compiler compiles IQueryable and IQueryable<T> queries to expression trees.

Immediate execution means that the data source is read and the operation is performed once. All the standard query operators that return a scalar result execute immediately. Examples of such queries are Count, Max, Average, and First. You can force any query to execute immediately using the ToList or ToArray methods.2

1
2
3
4
5
6
7
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

var evenNumQuery = from num in numbers
                   where (num % 2) == 0
                   select num;

int evenNumCount = evenNumQuery.Count();

To force immediate execution of any query and cache its results, you can call the ToList or ToArray methods.

1
2
3
4
5
6
7
List<int> numQuery2 = (from num in numbers
                       where (num % 2) == 0
                       select num).ToList();

var numQuery3 = (from num in numbers
                 where (num % 2) == 0
                 select num).ToArray();

Deferred execution means that the operation isn't performed at the point in the code where the query is declared. The operation is performed only when the query variable is enumerated, for example by using a foreach statement. The results of executing the query depend on the contents of the data source when the query is executed rather than when the query is defined. If the query variable is enumerated multiple times, the results might differ every time. Deferred execution provides the facility of query reuse since the query fetches the updated data from the data source each time query results are iterated.

1
2
3
4
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
string sentence = "the quick brown fox jumps over the lazy dog";
// Split the string into individual words to create a collection.
string[] words = sentence.Split(' ');

// Using query expression syntax.
var query = from word in words
            group word.ToUpper() by word.Length into gr
            orderby gr.Key
            select new { Length = gr.Key, Words = gr };

// Using method-based query syntax.
var query2 = words
    .GroupBy(w => w.Length, w => w.ToUpper())
    .Select(g => new { Length = g.Key, Words = g })
    .OrderBy(o => o.Length);

foreach (var obj in query)
{
    Console.WriteLine($"Words of length {obj.Length}:");
    foreach (string word in obj.Words)
        Console.WriteLine(word);
}

// This code example produces the following output:
//
// Words of length 3:
// THE
// FOX
// THE
// DOG
// Words of length 4:
// OVER
// LAZY
// Words of length 5:
// QUICK
// BROWN
// JUMPS

LINQ expressions that might be used in the following practical sessions:

Name Return type Immediate execution Deferred execution Notes
Any bool Determines whether any element of a sequence exists or satisfies a condition.
Average Single numeric value Computes the average of a sequence of numeric values.
Contains bool Determines whether a sequence contains a specified element.
Count int Returns the number of elements in a sequence.
Distinct IEnumerable Returns distinct elements from a sequence.
First TSource Returns the first element of a sequence.
FirstOrDefault TSource? Returns the first element of a sequence, or a default value if no element is found.
GroupBy IEnumerable Groups the elements of a sequence.
Join IEnumerable Correlates the elements of two sequences based on matching keys.
Last TSource Returns the last element of a sequence.
LastOrDefault TSource? Returns the last element of a sequence, or a default value if no element is found.
Max Single numeric value Returns the maximum value in a sequence of values.
Min Single numeric value Returns the minimum value in a sequence of values.
OrderBy IOrderedEnumerable Sorts the elements of a sequence in ascending order.
OrderByDescending IOrderedEnumerable Sorts the elements of a sequence in descending order.
Select IEnumerable Projects each element of a sequence into a new form.
Single TSource Returns a single, specific element of a sequence.
SingleOrDefault TSource? Returns a single, specific element of a sequence, or a default value if that element is not found.
Sum Single numeric value Computes the sum of a sequence of numeric values.
Take IEnumerable Returns a specified number of contiguous elements from the start of a sequence.
ThenBy IOrderedEnumerable Performs a subsequent ordering of the elements in a sequence in ascending order.
ThenByDescending IOrderedEnumerable Performs a subsequent ordering of the elements in a sequence in descending order.
ToList IList Creates a List from an IEnumerable.
Where IEnumerable Filters a sequence of values based on a predicate.

Tasks

Based on the latest project snapshot (apiary-practice-4.zip), the following functionalities should be implemented:

  1. Create a new model class called Order with the following properties:
    • Order Date
    • Status (New, Approved, Completed)
  2. Add the Order class to the ApiaryContext and create a new migration
  3. Update the database
  4. Create a new Form for creating Orders
  5. The order date should be changeable via a DateTimePicker control
  6. The order status should be set to New during creation
  7. Create a new (or reuse the existing) listing Form where the orders can be listed and ordered by their OrderDate property.

References