Lesson 32 · LINQ & Collections

LINQ & Collections

Query, filter, sort, and transform any data source — arrays, lists, dictionaries, and DataTables — using LINQ's expressive operators, all without leaving VB.NET.

Key Takeaway: LINQ (Language Integrated Query) lets you write strongly-typed queries directly in VB code using either query syntax (From … Where … Select) or method syntax (.Where(…).Select(…)). Both compile to the same IL. Use List(Of T) as your default in-memory collection, Dictionary(Of K,V) for O(1) key lookups, and LINQ's GroupBy to produce summaries. Results are lazy — the query doesn't run until you iterate it or call .ToList().
List(Of T)
Generic collection
Resizable array. Add, Remove, Contains, Sort. Default choice for ordered sets.
Dictionary(Of K,V)
Generic collection
Key→Value pairs. O(1) lookup with .Item(key) or .TryGetValue(). Keys must be unique.
.Where()
LINQ operator
Filter. Returns elements matching the predicate. Equivalent to SQL WHERE.
.Select()
LINQ operator
Project/transform each element. Can produce anonymous types: Select(Function(s) New With {s.Name, s.Grade}).
.OrderBy() / .ThenBy()
LINQ operator
Sort ascending. Use .OrderByDescending() for DESC. Chain .ThenBy() for secondary sort.
.GroupBy()
LINQ operator
Group elements by key. Each group has .Key and is IEnumerable(Of T). Like SQL GROUP BY.
.First() / .FirstOrDefault()
LINQ operator
First match. First() throws if none; FirstOrDefault() returns Nothing. Always prefer FirstOrDefault().
.ToList() / .ToArray()
LINQ operator
Materialise the lazy query into a concrete collection. Forces immediate execution.

32.1 List(Of T) — The Workhorse Collection

A List(Of T) is a resizable, typed array. It replaces the old ArrayList and should be your first choice whenever you need an ordered, modifiable set of objects. Because it is generic, the compiler enforces the element type — no accidental mixing of strings and integers.

List.vb — Visual Basic 2026
' --- Define a simple class ---
Public Class Student
    Public Property Name  As String
    Public Property Grade As Double
    Public Property Class As String
End Class

' --- Create and populate ---
Dim students As New List(Of Student) From {
    New Student With {.Name="Ali",   .Grade=88, .Class="4A"},
    New Student With {.Name="Alice", .Grade=92, .Class="4B"},
    New Student With {.Name="David", .Grade=55, .Class="4B"}
}

' --- Common methods ---
students.Add(New Student With {.Name="Farah", .Grade=95, .Class="5A"})
students.Remove(students(0))            ' remove by reference
students.RemoveAt(0)                    ' remove by index
students.RemoveAll(Function(s) s.Grade < 60)  ' remove matching
Dim count = students.Count              ' number of items
Dim found = students.Find(Function(s) s.Name = "Alice")  ' first match
Dim exists = students.Any(Function(s) s.Grade > 90)       ' LINQ: any match?

' --- Sort in place ---
students.Sort(Function(a, b) a.Grade.CompareTo(b.Grade))   ' ascending by grade

' --- Iterate ---
For Each s In students
    Console.WriteLine($"{s.Name}: {s.Grade}")
Next

32.2 Dictionary(Of K, V) — Fast Key Lookups

A Dictionary maps unique keys to values, with O(1) average lookup time regardless of size. Use it when you need to find an item by an identifier quickly — student ID to student object, category name to count, etc.

Dictionary.vb — Visual Basic 2026
' --- Create: Integer key → Student value ---
Dim index As New Dictionary(Of Integer, Student)()

' --- Populate from a List ---
For Each s In students
    index(s.StudentID) = s   ' add or overwrite
Next

' --- Or with LINQ ToDictionary ---
Dim byId = students.ToDictionary(Function(s) s.StudentID)

' --- Safe lookup (never throws) ---
Dim found As Student = Nothing
If index.TryGetValue(3, found) Then
    Console.WriteLine(found.Name)
Else
    Console.WriteLine("Student 3 not found")
End If

' --- Check existence ---
If index.ContainsKey(5) Then index.Remove(5)

' --- Iterate keys, values, or pairs ---
For Each pair In index
    Console.WriteLine($"ID {pair.Key}: {pair.Value.Name}")
Next

' --- Count-by-category pattern ---
Dim classCounts As New Dictionary(Of String, Integer)()
For Each s In students
    If classCounts.ContainsKey(s.Class) Then
        classCounts(s.Class) += 1
    Else
        classCounts(s.Class) = 1
    End If
Next
' Equivalent with LINQ GroupBy (see section 32.4)

32.3 LINQ — Query and Method Syntax

LINQ provides two equivalent styles. Query syntax reads like SQL and is easier to read for complex multi-clause queries. Method syntax uses lambda functions chained with dot notation and is more concise for simple operations. Both produce identical IL.

LINQ.vb — Visual Basic 2026
' ═══ QUERY SYNTAX ═══
Dim topStudents = From s In students
                  Where s.Grade >= 80
                  Order By s.Grade Descending
                  Select s.Name, s.Grade, s.Class

