The LINQ, the Lazy and the C#: What You Need to Know About Lazily Evaluated Queries
Feb 28, 2020 • 6 Minute Read
Introduction
LINQ, which is short for Language Integrated Query, was introduced to the .NET framework with version 3.5 back in 2007. The core concept was to extend the framework—and, along with it, the C# language—with query expression capabilities. We differentiate these capabilities into two groups: query syntax and method syntax. The first is more like a SQL query to filter out different data which fit a certain criteria, while the second is done with method calls similar to the keywords of SQL queries. It allows you to work with different types of data, such as SQL databases, XML documents, and responses from web services.
In this guide, we will see examples of both these syntaxes, and we will discuss deferred execution and lazy and eager evaluation.
Query and Method Syntax
This is a small demonstration of query syntax. Let's say we have a list of servers with their functionality.
IList<string> serverList = new List<string>() {
"Parabellum - Domain Controller",
"Prostetic - DNS",
"Spooky - Domain Controller",
"Scar - DHCP" ,
"BigBoy - Web Server"
};
Let's say we would like to query all the Domain Controllers in the list.
With the query syntax method, we can achieve this the following way.
var DCS = from s in serverList
where s.Contains("Domain Controller")
select s;
With method syntax, this is the way to go about it.
var DCS = serverList.Where(s => s.Contains("Domain Controller"));
In both cases, the DCS variable holds a list with two matching servers.
Deferred Execution
In order to dig deeper into LINQ and its other benefits, we need to understand deferred execution. In most cases, during compilation, LINQ expressions are evaluated on a specific dataset and the variables are initialized with filtered values. By implementing deferred execution, we can save valuable CPU and RAM resources because we don't have to evaluate a specific expression or delay the evaluation until the realized value is actually required. There are two cases when this is very useful. The first is when your application is working on multiple gigabytes or even terrabytes of data, and the second is when you have chained together multiple queries that result in different manipulations of the dataset.
yield
In order to deepen our understanding of deferred execution, we need to introduce the keyword yield. This keyword was introduced with version 2.0 of C#. It is vital for lazy evaluation and improves the performance of LINQ queries. The keyword yield is contextual, which means it can be used as a variable name without any problems. Let's create a small app that uses yield to iterate over the servers.
using System;
using System.Collections.Generic;
namespace Pluralsight
{
public class Server {
string _name;
string _function;
public string Name {
get { return _name; }
set { _name = value; }
}
public string Function {
get { return _function; }
set { _function = value; }
}
public Server(string Name, string Function) {
_name = Name;
_function = Function;
}
}
public class Yielder
{
public static IEnumerable<Server> ServerDB()
{
Server[] servers = new Server[]
{
new Server("Parabellum","Domain Controller"),
new Server("Pluck","DNS Server"),
new Server("Pilgrim","Web Server"),
new Server("Spark","Firewall Server"),
};
foreach(Server s in servers)
{
yield return s;
}
}
public static void Main()
{
foreach(Server s in ServerDB())
{
Console.WriteLine($"Name: {s.Name}, Function: {s.Function}");
}
Console.ReadKey();
}
}
}
This produces the following output.
Name: Parabellum, Function: Domain Controller
Name: Pluck, Function: DNS Server
Name: Pilgrim, Function: Web Server
Name: Spark, Function: Firewall Server
The key here is the yield, which allows us to iterate over each element of our Server[] list. It does not return the whole list at once, thus saving memory and improving performance. This is also an example of the deferred execution, as the foreach loop pulled out items one by one, iterating over them. If we want, we can play around with it by placing the following line after the yield return s; section.
Console.WriteLine($"Sending {s.Name}");
This would modify the output in the following way.
Name: Parabellum, Function: Domain Controller
Sending Parabellum
Name: Pluck, Function: DNS Server
Sending Pluck
Name: Pilgrim, Function: Web Server
Sending Pilgrim
Name: Spark, Function: Firewall Server
Sending Spark
Note how Sending... appears after each element in the array.
Lazy vs. Eager
The use of lazy evaluation means that only a single element of the source collection is processed during each call to the iterator. An iterator can be a custom class or a foreach or while loop, depending on your context.
The opposite of this is usually eager evaluation. This means the first call to the iterator will return the entire collection, or dataset.
Conclusion
In this guide, we have looked at how LINQ revolutionized the way programmers work on different datasets and interact with different datasources. We have seen some examples of query and method syntax, and we learned the difference between lazy and eager evaluation. I hope this guide has been informative to you and I would like to thank you for reading it!