LINQ and Query Expressions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example is a complete
console program that starts with Option Strict On, was built with warnings treated as errors (except where the text says a warning
is the subject) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The examples that are meant to
fail say so and show the compiler messages that the SDK printed, with the file path removed. The project-level imports that the SDK
adds (System, System.Collections.Generic, System.Linq and others) are why List(Of T) and the query operators need no Imports
line; System.Data and System.Linq.Expressions are imported where they are used. To keep every program self-contained, the
examples declare small Book, Author and Loan classes with the members they need, instead of referencing the full shelf
domain model; the names and the data follow the shelf library catalogue.
Language Integrated Query (LINQ) puts a declarative query language into the language itself: the same From ... Where ... Select
text works over arrays, lists, XML trees, DataTable rows and, through other providers, databases. A query expression is not a
separate engine. The compiler translates every clause into a call of a query operator method (Where, Select, OrderBy and so on)
that is looked up by name on the source, and the arguments are lambda expressions built from the clause text. What that call returns,
and when the work happens, is decided by the method that is found, which is the reason the same syntax can be a lazy in-memory
pipeline over IEnumerable(Of T) or a SQL statement over IQueryable(Of T).
Visual Basic had LINQ from the start. It arrived with Visual Basic 9 and Visual Studio 2008, together with local type inference,
anonymous types, extension methods, lambda expressions and XML literals (see
XML Literals and Axis Properties). Its query syntax is
richer than C#'s: Aggregate, Distinct, Skip, Take, Skip While, Take While and Group By ... Into are query clauses in
Visual Basic, and they have no query-syntax counterpart in C#, where they exist only as method calls. The method-based operators of
System.Linq are shared and are the same in both languages; the C# reference covers them in
LINQ in C#.
The query clauses at a glance
| Clause | What it does | Translated into |
|---|---|---|
|
starts a query and declares the range variable |
the source itself, or |
|
keeps the elements for which the |
|
|
chooses what the query returns; may name several members, which produces an anonymous type |
|
|
sorts; each key takes |
|
|
inner join on equal keys |
|
|
joins and keeps every outer element with its (possibly empty) group of matches |
|
|
groups by key and computes aggregates per group |
|
|
introduces a new range variable computed once per element |
|
|
removes duplicate results |
|
|
drops or keeps a fixed number of elements |
|
|
drops or keeps elements while the condition holds |
|
|
computes one or more aggregates and returns the value (or a record of values) immediately |
|
Rules shared by all clauses:
-
A query has exactly one first clause, which is
FromorAggregate. Every later clause works on the range variables that are still in scope. -
Clauses may be repeated and, unlike in C#, mostly reordered:
Selectcan come beforeWhere, and a query can contain severalSelect,WhereandOrder Byclauses. ASelectis optional; without one the query returns the range variable itself. -
The query is one logical line. Line breaks are allowed before each clause keyword, and the example listings use that to put one clause per line (see Program Structure and Code Conventions).
-
A query cannot contain statements, only expressions. Any
ByRefparameter used in a query is rejected (BC36533, shown below).
The clause pipeline
Reading a query top to bottom shows how data flows: each clause receives the sequence that the previous one produced.
Group By ... Into is the one clause that changes what the range variables are, so the diagram splits there.
The sections below follow the pipeline. The first example is a complete query next to the same query in method syntax; the two
are the same program for the compiler, which SequenceEqual confirms (the Key keywords the method form needs are explained
in the section on anonymous types).
Option Strict On
Public Class Book
Public Property Title As String
Public Property Author As String
Public Property Year As Integer
Public Property Pages As Integer
Public Property Genre As String
Public Sub New(title As String, author As String, year As Integer, pages As Integer, genre As String)
Me.Title = title
Me.Author = author
Me.Year = year
Me.Pages = pages
Me.Genre = genre
End Sub
End Class
Module Program
Function Library() As List(Of Book)
Return New List(Of Book) From {
New Book("Dune", "Frank Herbert", 1965, 412, "Science fiction"),
New Book("Emma", "Jane Austen", 1815, 474, "Novel"),
New Book("Neuromancer", "William Gibson", 1984, 271, "Science fiction"),
New Book("Persuasion", "Jane Austen", 1817, 249, "Novel"),
New Book("Hyperion", "Dan Simmons", 1989, 482, "Science fiction"),
New Book("Ulysses", "James Joyce", 1922, 730, "Novel")
}
End Function
Sub Main()
Dim books = Library()
' Query syntax: From, Where, Order By, Select
Dim recent = From b In books
Where b.Year > 1900 AndAlso b.Genre = "Science fiction"
Order By b.Year Descending
Select b.Title, b.Year
For Each r In recent
Console.WriteLine(r.Title & " (" & r.Year & ")")
Next
' The same query in method syntax
Dim sameQuery = books.
Where(Function(b) b.Year > 1900 AndAlso b.Genre = "Science fiction").
OrderByDescending(Function(b) b.Year).
Select(Function(b) New With {Key b.Title, Key b.Year})
Console.WriteLine(recent.SequenceEqual(sameQuery))
End Sub
End Module
Hyperion (1989)
Neuromancer (1984)
Dune (1965)
True
Range variables, type inference and the type of a query
From b In books is neither an assignment nor a loop. It declares b as the range variable of the query: it stands for one
element at a time and is typed from the element type of books (here Book). The source can be anything that has the query
operator methods in scope, which in practice means IEnumerable(Of T) (through the extension methods of System.Linq.Enumerable),
IQueryable(Of T) (through System.Linq.Queryable) and any type that supplies its own methods. A type that is not queryable
produces BC36593, as shown below.
The query result is an unexecuted query object. Its static type is rarely worth writing out: with Select b.Title, b.Year it
is an IEnumerable of a compiler-generated anonymous type that has no name, so the declaration must rely on local type
inference. That is the reason Option Infer On (the SDK default; see
Option Statements) is practically a requirement for query code. With
Option Infer Off and Option Strict On the same declaration is rejected:
Option Strict On
Option Infer Off
Module Program
Sub Main()
Dim numbers As New List(Of Integer)
Dim q = From n In numbers Select n
End Sub
End Module
Program.vb(7,13): error BC30209: Option Strict On requires all variable declarations to have an 'As' clause.
Source: Option Infer Statement
A query whose Select returns an existing type has a nameable type, IEnumerable(Of T), and may be assigned to a variable
declared with it; with a Select of several members it cannot be.
Filtering, ordering, projecting and trimming
Where takes any Boolean expression, and AndAlso and OrElse short-circuit inside it as everywhere else. Order By takes a
comma-separated list of keys, each optionally followed by Ascending (the default) or Descending; the sort is stable, so equal
keys keep their source order. Select can be followed by the trimming clauses Distinct, Skip, Take, Skip While and Take While. The next
program shows each clause on one small data set; every For Each or String.Join runs the query at that moment.
Option Strict On
Public Class Book
Public Property Title As String
Public Property Author As String
Public Property Year As Integer
Public Sub New(title As String, author As String, year As Integer)
Me.Title = title
Me.Author = author
Me.Year = year
End Sub
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("Emma", "Jane Austen", 1815),
New Book("Persuasion", "Jane Austen", 1817),
New Book("Dune", "Frank Herbert", 1965),
New Book("Ulysses", "James Joyce", 1922),
New Book("Dubliners", "James Joyce", 1914),
New Book("Neuromancer", "William Gibson", 1984)
}
Console.WriteLine("-- Order By author, then year descending")
For Each b In From x In books Order By x.Author, x.Year Descending Select x
Console.WriteLine(b.Author & " / " & b.Year & " / " & b.Title)
Next
Console.WriteLine("-- Distinct authors")
Console.WriteLine(String.Join("; ", From b In books Select b.Author Distinct))
Console.WriteLine("-- Skip 1, Take 3 over Order By Year")
Console.WriteLine(String.Join("; ", From b In books Order By b.Year Select b.Title Skip 1 Take 3))
Console.WriteLine("-- Skip While year < 1960 (in source order)")
Console.WriteLine(String.Join("; ", From b In books Skip While b.Year < 1960 Select b.Title))
Console.WriteLine("-- Take While year < 1960 (stops at the first failure)")
Console.WriteLine(String.Join("; ", From b In books Take While b.Year < 1960 Select b.Title))
Console.WriteLine("-- Where after Select, with the projected name")
Console.WriteLine(String.Join("; ", From b In books Select b.Title, b.Year Where Year > 1950 Select Title))
End Sub
End Module
-- Order By author, then year descending
Frank Herbert / 1965 / Dune
James Joyce / 1922 / Ulysses
James Joyce / 1914 / Dubliners
Jane Austen / 1817 / Persuasion
Jane Austen / 1815 / Emma
William Gibson / 1984 / Neuromancer
-- Distinct authors
Jane Austen; Frank Herbert; James Joyce; William Gibson
-- Skip 1, Take 3 over Order By Year
Persuasion; Dubliners; Ulysses
-- Skip While year < 1960 (in source order)
Dune; Ulysses; Dubliners; Neuromancer
-- Take While year < 1960 (stops at the first failure)
Emma; Persuasion
-- Where after Select, with the projected name
Dune; Neuromancer
Source: Order By Clause, Distinct Clause, Skip While Clause
Points worth remembering from that program:
-
Skip WhileandTake Whiletest the elements in source order and stop testing at the first element that decides the matter. WithSkip While b.Year < 1960the prefix Emma and Persuasion is dropped, and everything after Dune is kept, even Ulysses (1922) and Dubliners (1914), which are older than 1960.Take While b.Year < 1960is the mirror image: it ends the result at Dune and never examines the older books after it. Neither is a filter; useWherefor that. -
SkipandTakeare clauses, not methods, and apply to the query as written so far. Written afterOrder By, they page the sorted sequence; written before it, they would cut the unsorted sequence. -
Distinctcompares the projected elements. With a single member it compares values; with several members it compares the anonymous-type instances, whose equality depends on theKeyproperties (explained below). -
A
Wheremay follow aSelect. The projected names (Title,Year) are then the range variables for the later clauses and the originalbis no longer visible.
Deferred and immediate execution
Defining a query does not run it. The query object holds the source reference and the lambdas; the work happens when something
asks for the elements: a For Each, String.Join, ToList(), ToArray(), Count(), First(), Aggregate and every other
operator that returns a value rather than a sequence. Two consequences follow, and the program shows both. First, a query
that is enumerated twice does its work twice. Second, a query sees the source and the captured variables as they are when it runs,
not as they were when it was written.
Option Strict On
Module Program
Sub Main()
Dim numbers As New List(Of Integer) From {1, 2, 3}
' Deferred: nothing runs here
Dim evens = From n In numbers
Where n Mod 2 = 0
Select n
' Immediate: ToList, Count(), Sum() and friends run the query now
Dim snapshot = evens.ToList()
Dim total = (From n In numbers Select n).Sum()
numbers.Add(4)
numbers.Add(6)
Console.WriteLine("snapshot: " & String.Join(",", snapshot))
Console.WriteLine("evens : " & String.Join(",", evens))
Console.WriteLine("total : " & total)
' A query sees the variables it captures when it runs, not when it is written
Dim limit As Integer = 3
Dim small = From n In numbers Where n <= limit Select n
limit = 4
Console.WriteLine("limit 4 : " & String.Join(",", small))
' Tracing the pipeline: each element flows through all stages before the next one starts
Dim traced = From n In numbers.Where(Function(x)
Console.WriteLine(" where " & x)
Return x > 2
End Function)
Select Doubled = Doubler(n)
Console.WriteLine("query defined, nothing traced yet")
For Each d In traced
Console.WriteLine("got " & d)
Next
End Sub
Function Doubler(n As Integer) As Integer
Console.WriteLine(" select " & n)
Return n * 2
End Function
End Module
snapshot: 2
evens : 2,4,6
total : 6
limit 4 : 1,2,3,4
query defined, nothing traced yet
where 1
where 2
where 3
select 3
got 6
where 4
select 4
got 8
where 6
select 6
got 12
The trace at the end of the output also shows how the pipeline works: the operators are stacked iterators, so each element travels
through Where and Select before the next element is touched. Nothing is buffered between them, except by the operators that must
see everything before they can emit anything: Order By, Group By, the inner side of Join and Group Join, and Reverse. That is why a pipeline of Where and Select can start returning results immediately, while the first result of an
Order By is available only after the whole source has been read.
| Execution style | Operators and keywords |
|---|---|
Deferred, streaming |
|
Deferred, buffering |
|
Immediate (returns a value) |
|
Immediate (materialises a collection) |
|
The classic pitfalls of deferral are the next program: running a query several times, modifying the source while it is being
enumerated, and calling an operator that needs an element on an empty sequence. Sum of nothing is 0, but Max, Min and
Average of a sequence of non-nullable values have no answer and throw InvalidOperationException; use FirstOrDefault,
DefaultIfEmpty or a nullable projection when emptiness is possible.
Option Strict On
Module Program
Sub Main()
Dim names As New List(Of String) From {"emma", "dune"}
' 1. Multiple enumeration runs the query again
Dim runs As Integer = 0
Dim q = From n In names
Let counted = Tick(runs)
Select n.ToUpper()
Console.WriteLine(q.Count() & " " & q.Count() & " runs=" & runs)
' 2. Changing the source while a query is being enumerated
Try
For Each n In From x In names Select x
names.Add(n & "2")
Next
Catch ex As InvalidOperationException
Console.WriteLine("modify while iterating: " & ex.Message)
End Try
' 3. First/Single/Max on an empty sequence
Dim empty = From n In New List(Of Integer) Select n
Try
Console.WriteLine(empty.First())
Catch ex As InvalidOperationException
Console.WriteLine("First : " & ex.Message)
End Try
Try
Console.WriteLine(Aggregate n In empty Into Max())
Catch ex As InvalidOperationException
Console.WriteLine("Max : " & ex.Message)
End Try
Console.WriteLine("Sum of nothing: " & Aggregate n In empty Into Sum())
Try
Console.WriteLine(Aggregate n In empty Into Average())
Catch ex As InvalidOperationException
Console.WriteLine("Average: " & ex.Message)
End Try
End Sub
Function Tick(ByRef runs As Integer) As Integer
runs += 1
Return runs
End Function
End Module
2 2 runs=4
modify while iterating: Collection was modified; enumeration operation may not execute.
First : Sequence contains no elements
Max : Sequence contains no elements
Sum of nothing: 0
Average: Sequence contains no elements
Source: Aggregate Clause
Capturing the loop variable
A query captures variables, not values. A For loop control variable is a single variable that changes during the loop, so a
query created in the loop and enumerated after it sees the final value. The compiler warns about it (BC42327); under
TreatWarningsAsErrors the program does not build, so the next listing is built with that one setting turned off in the project to
show the warning and the effect. Copy the loop variable to a local declared inside the loop (the second list) to get one
variable per iteration.
Option Strict On
Module Program
Sub Main()
' Lambdas created in a loop capture the variable, not its value at that moment
Dim queries As New List(Of IEnumerable(Of Integer))
Dim numbers = {1, 2, 3, 4, 5, 6}
For divisor As Integer = 2 To 3
queries.Add(From n In numbers Where n Mod divisor = 0 Select n)
Next
' The loop variable of For ... Next is shared by every iteration (BC42327 is a warning)
For Each q In queries
Console.WriteLine(String.Join(",", q))
Next
Dim fixedQueries As New List(Of IEnumerable(Of Integer))
For d As Integer = 2 To 3
Dim divisor As Integer = d
fixedQueries.Add(From n In numbers Where n Mod divisor = 0 Select n)
Next
For Each q In fixedQueries
Console.WriteLine(String.Join(",", q))
Next
End Sub
End Module
Program.vb(10,55): warning BC42327: Using the iteration variable in a query expression may have unexpected results. Instead, create a local variable within the loop and assign it the value of the iteration variable.
4
4
2,4,6
3,6
Joins
Join is an inner join. Both sides of the Equals keyword must each mention range variables from only one side of the join (the
left key uses the variables defined before the Join, the right key uses only the new one), and the keyword is Equals, not =.
Several key pairs are joined with And. A Group Join keeps every outer element together with its group of matches, which may be
empty, and is also how a left outer join is written: flatten the group with DefaultIfEmpty() in a second From.
Option Strict On
Public Class Author
Public Property Id As Integer
Public Property Name As String
Public Sub New(id As Integer, name As String)
Me.Id = id
Me.Name = name
End Sub
End Class
Public Class Book
Public Property Title As String
Public Property AuthorId As Integer
Public Property Year As Integer
Public Property Pages As Integer
Public Sub New(title As String, authorId As Integer, year As Integer, pages As Integer)
Me.Title = title
Me.AuthorId = authorId
Me.Year = year
Me.Pages = pages
End Sub
End Class
Public Class Review
Public Property Title As String
Public Property Year As Integer
Public Property Stars As Integer
Public Sub New(title As String, year As Integer, stars As Integer)
Me.Title = title
Me.Year = year
Me.Stars = stars
End Sub
End Class
Module Program
Sub Main()
Dim authors As New List(Of Author) From {
New Author(1, "Jane Austen"), New Author(2, "Frank Herbert"), New Author(3, "Anonymous")
}
Dim books As New List(Of Book) From {
New Book("Emma", 1, 1815, 474), New Book("Persuasion", 1, 1817, 249),
New Book("Dune", 2, 1965, 412), New Book("Orphan", 99, 2001, 100)
}
Console.WriteLine("-- Inner join: only matching pairs")
Dim pairs = From b In books
Join a In authors On b.AuthorId Equals a.Id
Select b.Title, AuthorName = a.Name
For Each p In pairs
Console.WriteLine(p.Title & " <- " & p.AuthorName)
Next
Console.WriteLine("-- Group Join: every author, with a possibly empty group of books")
Dim shelves = From a In authors
Group Join b In books On a.Id Equals b.AuthorId Into Titles = Group
Select a.Name, Count = Titles.Count(), Titles
For Each s In shelves
Console.WriteLine(s.Name & ": " & s.Count & " [" & String.Join(", ", From t In s.Titles Select t.Title) & "]")
Next
Console.WriteLine("-- Left outer join = Group Join + Select Many with DefaultIfEmpty")
Dim leftJoin = From a In authors
Group Join b In books On a.Id Equals b.AuthorId Into g = Group
From x In g.DefaultIfEmpty()
Select a.Name, Title = If(x Is Nothing, "(none)", x.Title)
For Each l In leftJoin
Console.WriteLine(l.Name & " / " & l.Title)
Next
Console.WriteLine("-- Composite key: the pairs on each side of And are compared with Equals")
Dim reviews As New List(Of Review) From {
New Review("Emma", 1815, 5), New Review("Emma", 1816, 1), New Review("Dune", 1965, 4)
}
Dim matched = From b In books
Join r In reviews On b.Title Equals r.Title And b.Year Equals r.Year
Select b.Title, r.Stars
For Each m In matched
Console.WriteLine(m.Title & " " & m.Stars)
Next
Console.WriteLine("-- Join with a third source (chained Join)")
Dim chain = From b In books
Join a In authors On b.AuthorId Equals a.Id
Join a2 In authors On a.Id Equals a2.Id
Select b.Title, a2.Name
Console.WriteLine(chain.Count())
End Sub
End Module
-- Inner join: only matching pairs
Emma <- Jane Austen
Persuasion <- Jane Austen
Dune <- Frank Herbert
-- Group Join: every author, with a possibly empty group of books
Jane Austen: 2 [Emma, Persuasion]
Frank Herbert: 1 [Dune]
Anonymous: 0 []
-- Left outer join = Group Join + Select Many with DefaultIfEmpty
Jane Austen / Emma
Jane Austen / Persuasion
Frank Herbert / Dune
Anonymous / (none)
-- Composite key: the pairs on each side of And are compared with Equals
Emma 5
Dune 4
-- Join with a third source (chained Join)
3
Source: Join Clause, Group Join Clause
Two behaviours matter in practice. The join keys are compared with the default equality of the key type: strings are compared
case-sensitively whatever Option Compare says (see the section on comparison below). And a Join builds a lookup of the
inner source before it starts, so the inner source is read completely, while the outer source streams. Put the smaller sequence
on the inner side.
Enumerable.LeftJoin and Enumerable.RightJoin, so the Group Join plus DefaultIfEmpty() idiom is no longer theonly way to write an outer join, but they exist only as method calls; there is no Left Join clause. The call below runs on the .NET
10 SDK. The elements are anonymous types declared in array initializers.
Option Strict On
Module Program
Sub Main()
' Enumerable.LeftJoin and RightJoin were added in .NET 10
Dim authors = {New With {Key .Id = 1, Key .Name = "Austen"}, New With {Key .Id = 2, Key .Name = "Anonymous"}}
Dim titles = {New With {Key .AuthorId = 1, Key .Title = "Emma"}}
Dim result = authors.LeftJoin(titles,
Function(a) a.Id,
Function(t) t.AuthorId,
Function(a, t) a.Name & ": " & If(t Is Nothing, "(none)", t.Title))
For Each r In result
Console.WriteLine(r)
Next
End Sub
End Module
Austen: Emma
Anonymous: (none)
Source: Enumerable.LeftJoin
Grouping and aggregating
Group By partitions the sequence by a key. The Into list that follows is required: it names what each group contributes to the
result. It may contain the aggregate functions (Count(), Sum(expr), Min, Max, Average, Any, All, and any extension method
that takes the group) and the group itself under any name, by default Group. After the clause the original range variables are no
longer in scope. Only the key (named, or inferred from the expression) and the Into names are, which is why Order By Genre after
the grouping works and Order By b.Year does not.
Group By with an element selector (Group b.Title By ...) makes Group a sequence of titles instead of books. The key can be an
expression (b.Year \ 100 + 1, the century) or a composite built with an anonymous type; for a composite key the members that
must take part in the comparison are marked Key.
Aggregate is the one clause that can start a query. It runs at once and returns either the value of its single aggregate, or a
record whose members are named after the functions when there are several. It also works as a clause inside a larger query,
introducing a range variable that holds the aggregate for each element (with Let here).
Option Strict On
Public Class Book
Public Property Title As String
Public Property Genre As String
Public Property Year As Integer
Public Property Pages As Integer
Public Sub New(title As String, genre As String, year As Integer, pages As Integer)
Me.Title = title
Me.Genre = genre
Me.Year = year
Me.Pages = pages
End Sub
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("Dune", "Science fiction", 1965, 412),
New Book("Neuromancer", "Science fiction", 1984, 271),
New Book("Hyperion", "Science fiction", 1989, 482),
New Book("Emma", "Novel", 1815, 474),
New Book("Ulysses", "Novel", 1922, 730),
New Book("Walden", "Essay", 1854, 352)
}
Console.WriteLine("-- Group By ... Into: Key, Group and aggregate functions")
Dim byGenre = From b In books
Group By b.Genre Into Count(), TotalPages = Sum(b.Pages), Newest = Max(b.Year), Titles = Group
Order By Genre
For Each g In byGenre
Console.WriteLine(g.Genre & ": " & g.Count & " book(s), " & g.TotalPages & " pages, newest " & g.Newest &
", first = " & g.Titles.First().Title)
Next
Console.WriteLine("-- Group By with an explicit key name and element selector")
Dim byCentury = From b In books
Group b.Title By Century = b.Year \ 100 + 1 Into Group
Order By Century
Select Century, Titles = String.Join(", ", Group)
For Each c In byCentury
Console.WriteLine("century " & c.Century & ": " & c.Titles)
Next
Console.WriteLine("-- Group By a composite key")
Dim grouped = From b In books
Group By Key = New With {Key b.Genre, Key .Old = b.Year < 1950} Into Group
Order By Key.Genre
For Each g In grouped
Console.WriteLine(g.Key.Genre & " old=" & g.Key.Old & " -> " & g.Group.Count())
Next
Console.WriteLine("-- Aggregate ... Into at the start of a query returns one value")
Dim stats = Aggregate b In books
Where b.Genre = "Science fiction"
Into Count(), Sum(b.Pages), Average(b.Pages), Min(b.Year), Max(b.Year), Any(), All(b.Pages > 200)
Console.WriteLine(stats.Count & " " & stats.Sum & " " & stats.Average.ToString("F1") & " " & stats.Min & " " & stats.Max &
" " & stats.Any & " " & stats.All)
Console.WriteLine("-- Aggregate as a clause inside a query")
Dim perGenre = From g In (From b In books Select b.Genre Distinct)
Let pages = Aggregate b In books Where b.Genre = g Into Sum(b.Pages)
Select g, pages
For Each p In perGenre
Console.WriteLine(p.g & " = " & p.pages)
Next
End Sub
End Module
-- Group By ... Into: Key, Group and aggregate functions
Essay: 1 book(s), 352 pages, newest 1854, first = Walden
Novel: 2 book(s), 1204 pages, newest 1922, first = Emma
Science fiction: 3 book(s), 1165 pages, newest 1989, first = Dune
-- Group By with an explicit key name and element selector
century 19: Emma, Walden
century 20: Dune, Neuromancer, Hyperion, Ulysses
-- Group By a composite key
Essay old=True -> 1
Novel old=True -> 2
Science fiction old=False -> 3
-- Aggregate ... Into at the start of a query returns one value
3 1165 388.3 1965 1989 True True
-- Aggregate as a clause inside a query
Science fiction = 1165
Novel = 1204
Essay = 352
Source: Group By Clause, Aggregate Clause
Notes on the results above:
-
The group order of
Group Byfollows the first appearance of each key in the source, which is why the query sorts byGenreafterwards. -
The
AverageofIntegervalues is aDouble.SumofIntegervalues is computed with checked arithmetic and throwsOverflowExceptionon overflow whatever the project’s integer-overflow setting is (the last program on this page shows it); sum aLongprojection (Sum(Function(x) CLng(x))) when the total can be large. The-removeintchecksoption of the Visual Basic compiler affects only the+operator and its relatives (see Type Conversions). -
AggregatewithAny()andAll(...)is allowed in theIntolist; they returnBoolean. -
Grouping by a composite key needs
Keyon the members of the anonymous type (see the section on anonymous types), or every element ends up in its own group because anonymous instances withoutKeycompare by reference.
Let and several From clauses
Let computes a value once per element and gives it a name that later clauses can use, which both avoids repeated work and keeps
queries readable. A second From clause iterates a second sequence for each element of the first: if the second sequence depends
on the first range variable it flattens nested collections (this is SelectMany), and if it does not it is a cross join. Range
variables that are declared by a clause stay in scope until a Select, Group By or Aggregate … Into ends their scope, and
a name cannot be declared twice (BC30978).
Option Strict On
Module Program
Sub Main()
Dim sentences As String() = {"the quick brown fox", "jumps over", "the lazy dog"}
Console.WriteLine("-- Let introduces a range variable computed once per element")
Dim q1 = From s In sentences
Let words = s.Split(" "c)
Let count = words.Length
Where count > 2
Select s, count, First = words(0)
For Each r In q1
Console.WriteLine(r.s & " | " & r.count & " | " & r.First)
Next
Console.WriteLine("-- Several From clauses: Select Many (flatten)")
Dim q2 = From s In sentences
From w In s.Split(" "c)
Where w.Length > 3
Select w
Console.WriteLine(String.Join(",", q2))
Console.WriteLine("-- Several From clauses without a relation: cross join")
Dim q3 = From x In {1, 2, 3}
From y In {"a", "b"}
Select x & y
Console.WriteLine(String.Join(",", q3))
Console.WriteLine("-- Range variable scope: only the last Select/Group By/Into survives")
Dim q4 = From s In sentences
Select Upper = s.ToUpper()
Where Upper.StartsWith("THE")
Select Upper.Length
Console.WriteLine(String.Join(",", q4))
Console.WriteLine("-- Take/Skip as query operators and the Distinct keyword on an anonymous type")
Dim q5 = From w In (From s In sentences From x In s.Split(" "c) Select x)
Select w.Length, First = w(0) Distinct
Order By Length
For Each r In q5
Console.WriteLine(r.Length & r.First)
Next
End Sub
End Module
-- Let introduces a range variable computed once per element
the quick brown fox | 4 | the
the lazy dog | 3 | the
-- Several From clauses: Select Many (flatten)
quick,brown,jumps,over,lazy
-- Several From clauses without a relation: cross join
1a,1b,2a,2b,3a,3b
-- Range variable scope: only the last Select/Group By/Into survives
19,12
-- Take/Skip as query operators and the Distinct keyword on an anonymous type
3t
3f
3d
4o
4l
5q
5b
5j
Source: Let Clause, From Clause
How the compiler translates a query
The translation follows the query expression pattern. For each clause the compiler binds a method by name to the current
source, with ordinary overload resolution, so any type that has methods called Where, Select and the others participates, whether
they are instance methods or extension methods. The next program is a type that only records which methods it is asked to run. It
shows the exact translation of eleven queries.
Option Strict On
Module Recorder
Public ReadOnly Calls As New List(Of String)
End Module
' A type that implements the query pattern: it only records which methods the compiler calls
Public Class Spy(Of T)
Public Function [Where](predicate As Func(Of T, Boolean)) As Spy(Of T)
Recorder.Calls.Add("Where")
Return Me
End Function
Public Function [Select](Of TResult)(selector As Func(Of T, TResult)) As Spy(Of TResult)
Recorder.Calls.Add("Select")
Return New Spy(Of TResult)
End Function
Public Function OrderBy(Of TKey)(keySelector As Func(Of T, TKey)) As Spy(Of T)
Recorder.Calls.Add("OrderBy")
Return Me
End Function
Public Function OrderByDescending(Of TKey)(keySelector As Func(Of T, TKey)) As Spy(Of T)
Recorder.Calls.Add("OrderByDescending")
Return Me
End Function
Public Function ThenBy(Of TKey)(keySelector As Func(Of T, TKey)) As Spy(Of T)
Recorder.Calls.Add("ThenBy")
Return Me
End Function
Public Function ThenByDescending(Of TKey)(keySelector As Func(Of T, TKey)) As Spy(Of T)
Recorder.Calls.Add("ThenByDescending")
Return Me
End Function
Public Function Skip(count As Integer) As Spy(Of T)
Recorder.Calls.Add("Skip")
Return Me
End Function
Public Function Take(count As Integer) As Spy(Of T)
Recorder.Calls.Add("Take")
Return Me
End Function
Public Function SkipWhile(predicate As Func(Of T, Boolean)) As Spy(Of T)
Recorder.Calls.Add("SkipWhile")
Return Me
End Function
Public Function TakeWhile(predicate As Func(Of T, Boolean)) As Spy(Of T)
Recorder.Calls.Add("TakeWhile")
Return Me
End Function
Public Function Distinct() As Spy(Of T)
Recorder.Calls.Add("Distinct")
Return Me
End Function
Public Function SelectMany(Of TCollection, TResult)(collectionSelector As Func(Of T, Spy(Of TCollection)),
resultSelector As Func(Of T, TCollection, TResult)) As Spy(Of TResult)
Recorder.Calls.Add("SelectMany")
Return New Spy(Of TResult)
End Function
Public Function Join(Of TInner, TKey, TResult)(inner As Spy(Of TInner), outerKeySelector As Func(Of T, TKey),
innerKeySelector As Func(Of TInner, TKey),
resultSelector As Func(Of T, TInner, TResult)) As Spy(Of TResult)
Recorder.Calls.Add("Join")
Return New Spy(Of TResult)
End Function
Public Function GroupJoin(Of TInner, TKey, TResult)(inner As Spy(Of TInner), outerKeySelector As Func(Of T, TKey),
innerKeySelector As Func(Of TInner, TKey),
resultSelector As Func(Of T, Spy(Of TInner), TResult)) As Spy(Of TResult)
Recorder.Calls.Add("GroupJoin")
Return New Spy(Of TResult)
End Function
Public Function GroupBy(Of TKey, TResult)(keySelector As Func(Of T, TKey),
resultSelector As Func(Of TKey, Spy(Of T), TResult)) As Spy(Of TResult)
Recorder.Calls.Add("GroupBy")
Return New Spy(Of TResult)
End Function
Public Function Count() As Integer
Recorder.Calls.Add("Count")
Return 0
End Function
Public Function Sum(selector As Func(Of T, Integer)) As Integer
Recorder.Calls.Add("Sum")
Return 0
End Function
End Class
Module Program
Sub Report(label As String)
Console.WriteLine(label.PadRight(28) & String.Join(" > ", Recorder.Calls))
Recorder.Calls.Clear()
End Sub
Sub Main()
Dim numbers As New Spy(Of Integer)
Dim words As New Spy(Of String)
Dim q1 = From n In numbers Where n > 1 Select n * 2
Report("Where, Select")
Dim q2 = From n In numbers Order By n, n * 2 Descending Select n
Report("Order By a, b Descending")
Dim q3 = From n In numbers Let sq = n * n Where sq > 4 Select sq
Report("Let, Where, Select")
Dim q4 = From n In numbers From w In words Select n & w
Report("two From clauses")
Dim q5 = From n In numbers Join w In words On n Equals w.Length Select w
Report("Join")
Dim q6 = From n In numbers Group Join w In words On n Equals w.Length Into Group Select n
Report("Group Join")
Dim q7 = From n In numbers Group By k = n Mod 2 Into Group
Report("Group By ... Into Group")
Dim q8 = From n In numbers Select n Distinct Skip 1 Take 2
Report("Select Distinct Skip Take")
Dim q9 = From n In numbers Skip While n < 3 Take While n < 9 Select n
Report("Skip While, Take While")
Dim q10 = Aggregate n In numbers Where n > 0 Into Count(), Sum(n)
Report("Aggregate Into Count Sum")
Dim q11 = From n In numbers Where n > 0
Report("Where only (no Select)")
End Sub
End Module
Where, Select Where > Select
Order By a, b Descending OrderBy > ThenByDescending > Select
Let, Where, Select Select > Where > Select
two From clauses SelectMany
Join Join
Group Join GroupJoin > Select
Group By ... Into Group GroupBy
Select Distinct Skip Take Select > Distinct > Skip > Take
Skip While, Take While SkipWhile > TakeWhile > Select
Aggregate Into Count Sum Where > Count > Sum
Where only (no Select) Where
What the output teaches:
-
Order By a, b Descendingis oneOrderByfollowed byThenByDescending; the first key usesOrderBy, neverThenBy. -
A
Selectthat just projects the final range variable is folded into the previous operator when one can carry it (JoinandSelectManytake a result selector), which is why those queries show no separateSelect. -
Letis aSelectthat carries the old range variables along with the new one in a hidden anonymous type; this is the reasonLetappears asSelectin the trace. -
Group Join ... Into GroupisGroupJoinfollowed bySelectthat exposes the group;Group Byis a singleGroupBycall. -
Aggregateapplies the filtering clauses once and then calls each aggregate method (Count,Sum) separately on the filtered query object. Over a lazy source every function enumerates the filter again: the last program on this page counts eight filter calls for four elements and two aggregate functions. -
A query with only a
Wherehas noSelectat all; its type is the source’s element type.
The same pattern is why the same query text can be a pipeline of delegates over an array or an expression tree handed to a
database provider: the lookup finds Enumerable.Where(Func(Of T, Boolean)) for the former and Queryable.Where(Expression(Of Func(Of T, Boolean))) for the latter.
Method syntax and the operators without a keyword
Method syntax is the ordinary extension-method chain with lambdas. It is not a different feature, and it can be mixed with query
syntax in both directions: a query can start from a method call, and a method call can be applied to a parenthesised query. Many
operators have no query keyword and are only available as methods: MaxBy, MinBy, DistinctBy, Chunk, Zip, ToLookup,
ToDictionary, ToHashSet, Union, Intersect, Except, Concat, Append, Prepend, Reverse, Order, OrderDescending,
ElementAt, Contains, SequenceEqual, and the newest additions CountBy, AggregateBy and Index, which were added to
System.Linq in .NET 9. All of them work from Visual Basic exactly like any other extension method.
Option Strict On
Public Class Book
Public Property Title As String
Public Property Author As String
Public Property Genre As String
Public Property Year As Integer
Public Sub New(title As String, author As String, genre As String, year As Integer)
Me.Title = title
Me.Author = author
Me.Genre = genre
Me.Year = year
End Sub
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("Emma", "Jane Austen", "Novel", 1815),
New Book("Persuasion", "Jane Austen", "Novel", 1817),
New Book("Dune", "Frank Herbert", "Science fiction", 1965),
New Book("Neuromancer", "William Gibson", "Science fiction", 1984),
New Book("Walden", "Henry David Thoreau", "Essay", 1854)
}
' Operators that have no query keyword exist only as methods
Console.WriteLine("MaxBy : " & books.MaxBy(Function(b) b.Year).Title)
Console.WriteLine("DistinctBy : " & String.Join(",", books.DistinctBy(Function(b) b.Genre).Select(Function(b) b.Title)))
Console.WriteLine("Chunk : " & String.Join(" | ", books.Chunk(2).Select(Function(c) String.Join(",", c.Select(Function(b) b.Title)))))
Console.WriteLine("Zip : " & String.Join(",", books.Select(Function(b) b.Title).Zip({1, 2, 3}, Function(t, n) n & t)))
Console.WriteLine("ToLookup : " & books.ToLookup(Function(b) b.Genre)("Novel").Count())
Console.WriteLine("ToDictionary: " & books.ToDictionary(Function(b) b.Title, Function(b) b.Year)("Dune"))
Console.WriteLine("Order : " & String.Join(",", {3, 1, 2}.Order()))
Console.WriteLine("OrderDesc : " & String.Join(",", {3, 1, 2}.OrderDescending()))
Console.WriteLine("Append : " & String.Join(",", {1, 2}.Append(3).Prepend(0)))
Console.WriteLine("Contains : " & books.Select(Function(b) b.Genre).Contains("Essay"))
Console.WriteLine("First? : " & If(books.FirstOrDefault(Function(b) b.Year > 2000) Is Nothing, "none", "found"))
Console.WriteLine("ElementAt : " & books.ElementAt(2).Title)
Console.WriteLine("Aggregate : " & books.Aggregate(0, Function(acc, b) acc + b.Title.Length))
Console.WriteLine("Union : " & String.Join(",", {1, 2, 3}.Union({3, 4})))
Console.WriteLine("Except : " & String.Join(",", {1, 2, 3}.Except({3, 4})))
Console.WriteLine("Intersect : " & String.Join(",", {1, 2, 3}.Intersect({3, 4})))
Console.WriteLine("Reverse : " & String.Join(",", {1, 2, 3}.Reverse()))
' Added to System.Linq after the language feature set was frozen
Console.WriteLine("CountBy : " & String.Join(",", books.CountBy(Function(b) b.Genre).Select(Function(kv) kv.Key & "=" & kv.Value)))
Console.WriteLine("AggregateBy: " & String.Join(",", books.AggregateBy(Function(b) b.Genre, 0, Function(acc, b) acc + b.Year).Select(Function(kv) kv.Key & "=" & kv.Value)))
Console.WriteLine("Index : " & String.Join(",", {"a", "b"}.Index().Select(Function(t) t.Index & t.Item)))
' Query keywords and method calls mix freely
Dim mixed = From g In books.GroupBy(Function(b) b.Genre)
Where g.Count() > 1
Select g.Key, Newest = g.Max(Function(b) b.Year)
For Each m In mixed
Console.WriteLine(m.Key & " " & m.Newest)
Next
End Sub
End Module
MaxBy : Neuromancer
DistinctBy : Emma,Dune,Walden
Chunk : Emma,Persuasion | Dune,Neuromancer | Walden
Zip : 1Emma,2Persuasion,3Dune
ToLookup : 2
ToDictionary: 1965
Order : 1,2,3
OrderDesc : 3,2,1
Append : 0,1,2,3
Contains : True
First? : none
ElementAt : Dune
Aggregate : 35
Union : 1,2,3,4
Except : 1,2
Intersect : 3
Reverse : 3,2,1
CountBy : Novel=2,Science fiction=2,Essay=1
AggregateBy: Novel=3632,Science fiction=3949,Essay=1854
Index : 0a,1b
Novel 1817
Science fiction 1984
Source: Enumerable class, Enumerable.CountBy
Two Visual Basic details about method syntax:
-
A lambda for the operators is a
Function(x) ...expression with a single expression as its body; to use statements, write a multi-lineFunction ... End Function(a statement lambda), which is fine forIEnumerableoperators and rejected for expression trees (BC36675). -
A method chain may wrap lines after the dot (
books.at the end of the line), as the first example does, because the dot and the parenthesis are implicit continuation points (see Delegates and Lambda Expressions).
Anonymous types in projections
Select b.Title, b.Year produces elements of an anonymous type. In a query, the members that the compiler creates from the
projection are Key properties: they are read-only and they take part in equality and hash codes. The same projection written as
New With {b.Title, b.Year} in a method call creates non-key properties, which is why the comparison at the start of this page
needed Key in the method form to type-check (without it, BC36754 reports that the two anonymous types differ). The consequences
are:
-
Two projected rows with equal values are equal, so
Distinct,Group Bywith an anonymous key andJoinon a composite key work as expected for query-syntax projections. -
Anonymous types declared without
Keyuse reference equality for those members, soDistinctremoves nothing and a compositeGroup Bykey creates one group per element. -
The members cannot be assigned after construction (
Keymembers are read-only) and their types are not nameable, so a function that returns them must returnIEnumerable(Of Object), a named class or a tuple.
Option Strict On
Module Program
Sub Main()
Dim genres As String() = {"Novel", "Essay", "Novel", "Novel", "Essay"}
' Query projections have Key members: equal values are equal
Console.WriteLine((From g In genres Select g, Len = g.Length Distinct).Count())
' New With without Key: the members take no part in equality
Dim plain = genres.Select(Function(g) New With {.Genre = g}).Distinct().Count()
Dim keyed = genres.Select(Function(g) New With {Key .Genre = g}).Distinct().Count()
Console.WriteLine("plain Distinct: " & plain & ", keyed Distinct: " & keyed)
' Group By with a composite key built without and with Key
Dim plainGroups = From g In genres Group By k = New With {.Genre = g} Into Group
Dim keyedGroups = From g In genres Group By k = New With {Key .Genre = g} Into Group
Console.WriteLine("plain groups: " & plainGroups.Count() & ", keyed groups: " & keyedGroups.Count())
End Sub
End Module
2
plain Distinct: 5, keyed Distinct: 2
plain groups: 5, keyed groups: 2
Source: Anonymous Types
Tuples are an alternative that has a name: Select (Member:=l.Member, b.Title, daysLate) (see the shelf report at the end of
this page) gives ValueTuple elements with named members. The full comparison, including the equality rules, is on
Tuples and Anonymous Types.
Reusable queries
A query that is useful in several places becomes a function. The function should return IEnumerable(Of T), not a materialised list,
so that callers can keep composing on it, and the function should take the source as a parameter instead of reading a field. A
query returned from a function is still unexecuted. The classic extension-method form turns it into a query operator of its own;
an Iterator Function can run code between elements and stays lazy as well (see
Iterators and Yield and
Extension Methods and Partial Methods).
Option Strict On
Imports System.Runtime.CompilerServices
Public Class Book
Public Property Title As String
Public Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
End Class
Module BookQueries
' A reusable query operator is an extension method over IEnumerable(Of T)
<Extension>
Function Published(source As IEnumerable(Of Book), after As Integer) As IEnumerable(Of Book)
Return From b In source Where b.Year > after
End Function
<Extension>
Function Page(Of T)(source As IEnumerable(Of T), pageNumber As Integer, size As Integer) As IEnumerable(Of T)
Return From x In source Skip (pageNumber - 1) * size Take size
End Function
' An iterator version can run code between elements and stays lazy
Iterator Function Numbered(source As IEnumerable(Of Book)) As IEnumerable(Of String)
Dim i As Integer = 0
For Each b In source
i += 1
Yield i & ". " & b.Title
Next
End Function
End Module
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("Emma", 1815), New Book("Dune", 1965), New Book("Neuromancer", 1984),
New Book("Hyperion", 1989), New Book("Ulysses", 1922)
}
Dim page2 = books.Published(1900).Page(2, 2)
For Each line In Numbered(page2)
Console.WriteLine(line)
Next
' A function that returns a query returns the description, not the results
Dim lazy = books.Published(1950)
books.Add(New Book("Solaris", 1961))
Console.WriteLine(lazy.Count())
End Sub
End Module
1. Hyperion
2. Ulysses
4
Source: Extension Methods
Comparison, ordering and Option Compare
Strings in a Where clause are compared with the normal = operator, so Option Compare Text makes b.Genre = "NOVEL" match
"Novel". The key comparisons that the operators perform internally are different: Group By, Join and Distinct call the default
equality of the key type, which for String is ordinal and case-sensitive, and the Equals keyword of Join is therefore not
affected by Option Compare either. Order By uses the default comparer of the key type (for strings, the comparison of the current culture); it needs a key that implements IComparable, and a key that does not fails when the query runs, not when it is
compiled. To control string comparison in grouping, ordering or joining, call the method form with an explicit StringComparer
(books.GroupBy(Function(b) b.Genre, StringComparer.OrdinalIgnoreCase)).
Option Strict On
Option Compare Text
Public Class Book
Public Property Title As String
Public Property Genre As String
Public Sub New(title As String, genre As String)
Me.Title = title
Me.Genre = genre
End Sub
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {
New Book("Emma", "Novel"), New Book("Ulysses", "novel"), New Book("Dune", "Science fiction")
}
' The = operator honours Option Compare Text ...
Console.WriteLine("Where : " & (From b In books Where b.Genre = "NOVEL" Select b.Title).Count())
' ... but the key comparisons of Group By, Join and Distinct use the default .NET equality
Console.WriteLine("Group : " & (From b In books Group By b.Genre Into Group).Count())
Console.WriteLine("Distinct: " & (From b In books Select b.Genre Distinct).Count())
Dim lookup = {"NOVEL"}
Console.WriteLine("Join : " & (From b In books Join l In lookup On b.Genre Equals l Select b.Title).Count())
' Order By needs keys that can be compared
Try
Dim q = From b In books Order By b Select b.Title
Console.WriteLine(String.Join(",", q))
Catch ex As InvalidOperationException
Console.WriteLine("Order By object: " & ex.GetType().Name & ": " & ex.Message)
Catch ex As ArgumentException
Console.WriteLine("Order By object: " & ex.GetType().Name & ": " & ex.Message)
End Try
End Sub
End Module
Where : 2
Group : 3
Distinct: 3
Join : 0
Order By object: InvalidOperationException: Failed to compare two elements in the array.
Source: Option Compare Statement
LINQ to DataSet
A DataTable is not generic and does not implement IEnumerable(Of DataRow); the last line of the program shows that. The adapter
is AsEnumerable() (from System.Data.DataTableExtensions, in the shared framework), which exposes the rows as
IEnumerable(Of DataRow). Columns are read with Field(Of T)(name), which converts to the requested type and maps DBNull to
Nothing for reference types and nullable value types, while a direct cast of DBNull fails (and Field(Of Integer) on a DBNull throws). CopyToDataTable() goes the other way, from
a query over rows back to a table. LINQ to DataSet is plain LINQ to Objects over rows; no SQL is generated, so it fits in-memory
data, data that has already been fetched with a DataAdapter, and tests. See
Data Access: ADO.NET and EF Core for the connection
and adapter side.
Option Strict On
Imports System.Data
Module Program
Sub Main()
Dim loans As New DataTable("Loans")
loans.Columns.Add("Isbn", GetType(String))
loans.Columns.Add("Member", GetType(String))
loans.Columns.Add("DaysLate", GetType(Integer))
loans.Columns.Add("Note", GetType(String))
loans.Rows.Add("9780441172719", "ana", 0, DBNull.Value)
loans.Rows.Add("9780451524935", "ben", 12, "reminder sent")
loans.Rows.Add("9780141439518", "ana", 3, DBNull.Value)
loans.Rows.Add("9780261103573", "cleo", 30, "final notice")
' AsEnumerable() adapts the rows to IEnumerable(Of DataRow); Field(Of T) converts and maps DBNull
Dim late = From r In loans.AsEnumerable()
Where r.Field(Of Integer)("DaysLate") > 2
Order By r.Field(Of Integer)("DaysLate") Descending
Select Member = r.Field(Of String)("Member"),
Days = r.Field(Of Integer)("DaysLate"),
Note = r.Field(Of String)("Note")
For Each l In late
Console.WriteLine(l.Member & " " & l.Days & " " & If(l.Note, "(no note)"))
Next
Dim perMember = From r In loans.AsEnumerable()
Group By Member = r.Field(Of String)("Member") Into Total = Sum(r.Field(Of Integer)("DaysLate"))
For Each p In perMember
Console.WriteLine(p.Member & ": " & p.Total)
Next
' CopyToDataTable turns a query over DataRow back into a DataTable
Dim lateTable As DataTable = (From r In loans.AsEnumerable()
Where r.Field(Of Integer)("DaysLate") > 10
Select r).CopyToDataTable()
Console.WriteLine("copied rows: " & lateTable.Rows.Count)
' A typed read of a DBNull column throws; Field(Of String) maps DBNull to Nothing
Try
Dim bad As String = CStr(loans.Rows(0)("Note"))
Console.WriteLine(bad)
Catch ex As InvalidCastException
Console.WriteLine("CStr(DBNull): " & ex.GetType().Name)
End Try
Console.WriteLine("Field(Of String): " & If(loans.Rows(0).Field(Of String)("Note"), "Nothing"))
' Without AsEnumerable the table itself is not queryable
Console.WriteLine(GetType(IEnumerable(Of DataRow)).IsAssignableFrom(GetType(DataTable)))
End Sub
End Module
cleo 30 final notice
ben 12 reminder sent
ana 3 (no note)
ana: 3
ben: 12
cleo: 30
copied rows: 2
CStr(DBNull): InvalidCastException
Field(Of String): Nothing
False
Expression trees and IQueryable
A lambda is converted to one of two things depending on the target type. Assigned to a delegate type such as Func(Of Book, Boolean)
it becomes compiled code. Assigned to Expression(Of Func(Of Book, Boolean)) it becomes a data structure that describes the code
(an expression tree) which a provider can inspect and translate, for example to SQL. Queries over an IQueryable(Of T) pick the
Queryable operators, whose parameters are expression trees, so the query is captured as one tree and executed by the provider
when it is enumerated. Entity Framework Core works this way; see
Data Access: ADO.NET and EF Core.
The restrictions of expression trees in Visual Basic are stricter than those of delegates: only single-expression lambdas
(Function(b) b.Year > 1900) can be converted, statement lambdas cannot (BC36675, shown below), and the tree supports the
operators that System.Linq.Expressions has nodes for. The next program shows the two conversions, the tree of a query over
AsQueryable() (a real provider such as EF Core would translate it to SQL; the in-memory EnumerableQuery provider simply compiles
it), and a tree built by hand.
Option Strict On
Imports System.Linq.Expressions
Public Class Book
Public Property Title As String
Public Property Year As Integer
Public Sub New(title As String, year As Integer)
Me.Title = title
Me.Year = year
End Sub
End Class
Module Program
Sub Main()
Dim books As New List(Of Book) From {New Book("Emma", 1815), New Book("Dune", 1965)}
' A lambda converted to a delegate type is code; converted to Expression(Of ...) it is data
Dim asDelegate As Func(Of Book, Boolean) = Function(b) b.Year > 1900
Dim asTree As Expression(Of Func(Of Book, Boolean)) = Function(b) b.Year > 1900
Console.WriteLine(asTree)
Console.WriteLine(asTree.Body.NodeType.ToString() & " / " & asTree.Parameters(0).Name)
Console.WriteLine(asTree.Compile()(books(1)) & " " & asDelegate(books(0)))
' IEnumerable(Of T) queries take delegates; IQueryable(Of T) queries take expression trees
Dim source As IQueryable(Of Book) = books.AsQueryable()
Dim q = From b In source Where b.Year > 1900 Order By b.Title Select b.Title
Console.WriteLine(q.Expression)
Console.WriteLine(q.Provider.GetType().Name)
Console.WriteLine(String.Join(",", q))
' Building a tree by hand: b => b.Title = "Dune"
Dim p = Expression.Parameter(GetType(Book), "b")
Dim body = Expression.Equal(Expression.Property(p, "Title"), Expression.Constant("Dune"))
Dim lambda = Expression.Lambda(Of Func(Of Book, Boolean))(body, p)
Console.WriteLine(lambda)
Console.WriteLine(books.AsQueryable().Count(lambda))
End Sub
End Module
b => (b.Year > 1900)
GreaterThan / b
True False
System.Collections.Generic.List`1[App.Book].Where(b => (b.Year > 1900)).OrderBy(b => b.Title).Select(b => b.Title)
EnumerableQuery`1
Dune
b => (b.Title == "Dune")
1
PLINQ
AsParallel() turns an IEnumerable(Of T) into a ParallelQuery(Of T), and the same query clauses then bind to the operators of
System.Linq.ParallelEnumerable, which partition the source across threads. Query syntax works unchanged, Aggregate accepts a
parallel source, and AsOrdered() restores the source order at a cost. Parallelism pays off for CPU-bound work on large
sources, and not for small sources or cheap lambdas, where partitioning costs more than it saves. Delegates inside the query must be
thread safe and free of side effects; to collect results use ForAll with a concurrent collection, or enumerate the result.
The threading primitives are covered on
Threads, Parallelism and Synchronization.
Option Strict On
Module Program
Sub Main()
Dim numbers = Enumerable.Range(1, 20)
' AsParallel: the query runs on several threads, so the result order is not defined...
Dim unordered = From n In numbers.AsParallel()
Where n Mod 3 = 0
Select n * n
Console.WriteLine("unordered sorted: " & String.Join(",", unordered.OrderBy(Function(x) x)))
' ...unless AsOrdered is used
Dim ordered = From n In numbers.AsParallel().AsOrdered()
Where n Mod 3 = 0
Select n * n
Console.WriteLine("AsOrdered : " & String.Join(",", ordered))
' Aggregates over a parallel source
Dim stats = Aggregate n In numbers.AsParallel() Into Sum(n), Count()
Console.WriteLine("sum=" & stats.Sum & " count=" & stats.Count)
' Do not mutate shared state from a parallel query
Dim sink As New Concurrent.ConcurrentBag(Of Integer)
numbers.AsParallel().ForAll(Sub(n) sink.Add(n))
Console.WriteLine("collected: " & sink.Count)
End Sub
End Module
unordered sorted: 9,36,81,144,225,324
AsOrdered : 9,36,81,144,225,324
sum=210 count=20
collected: 20
Source: Introduction to PLINQ
A shelf report
A last example puts the pieces together on the shelf scenario: overdue loans joined with their books, a Let for the days late,
an Order By, a tuple projection with a named member, a grouped summary that uses the conditional form of Count, and a query whose
Select creates objects of an ordinary class.
Option Strict On
Public Enum LoanState
Active = 1
Returned = 2
Overdue = 3
End Enum
Public Class Book
Public Property Isbn As String
Public Property Title As String
Public Sub New(isbn As String, title As String)
Me.Isbn = isbn
Me.Title = title
End Sub
End Class
Public Class Loan
Public Property Isbn As String
Public Property Member As String
Public Property DueOn As Date
Public Property State As LoanState
Public Sub New(isbn As String, member As String, dueOn As Date, state As LoanState)
Me.Isbn = isbn
Me.Member = member
Me.DueOn = dueOn
Me.State = state
End Sub
End Class
Module Program
Sub Main()
Dim today As New Date(2026, 10, 19)
Dim books As New List(Of Book) From {
New Book("9780441172719", "Dune"), New Book("9780451524935", "1984"), New Book("9780141439518", "Emma")
}
Dim loans As New List(Of Loan) From {
New Loan("9780441172719", "ana", New Date(2026, 9, 1), LoanState.Overdue),
New Loan("9780451524935", "ben", New Date(2026, 10, 1), LoanState.Overdue),
New Loan("9780141439518", "ana", New Date(2026, 10, 30), LoanState.Active),
New Loan("9780441172719", "ben", New Date(2026, 5, 1), LoanState.Returned)
}
Dim report = From l In loans
Where l.State = LoanState.Overdue
Join b In books On l.Isbn Equals b.Isbn
Let daysLate = (today - l.DueOn).Days
Order By daysLate Descending
Select (Member:=l.Member, b.Title, daysLate)
For Each row In report
Console.WriteLine(row.Member & " has " & row.Title & " " & row.daysLate & " days late")
Next
Dim perMember = From l In loans
Group By l.Member Into Total = Count(), Open = Count(l.State <> LoanState.Returned)
Order By Open Descending, Member
For Each m In perMember
Console.WriteLine(m.Member & ": " & m.Total & " loans, " & m.Open & " open")
Next
' Query results can be projected into ordinary objects too
Dim overdueBooks As List(Of Book) = (From b In books
Join l In loans On b.Isbn Equals l.Isbn
Where l.State = LoanState.Overdue
Select New Book(b.Isbn, b.Title.ToUpper())).ToList()
Console.WriteLine(String.Join(", ", From b In overdueBooks Select b.Title))
End Sub
End Module
ana has Dune 48 days late
ben has 1984 18 days late
ana: 2 loans, 2 open
ben: 2 loans, 1 open
DUNE, 1984
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Query clauses |
|
|
Clause order |
nearly free; |
|
Group result |
|
|
Aggregates |
|
method calls |
Range variable of a projection |
members of |
not possible, use |
Ordering keys |
|
|
Join equality |
|
|
Anonymous type from a query projection |
|
value-equality read-only properties always |
Statement lambdas in expression trees |
BC36675 |
not allowed either (CS0834), expression bodies only |
Tuples in projections |
named tuple members, no deconstruction in |
deconstruction available |
XML in queries |
XML literals and axis properties inside queries |
LINQ to XML method calls only |
Error and warning reference
The messages below were produced by the SDK. The first program contains one statement for each case that compiles far enough to
reach semantic analysis; the messages are listed in the order of the statements. In order: the ByRef parameter in the Count
procedure (line 18), a late-bound Object source (27), an Integer source (28), join keys of unrelated types (29), a join key that
mixes both sides (30), an unknown aggregate Foo (31, which produces two messages), a query assigned to a List(Of String) (32),
a range variable declared twice (33) and a statement lambda converted to an expression tree (34).
Option Strict On
Imports System.Linq.Expressions
Public Class Book
Public Property Id As Integer
Public Property Title As String
Public Property Year As Integer
End Class
Public Class Author
Public Property Id As Integer
Public Property Name As String
End Class
Module Program
Sub Count(ByRef n As Integer, books As List(Of Book))
Dim q = From b In books Where b.Year > n Select b
End Sub
Sub Main()
Dim books As New List(Of Book)
Dim authors As New List(Of Author)
Dim obj As Object = books
Dim n As Integer = 42
Dim a = From x In obj Select x
Dim b = From x In n Select x
Dim c = From bk In books Join au In authors On bk.Id Equals au.Name Select bk
Dim d = From bk In books Join au In authors On bk.Id + au.Id Equals au.Id Select bk
Dim e = From bk In books Group By bk.Year Into Foo() Select Year
Dim f As List(Of String) = From bk In books Select bk.Title
Dim g = From bk In books Let bk = 1 Select bk
Dim h As Expression(Of Func(Of Book, Boolean)) = Function(bk)
Return bk.Year > 1900
End Function
End Sub
End Module
Program.vb(18,48): error BC36533: 'ByRef' parameter 'n' cannot be used in a query expression.
Program.vb(27,27): error BC36593: Expression of type 'Object' is not queryable. Make sure you are not missing an assembly reference and/or namespace import for the LINQ provider.
Program.vb(28,27): error BC36593: Expression of type 'Integer' is not queryable. Make sure you are not missing an assembly reference and/or namespace import for the LINQ provider.
Program.vb(29,69): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.
Program.vb(30,64): error BC36622: You must reference at least one range variable on both sides of the 'Equals' operator. Range variable(s) 'bk' must appear on one side of the 'Equals' operator, and range variable(s) 'au' must appear on the other.
Program.vb(31,56): error BC36594: Definition of method 'Foo' is not accessible in this context.
Program.vb(31,62): error BC30518: Overload resolution failed because no accessible 'Select' can be called with these arguments:
Program.vb(32,36): error BC30512: Option Strict On disallows implicit conversions from 'IEnumerable(Of String)' to 'List(Of String)'.
Program.vb(33,38): error BC30978: Range variable 'bk' hides a variable in an enclosing block or a range variable previously defined in the query expression.
Program.vb(34,58): error BC36675: Statement lambdas cannot be converted to expression trees.
Syntax errors stop the compiler before semantic analysis, so they cannot be shown in the same program. The query below uses
= instead of Equals, and the next one leaves out the Into list of Group By:
Program.vb(20,62): error BC36619: 'Equals' expected.
Program.vb(19,51): error BC36615: 'Into' expected.
A name that is not a range variable, and a Where condition that is not Boolean:
Program.vb(20,41): error BC36610: Name 'Title' is either not declared or not in the current scope.
Program.vb(21,34): error BC30518: Overload resolution failed because no accessible 'Where' can be called with these arguments:
Program.vb(21,34): error BC36594: Definition of method 'Where' is not accessible in this context.
The scope rule of Group By: after the clause the range variable y no longer exists, only the key c and the Into names do.
Option Strict On
Module Program
Sub Main()
Dim years As Integer() = {1815, 1965}
Dim q = From y In years
Group By c = y \ 100 Into Group
Order By y
Select c
End Sub
End Module
Program.vb(8,26): error BC36610: Name 'y' is either not declared or not in the current scope.
| Code | Cause | Fix |
|---|---|---|
BC36593 |
the source is not queryable: |
use a typed |
BC36533 |
a |
copy to a local first |
BC36594 |
a method such as |
check the aggregate name, the condition type and the namespace imports |
BC36610 |
a name that is neither a range variable nor in scope |
use the range variable or add |
BC36619 |
|
write |
BC36622 |
the two sides of |
split the expression so each side uses one source |
BC36615 |
|
add the |
BC30978 |
a range variable hides another variable or range variable |
rename it |
BC36675 |
a statement lambda converted to an expression tree |
use a single expression |
BC36754 |
anonymous types with and without |
add |
BC30512 |
an implicit conversion refused by |
call |
BC42327 |
warning: the loop control variable is used in a query |
copy it to a local inside the loop |
BC30209 |
|
keep |
Pitfalls
The last program of the page collects three behaviours that the discussion above relies on: how many times an Aggregate with two
functions runs its filter, when a Nothing source is detected, and that Sum is overflow-checked.
Option Strict On
Module Program
Sub Main()
' 1. Aggregate with several functions: the filter runs once per function
Dim filterCalls As Integer = 0
Dim numbers As Integer() = {1, 2, 3, 4}
Dim stats = Aggregate n In numbers
Where Seen(filterCalls, n) > 1
Into Count(), Sum(n)
Console.WriteLine(stats.Count & " " & stats.Sum & " filter calls: " & filterCalls)
' 2. A Nothing source fails when the query is built, not when it is enumerated
Dim missing As List(Of Integer) = Nothing
Try
Dim q = From n In missing Select n
Console.WriteLine("query built")
Catch ex As ArgumentNullException
Console.WriteLine("Nothing source: " & ex.ParamName)
End Try
Console.WriteLine(String.Join(",", From n In If(missing, New List(Of Integer)) Select n).Length)
' 3. Sum of Integer is checked, whatever the project overflow setting is
Dim big As Integer() = {Integer.MaxValue, 1}
Try
Console.WriteLine(big.Sum())
Catch ex As OverflowException
Console.WriteLine("Sum: " & ex.GetType().Name)
End Try
Console.WriteLine(big.Sum(Function(x) CLng(x)))
End Sub
Function Seen(ByRef calls As Integer, n As Integer) As Integer
calls += 1
Return n
End Function
End Module
3 9 filter calls: 8
Nothing source: source
0
Sum: OverflowException
2147483648
Source: Enumerable.Sum
-
Writing a query does not run it, and a query that is enumerated twice runs twice; call
ToList()to take a snapshot, or keep the query when you want it to see later changes. -
ToList()on a very large source copies it; the streaming operators do not. -
Side effects in
WhereandSelectlambdas run at unpredictable times and possibly more than once. -
Order Byis stable, andGroup Byreturns the groups in the order their keys first appear in the source. -
Keys: use
Keyon every anonymous-type member that should take part in equality; do not rely onOption Compare Textfor grouping, joining or distinct. -
Single()throws on zero and on several matches;SingleOrDefault()throws on several;First()throws on zero.FirstOrDefault()returns the type’s default value, which is0forInteger, so test the result type before trusting a0. -
A
ByRefparameter or the loop control variable of aForcannot be used safely in a query (BC36533, BC42327); copy it to an ordinary local first. -
A
Nothingsource throwsArgumentNullExceptionas soon as the query is built (the last program shows it), not when it is enumerated. Guard withIf(source, Enumerable.Empty(Of T)()). -
Prefer
Any()toCount() > 0: it stops at the first element, andCount()on a plain sequence has to read all of it. -
LINQ to Objects on
DictionaryenumeratesKeyValuePair(Of TKey, TValue)items; useKeyandValueof the pair.
Where the books are outdated
The two books consulted for this section treat LINQ as a feature of the language and as a way to query databases. Three of their points are out of date for .NET 10:
-
LINQ to SQL (
System.Data.Linq,DataContext) is not part of modern .NET. It is a .NET Framework technology that is no longer developed. The current tool for object-relational queries is Entity Framework Core, whoseDbSet(Of T)is anIQueryable(Of T)and whose queries use the same query syntax shown here. EF6 (System.Data.Entity, automatic migrations,Enable-Migrations) is likewise replaced by EF Core withdotnet ef migrations add; the scaffolding tools emit C#, so a Visual Basic project writes its entity classes by hand or uses a community package. The data-access page has the details. -
Query-versus-method syntax is presented as a choice of style; the compiler translation shown above makes clear that they are the same thing and may be mixed, and that several operators exist only as methods.
-
The VB-only clauses (
Aggregate,Distinct,Skip,Take,Skip While,Take While,Group By ... Into) are usually not covered; they are shown on this page with their translations.
References
-
LINQ (Visual Basic Programming Guide) and Queries (Language Reference)
-
From, Where, Select, Order By, Join, Group Join, Group By, Let, Distinct, Skip, Take, Skip While, Take While and Aggregate clauses
-
Visual Basic Language Specification (query expressions)