' ═══ METHOD SYNTAX (identical result) ═══
Dim topStudents2 = students _
    .Where(Function(s) s.Grade >= 80) _
    .OrderByDescending(Function(s) s.Grade) _
    .Select(Function(s) New With {s.Name, s.Grade, s.Class})

' --- Where: filter ---
Dim passers  = students.Where(Function(s) s.Grade >= 60).ToList()
Dim class4A  = students.Where(Function(s) s.Class = "4A").ToList()

' --- Select: project / transform ---
Dim names  = students.Select(Function(s) s.Name).ToList()          ' List(Of String)
Dim scaled = students.Select(Function(s) s.Grade * 1.1).ToList()   ' scaled grades

' --- Ordering ---
Dim byGrade = students.OrderByDescending(Function(s) s.Grade) _
                       .ThenBy(Function(s) s.Name).ToList()

' --- Aggregates ---
Dim avg  = students.Average(Function(s) s.Grade)          ' Double
Dim max  = students.Max(Function(s) s.Grade)              ' highest grade
Dim sum  = students.Sum(Function(s) s.Grade)              ' total
Dim cnt  = students.Count(Function(s) s.Grade >= 80)     ' conditional count

' --- Existence checks ---
Dim anyFail = students.Any(Function(s) s.Grade < 50)
Dim allPass = students.All(Function(s) s.Grade >= 60)

' --- First / Single ---
Dim top    = students.OrderByDescending(Function(s) s.Grade).First()
Dim ali    = students.FirstOrDefault(Function(s) s.Name = "Ali")
If ali IsNot Nothing Then Console.WriteLine(ali.Grade)

' --- Lazy vs eager ---
Dim query  = students.Where(Function(s) s.Grade > 70)  ' LAZY — not executed yet
Dim result = query.ToList()                                 ' EAGER — runs now

32.4 GroupBy — Summaries and Aggregates

GroupBy partitions a sequence into groups sharing a common key. Each group implements IEnumerable(Of T) so you can chain any LINQ operator on it. This is the VB equivalent of SQL GROUP BY … HAVING.

GroupBy.vb — Visual Basic 2026
' --- Group by class ---
Dim byClass = students.GroupBy(Function(s) s.Class)
For Each grp In byClass
    Console.WriteLine($"Class {grp.Key}: {grp.Count()} students, avg={grp.Average(Function(s) s.Grade):F1}")
Next

' --- Project each group into an anonymous type ---
Dim summary = students _
    .GroupBy(Function(s) s.Class) _
    .Select(Function(g) New With {
        .ClassName  = g.Key,
        .Count      = g.Count(),
        .Average    = Math.Round(g.Average(Function(s) s.Grade), 1),
        .Highest    = g.Max(Function(s) s.Grade),
        .Lowest     = g.Min(Function(s) s.Grade),
        .PassRate   = g.Count(Function(s) s.Grade >= 60) * 100 \ g.Count()
    }) _
    .OrderBy(Function(g) g.ClassName) _
    .ToList()

' Bind to DataGridView
dgvSummary.DataSource = summary  ' anonymous types work as DataSource!

' --- HAVING equivalent (filter groups) ---
Dim largeClasses = students _
    .GroupBy(Function(s) s.Class) _
    .Where(Function(g) g.Count() >= 5)  ' only classes with 5+ students
    .Select(Function(g) g.Key)
    .ToList()

32.5 LINQ on DataTable

DataTable rows are not directly LINQ-queryable, but calling .AsEnumerable() exposes them as IEnumerable(Of DataRow). Access cell values through the Field(Of T) extension method for type-safe, null-safe reading.

DataTableLINQ.vb — Visual Basic 2026
Imports System.Data   ' for DataTableExtensions

' --- Filter a DataTable with LINQ ---
Dim highGraders = _dt.AsEnumerable() _
    .Where(Function(r) r.Field(Of Double)("Grade") >= 80) _
    .OrderByDescending(Function(r) r.Field(Of Double)("Grade"))

' --- Convert result back to DataTable for DataGridView ---
Dim filtered = highGraders.CopyToDataTable()
dgv.DataSource = filtered

' --- Distinct values (e.g. populate a ComboBox) ---
Dim classes = _dt.AsEnumerable() _
    .Select(Function(r) r.Field(Of String)("Class")) _
    .Distinct() _
    .OrderBy(Function(c) c) _
    .ToList()
cboClass.DataSource = classes

' --- Aggregate directly on DataTable ---
Dim avgGrade = _dt.AsEnumerable() _
    .Average(Function(r) r.Field(Of Double)("Grade"))

' --- Group DataTable rows by Class ---
Dim grouped = _dt.AsEnumerable() _
    .GroupBy(Function(r) r.Field(Of String)("Class")) _
    .Select(Function(g) New With {
        .Class   = g.Key,
        .Count   = g.Count(),
        .Average = g.Average(Function(r) r.Field(Of Double)("Grade"))
    }).ToList()
dgvSummary.DataSource = grouped
Try It — Simulation 32.1: Live LINQ Query Builder

