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.
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().
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.
' --- 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.
' --- 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.
' ═══ 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.
' --- 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.
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
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.
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.
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.
Apply LINQ operators to a DataTable using .AsEnumerable() and .Field(Of T)(). Results can be converted back to a DataTable with .CopyToDataTable().
32.6 GitHub Copilot — LINQ Reporting
' 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, andSortfor in-place operations. Always prefer it over plain arrays for mutable data. - Dictionary(Of K,V) gives O(1) key lookup. Use
TryGetValueinstead 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
.KeyandIEnumerable(Of T)body. Chain aggregates (Count,Average,Max) inside aSelectto 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
GroupByon a computed grade band. - Bind the class summary (Key, Count, Average, PassRate) to a DataGridView. Add a
RowPrePaintto 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 aDictionary(Of Integer, Product)keyed on ProductID usingToDictionary. - 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.
Related Resources
← Lesson 31
Editing Data — CommandBuilder, Transactions.
Lesson 33 →
File I/O & JSON — StreamReader, JsonSerializer.
MS Docs — LINQ in VB
Official LINQ reference and tutorials.
MS Docs — List(Of T)
Full List(Of T) API reference.
Featured Books
Visual Basic 2022 Made Easy
Collections, LINQ fundamentals, and data-binding patterns with worked examples.
View on Amazon →