
One of the peculiarities that distinguish the .NET world from other technological stacks is definitely LINQ, an acronym for Language INtegrated Query. Introduced with the .NET Framework 3.5 and Visual Studio 2008, it is, in fact, the first framework independent of the architecture and integrated within the C # and Visual Basic languages.
With LINQ we can query and manipulate data using a single programming model independent of the various types of sources. To better understand what it is, however, we must take a leap into the past.
In the firstest versions of C#, we had to use a for or foreach loop to iterate over a collection, which, as we know, implements the IEnumerable interface, and, for example, find a particular object inside it. The following code returns all the customers of a company, aged between 19 and 36 years (20 – 35 years):
class Customer
{
public int CustomerID { get; set; }
public String CustomerName { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main(string[] args)
{
Customer[] customerArray = {
new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 },
new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 },
new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
};
Customer[] customers = new Customer[10];
int i = 0;
foreach (Customer cst in customerArray)
{
if (cst.Age > 19 && cst.Age < 36)
{
customers[i] = cst;
i++;
}
}
}
}
What might be a different approach? Let’s try to get it step by step, starting from the concept of delegate. A delegate is a type that represents references to methods with the same parameters and returned type. It “delegates” to the method to which it aims the execution of the code and we can declare it in this way:
public delegate bool Operations(int number);
All methods that take in input an integer and return a Boolean can be pointed by this delegate. For example, suppose we have a method in a CustomerOperations class:
public bool CustomerAgeRangeCheck(int number)
{
return number > 19 && number < 36;
}
We can register one or more methods that will be executed when the delegate is executed:
Operations op = new Operations(CustomerOperations.CustomerAgeRangeCheck);
Or simply:
Operations op = CustomerOperations.CustomerAgeRangeCheck;
op(22);
Delegates are used to pass methods as arguments to other methods: event handlers and callbacks are examples of methods called through delegates.
C# 2.0 introduced anonymous delegates, you can now use an anonymous method to declare and initialize a delegate. For example, we can write:
delegate bool CustomerFilters(Customer customer);
class CustomerOperations
{
public static Customer[] FindWhere(Customer[] customers, CustomerFilters customerFiltersDelegate)
{
int i = 0;
Customer[] result = new Customer[6];
foreach (Customer customer in customers)
if (customerFiltersDelegate(customer))
{
result[i] = customer;
i++;
}
return result;
}
}
class Program
{
static void Main(string[] args)
{
Customer[] customers = {
new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 },
new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 },
new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
};
Customer[] filteredCustomersAge = CustomerOperations.FindWhere(customers, delegate (Customer customer) //Using anonimous delegate
{
return customer.Age > 19 && customer.Age < 36;
});
}
}
With C # 2.0, we have the advantage of using anonymous delegates in searching with different criteria with no need to use a for or foreach loop. For example, we can use the same delegate function used in the previous example, to find a customer whose “CustomerID” is 3 or whose name is “Bob”:
Customer[] filteredCustomersId = CustomerOperations.FindWhere(customers, delegate (Customer customer)
{
return customer.CustomerID == 3;
});
Customer[] filteredCustomersName = CustomerOperations.FindWhere(customers, delegate (Customer customer)
{
return customer.CustomerName == "Bob";
});
With the evolution of C#, since the 3.x versions, the Microsoft Team has introduced new features that make the code even more compact and readable. These are in direct support of LINQ to query different types of data sources and obtain the elements resulting in a single instruction.
These features are:
– The var construct, an implicitly typed local variable. It is strongly typed as the type itself had been declared, but it’s the compiler that determines the type using the Type Inference based on the value assigned to it. The following two statements are functionally equivalent:
var customerAge = 30; // Implicitly typed.
int customerAge = 30; // Explicitly typed.
– Object initializers allow you to assign values to all or some of the properties of an object during its creation time, with no need to invoke a constructor followed by assignment instruction lines.
Customer customer = new Customer { Age = 30, CustomerName = "Adolfo" };
Unlike the following code, in the previous case, everything is considered as a single operation.
Customer customer = new Customer();
customer.Age = 30;
customer.CustomerName = "Adolfo";
– Anonymous types, a read-only type that is built by the compiler, and only the compiler knows it. Still, if two or more anonymous object initializers in an assembly have a sequence of properties in the same order and with the same names and types, the compiler treats the objects as instances of the same type. Anonymous types are a good way to temporarily group a set of properties in the result of a query, without having to define a separate named type.
var customer = new { YearsOfFidelity = 10, Name = "Francesco"};
– Extension methods, which allow you to “add” methods to existing types without creating a new derived type, recompiling, or modifying otherwise the original type. Extension methods are static methods, but they are called thanks to the introduced syntactic sugar, as they were instance methods on the extended type.
public static class StringExtensionMethods
{
public static string ReverseString(this string input)
{
if (string.IsNullOrEmpty(input)) return "";
return new string(input.ToCharArray().Reverse().ToArray());
}
}
Extension methods must be defined in a static class. The first parameter represents the type to be extended and must be preceded by the keyword this, further parameters do not need it.
Console.WriteLine("Hello".ReverseString()); //olleH
Note that the first parameter, the one preceded by the this modifier, must not be specified in the method call.
– Lambda expressions, anonymous functions that can be passed as a variable or as a parameter in a method call.
customer => customer.Age > 19 && customer.Age < 36;
The => operator is called lambda operator, while customer is the input parameter of the function. The part on the right side of lambda operator represents the body of the function and the value it returns, in this case a Boolean.
We finally arrive with C# 3.5 version at the introduction of LINQ.
Simplifying, we could say that LINQ is an extension methods library for IEnumerable<T> and IQueryable<T> interfaces, which allows us to perform various operations such as filtering, making projections, aggregations and sorting.
We have several LINQ implementations available:
- LINQ to Objects (In-Memory Object Collection)
- LINQ to Entities (Entity Framework)
- LINQ to SQL (SQL Database)
- LINQ to XML (XML Document)
- LINQ to DataSet (ADO.Net Dataset)
- By implementing IQueryable interface (Other data source)
In the previous examples, an array was used as a data source, therefore the generic interface IEnumerable<T< is implicitly supported. Types that support IEnumerable<T> or its derived interface, such as the generic IQueryable<T> interface, are called queryable types and allow us to execute LINQ queries directly. If the data source is not already in memory as a queryable type, the LINQ provider must represent it as such.
As we said, LINQ queries are mostly based on generic types, introduced in version 2.0 of .NET Framework. This means that if you try, for example, to add a Customer object to a List<string> object, an error will be generated at compile-time. It is easy to use generic collections because there’s no need to cast types at run-time.
If we prefer, we can avoid the generic syntax by using the var keyword of which we spoke previously, which in the following example asks the compiler to deduce the type of a query variable by examining the data source specified in the from clause.
So let’s see how we can achieve the same result that in the previous code we obtained using anonymous delegates, using a LINQ to Object query, the var construct, and a lambda expression:
var filteredCustomersAge = customers.Where(c => c.Age > 19 && c.Age < 36);
This type of syntax is called Method Syntax.
In the next example, we’ll use the Query Syntax, a syntax introduced for those who already know the SQL language and would, therefore, feel comfortable with this type of approach:
var filteredCustomersAge =
from customer in customers
where customer.Age > 19 && customer.Age < 36
select customer;
Query Syntax and Method Syntax are semantically identical, and many people find the syntax of queries simpler and easier to read.
In Query Syntax, LINQ query operators are converted into calls to the related LINQ extension methods at compile-time.
In the next article, we’ll continue to talk about LINQ!
We’ll talk about the IQueryable<T> interface, its related LINQ extension methods, and the differences with IEnumerable<T> interface.
We will also see the use of LINQ with data sources from out-memory collections, as in the case of remote databases.
See you at the next article! Stay Tuned!