Build a LINQ query step by step — combine a Where filter, OrderBy, and aggregate. The code panel shows the exact VB expression as you configure the query.

LINQ Query Builder — Where + OrderBy + Select
Where Grade ≥:
And Class =
Order By:
Aggregate:

Try It — Simulation 32.2: GroupBy Visualiser

Choose a grouping key and which aggregate to compute per group. The summary table shows the result of .GroupBy().Select(…) — the same data you'd bind to a DataGridView.

GroupBy Visualiser — students.GroupBy(…).Select(…)
Group by:
Show per group:

Try It — Simulation 32.3: Dictionary vs List Lookup

Look up a student by ID using both a List linear scan and a Dictionary O(1) lookup. See the VB code for each approach and the relative "time" difference.

Dictionary(Of Integer, Student) vs List.Find()
Look up StudentID:
Method:

Try It — Simulation 32.4: LINQ on DataTable

Apply LINQ operators to a DataTable using .AsEnumerable() and .Field(Of T)(). Results can be converted back to a DataTable with .CopyToDataTable().

DataTable.AsEnumerable() — LINQ bridge
Operation:

32.6 GitHub Copilot — LINQ Reporting

GitHub Copilot — LINQ-powered report generation
You typed: ' Using the students List(Of Student), generate a report Sub that prints: top 3 students overall, per-class averages ranked highest first, the number of students failing (grade < 60), and the single student with the lowest grade.'
' Auto-generated LINQ report — Copilot
Private Sub PrintReport(students As List(Of Student))
    ' Top 3 students
    Dim top3 = students.OrderByDescending(Function(s) s.Grade).Take(3)
    Console.WriteLine("=== Top 3 Students ===")
    For Each s In top3
        Console.WriteLine($"  {s.Name,-20} {s.Grade:F1}")
    Next

    ' Per-class averages, highest first
    Dim classAvg = students _
        .GroupBy(Function(s) s.Class) _
        .Select(Function(g) New With {.Class=g.Key, .Avg=g.Average(Function(s) s.Grade)}) _
        .OrderByDescending(Function(g) g.Avg)
    Console.WriteLine("=== Class Averages ===")
    For Each c In classAvg
        Console.WriteLine($"  {c.Class}: {c.Avg:F1}")
    Next

    ' Failing students count
    Dim failCount = students.Count(Function(s) s.Grade < 60)
    Console.WriteLine($"Failing (< 60): {failCount}")

    ' Lowest-grade student
    Dim lowest = students.MinBy(Function(s) s.Grade)
    Console.WriteLine($"Lowest: {lowest.Name} — {lowest.Grade:F1}")
End Sub

Lesson Summary

  • List(Of T) is your default ordered collection. Use Add, RemoveAll, Find, and Sort for in-place operations. Always prefer it over plain arrays for mutable data.
  • Dictionary(Of K,V) gives O(1) key lookup. Use TryGetValue instead of the indexer to avoid KeyNotFoundException. Build one from a List using .ToDictionary(Function(x) x.Id).
  • LINQ query syntax (From … Where … Select) and method syntax (.Where().Select()) are interchangeable. Method syntax is more concise; query syntax is more readable for multi-join queries.
  • LINQ queries are lazy — they don't execute until iterated or .ToList()/.ToArray() is called. Call .ToList() to materialise and avoid re-executing a query each time you enumerate it.
  • GroupBy produces groups with a .Key and IEnumerable(Of T) body. Chain aggregates (Count, Average, Max) inside a Select to build summary tables bindable directly to DataGridView.
  • Query a DataTable with LINQ by calling .AsEnumerable() and reading typed cell values with .Field(Of T)("ColumnName"). Convert back with .CopyToDataTable().

Exercises

Exercise 32.1 — Student Analytics

  • Load 20 students from a DataTable. Use LINQ to find: top 5 by grade, bottom 5, class averages, and count of A/B/C/F grades using GroupBy on a computed grade band.
  • Bind the class summary (Key, Count, Average, PassRate) to a DataGridView. Add a RowPrePaint to colour rows where PassRate < 70% in salmon.
  • Copilot challenge: "Generate a Sub PrintClassReport that uses LINQ to produce a formatted string report, then displays it in a multi-line TextBox — one section per class, with header, student list, and averages"

Exercise 32.2 — Product Inventory with Dictionary

  • Load Products from SQLite into a List(Of Product). Build a Dictionary(Of Integer, Product) keyed on ProductID using ToDictionary.
  • TextBox lookup by ID uses TryGetValue. A second TextBox searches by name substring using .Where(Function(p) p.Name.Contains(txt)).
  • GroupBy Category: show count and total stock value (Price × Qty) per category, sorted by total value descending.

Next: Lesson 33 — File I/O & JSON

Read and write text, CSV, and JSON files using StreamReader, StreamWriter, and System.Text.Json's JsonSerializer.

Continue »

Related Resources


Featured Books

Visual Basic 2022 Made Easy

Visual Basic 2022 Made Easy

by Dr. Liew Voon Kiong

Collections, LINQ fundamentals, and data-binding patterns with worked examples.

View on Amazon →