Less is more – this is definitely true when it comes to object collections and LINQ queries. One of the most valuable advantages is the ability to write cleaner and more concise code, thus accomplishing more with fewer lines.
The current article will introduce the basic understanding of LINQ technologies and present alternatives to the “traditional” way of solving programming challenges based on data collections.
Ready? Let’s start!
Why Do We Use LINQ?
LINQ stands for Language Integrated Query. It is a set of technologies enabling us to write queries on strongly typed collections of objects as first-class constructs.
From the pure usage point, we can think of LINQ as a proxy allowing us to use the same queries for manipulating the data collections of multiple types.
LINQ Query Provider
Thus, LINQ can work with many other technologies. The component that makes it possible is called the LINQ Query provider. It converts the LINQ query into another format that matches the “counterpart” source of data.
This way, the LINQ provider resolves all the issues related to the expression processing and validation and calls the necessary procedure to construct a query to be accepted by the source or retrieve the data from it correctly. It is one of the biggest LINQ pluses that makes it so popular among developers.
The LINQ to SQL provider, for instance, will convert the specific LINQ query into the necessary T-SQL query that the SQL database will accept and understand precisely. The LINQ to XML provider will convert the LINQ query to make it the XML-accepted XLST query. The same goes for other technologies.
We’ll explore some of these providers and illustrate them with examples in further articles dedicated to LINQ.
Popular LINQ technologies
Furthermore, it enables us to use the same query expression patterns on different data collections, including SQL database, ADO.NET Datasets, XML objects, and any object collections that implement IEnumerable or the IEnumerable<T> interface:
- LINQ to Objects. It allows for querying in-memory objects. The method is similar to the previous one (LINQ to SQL) but even simpler.
- LINQ to SQL. It allows for using SQL queries to work with relational databases. The method creates an interface of its own that does not depend on the database engine used by the application.
- LINQ to Entities. This method converts a LINQ query to a command-tree query and allows for executing them against the Entity Framework. The objects returned are compatible with both LINQ and Entity Framework.
- LINQ to XML. It allows for working with XML – querying and modifying documents and saving them with LINQ query expressions.
- LINQ to JSON. It provides an API to work with JSON objects. With its help, you can query and create JSON objects quickly.
- LINQ to DataSet. It allows the developers to query the data in DataSet objects. It is possible to use programming languages to write queries instead of specialized query languages. It is a great benefit for Visual Studio users.
Among the Language Integrated Query technologies, LINQ-to-Objects is the most versatile. That’s why it will be the focus of this article. We will examine and compare traditional solutions to some programming challenges based on the data collections with alternative LINQ-based solutions.
LINQ to Object Query Examples
LINQ objects, classes, and interfaces live in System.Linq namespace. To them, we need our class first of all. In C#, we do it with:
using System.Linq;
Before digging into a comparison between the classic and LINQ-based solutions, we need to know which methods are available.
LINQ Extension Methods
Some of the commonly used LINQ methods are:
- Select – projects each element of the collection to a new form or object type.
- Where – filters collection elements based on a predicate.
- FirstOrDefault/LastOrDefault – returns the first/last element of the collection. If the collection is empty, it returns the default value of the object type.
- Count – returns the number of elements in a collection.
- Min/Max – returns the minimum/maximum value in the sequence of values.
- OrderBy/OrderByDescending – sorts the sequence elements in ascending/descending order based on a predicate.
For the full list of methods and details, please refer to the original Microsoft documentation.
Now that we know some of the methods at our disposal, let’s see how we can use them. In our first example of the LINQ usage, let’s define a class Student:
public class Student{ public string FirstName { get; set; } public string LastName { get; set; } public int BirthYear { get; set; } public enum GenderType { Female, Male}; public GenderType Gender { get; set; } public string Interests { get; set; }}
The list of students is as follows:
List<Student> students = GetStudents(); //Some arbitrary method that return a list of Students
In all code examples, we use the letter m as a reference to the current element in the sequence. Thus, in the below expression, m is the reference to each element in the list:
students.Select(m => m.FirstName)
Have a look at more LINQ query examples.
Get the list of students born before 1984:
var oldStudents = students.Where(m => m.BirthYear < 1984);
Get the total number of students to use:
var numberOfStudents = students.Count();
Define the number of female students:
var numberOfFemaleStudents = students.Where(m => m.Gender == Student.GenderType.Female).Count();
There is an even shorter variant of the above query:
numberOfFemaleStudents = students.Count(m => m.Gender == Student.GenderType.Female);
Get the first student on the list:
var firstStudent = students.FirstOrDefault();
Get the oldest student:
var oldestStudent = students.OrderBy(m => m.BirthYear).LastOrDefault();
Get the first student sorted alphabetically by the First Name:
var firstStudentByName = students.OrderBy(m => m.FirstName).FirstOrDefault();
Get the first male student on the list:
var firstMaleStudent = students.Where(m => m.Gender == Student.GenderType.Male).FirstOrDefault();
Again, we can make the query shorter:
firstMaleStudent = students.FirstOrDefault(m => m.Gender == Student.GenderType.Male);
If we only need to get each student’s First Name of each student, we use the following code:
var studentNames = students.Select(m => m.FirstName);
Get the simple list of all students’ full names:
var studentFullNames = students.Select(m => m.FirstName + " " + m.LastName);
The next case is a bit more advanced example of the SELECT method. This method accepts predicates with the member reference m and the index of the element in the list.
The result will be the list of first and last names with the index for each record:
var studentFullNamesWithIndexes = students.Select((m, index) => string.Format("{0}. {1} {2}", index.ToString(), m.FirstName, m.LastName)).ToList();
More Examples of How to Use LINQ in C#
To understand the difference from “traditional” solutions, we will use the coding challenges. Many job interviews include them as tests, so, it’s better to be well-prepared. Though we would not claim our solutions provided in this article are the best or even optimal, but they will suggest helpful approaches for solving some problems.
Let’s start with something pretty simple. The task is to write a function that takes a string and checks if it is a palindrome (written the same forward and backward).
The first approach would be to iterate through the indices of the character, reverse the string and compare it with the original to see if they match.
public static bool IsPalindrome(string str){ var reversed = ""; for (var i = str.Length - 1; i >= 0; i--) { reversed += str[i]; } return str.Equals(reversed);}
Let’s see how we could do the same with LINQ:
public static bool IsPalindrome(string str){return str.Equals(string.Join("", str.ToArray().Reverse()));}
In this case, we use build-in LINQ methods to separate the string into an array of characters (str.ToArray()), reverse the order of the characters (Reverse()), join the characters to a new string (string.Join method) and finally, compare it with the original string. If they match, the string is a palindrome.
Everything is nicely packed in one line. Thus, we can refer to this expression as a “one-liner.”
Just for fun, let’s see a couple more LINQ query solutions to this problem. For instance, the following example suits well:
public static bool IsPalindrome(string str){ for (var i = 0; i < str.Length / 2; i++)if (str[i] != str[str.Length - i - 1]) return false; return true;}
In this case, we use a different logic. It is looping through the input string and comparing the character on the current index in the string with the character on the same index in a reverse string. If they do not match, the string is not a palindrome.
We can do the same with a LINQ query:
public static bool IsPalindrome(string str){ return !str.Where((m,i) => m != str[str.Length - i - 1]).Any();}
As you can see in these examples, LINQ-based solutions are easier to write and read. They require less code. The original solution can be better in resource-critical situations when the execution time or memory usage is critical. We’ll deal with the issue of the LINQ performance in some of the future posts. Meantime, let’s see more examples of how LINQ can make our life easier.
You are given a random array of strings with the names of playing cards. The goal is to write a function that returns the number of full decks of cards that we could assemble with our available cards. To be honest, it took me longer to come up with a solution that would not LINQ
One approach can be to loop through all cards, stack them by the card, and determine if we have a stack for every card. If so, the number of decks we can assemble is actually the length of the smallest stack. This would be one implementation of this logic.
public static int HowManyDecks(this IEnumerable<string> cards, int deckSize = 32){ var dict = new Dictionary<string, int>(); foreach (var card in cards) if (dict.ContainsKey(card)) dict[card] += 1; else dict.Add(card, 1); if (dict.Keys.Count < deckSize) { return 0; } else { var minCount = int.MaxValue; foreach (var card in dict) if (card.Value < minCount) minCount = card.Value; return minCount; }}
Now let’s do it with LINQ:
public static int HowManyDecksLinq(this IEnumerable<string> cards, int deckSize = 32){ var groups = cards.GroupBy(m => m); if (groups.Count() < deckSize) return 0; var counts = groups.Select(g => g.Count()); return counts.Min();}
First, we will group our cards by the card value. If we do not have enough card groups, we can not assemble any full decks, and that’s all. Then we get counts for each group. Again, the number of decks we can assemble is the length of the smallest deck.
After some housekeeping, we end up with the below method:
public static int HowManyDecksLinq(this IEnumerable<string> cards, int deckSize = 32){ var groups = cards.GroupBy(m => m); return groups.Count() < deckSize ? 0 : groups.Select(g => g.Count()).Min();}
I think we can conclude it is a lot cleaner and easier to read than the original.
A Bit More Advantages of Using LINQ
LINQ comes in handy in situations when we need to do multiple operations on data collections.
Let’s say we have a list of objects and we need a function that returns a list of completely different objects based on the original data. To be more specific, we have a list of Students (defined in the first chapter), and we need to convert it to the list of the Student Profiles.
public class StudentProfile{ public string FullName { get; set; } public int Age { get; set; } public Student.GenderType Gender { get; set; } public string[] Interests { get; set; }}
With LINQ, this is a simple task.
public static List<StudentProfile> GetStudentProfiles(this List<Student> students){ return students.Select(m => new StudentProfile() { FullName = string.Format("{0} {1}", m.FirstName,m.LastName), Age = DateTime.Now.Year - m.BirthYear, Gender = m.Gender, Interests = m.Interests .Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) .Select(i =>i.Trim()) .ToArray() }).ToList();}
Here we can see that data manipulation on object collections is intuitive and simple with LINQ.
Let’s expand this example a bit. Assume, we want to find out how many male and female students are younger than 25 years.
Again, the LINQ almighty makes this easy:
var students = GetStudents();var studentProfiles = students.GetStudentProfiles();var youngerThan25GenderGoups = studentProfiles .Where(m => m.Age < 25) .GroupBy(m => m.Gender) .Select(g => new { Gender = g.Key, Count = g.Count() }) .ToArray();
Or even shorter:
var youngerThan25GenderGoups = GetStudents().GetStudentProfiles().Where(m => m.Age <25).GroupBy(m => m.Gender).Select(g => new { Gender = g.Key, Count = g.Count()}).ToArray();
Conclusion
I hope these examples have proved the benefits of using LINQ in your code. Once you get used to the LINQ query syntax and logic, it helps you to think about manipulating the data collections more clearly. You could see it in the second challenge where the LINQ solution came in more intuitive and natural than the iterative approach.
Of course, it is not all rainbows and butterflies when it comes to LINQ. As with anything else, it can be a valuable tool in certain or even most situations. However, if the situation requires more attention on memory usage or execution time, you might need to refer to other approaches offering more control over the resources.
We have seen only one aspect and barely scratched the surface of LINQ technologies. In future posts, we’ll dig deeper.
Thank you for sticking until the end of the article. Hope you enjoyed it and will join us at the next one!
Tags: .net, c#, linq Last modified: March 13, 2023