When I was working on a recent .NET Core project, specifically on the SOA (Statement of Account) generation service, I encountered a familiar challenge; how to efficiently transform, filter and group data from various sources before writing it into Excel files. That’s where LINQ really shined.
LINQ (Language Integrated Query) has bee around for years, but its power still amazes me, especially when you know how to use it properly. If you’ve ever juggled nested loops and endless if conditions, LINQ is like discovering coffee after years of instant powder. Let me walk through what I’ve learned and relearned about LINQ while building real-world apps.
What is LINQ?
LINQ stands for Language Integrated Query. It allows you to query collections in a readable, concise, and expressive way right inside C#.
Instead of writing database style queries or dealing with messy loops, you write queries directly against in-memory objects like arrays, lists, XML and even databases (via Entity Framework).
List<string> names = new List<string> {"Anna", "Mark", "John"};
var shortNames = names.Where(n => n.Length <= 4).ToList();
With just one line, you’ve filtered a list. That’s LINQ magic.
Why Use LINQ?
In my project, I was dealing with thousands of transactions. I needed to:
- Filter by date range
- Group by merchant
- Aggregate values like total sales
- Sort results before writing to Excel
Without LINQ, that meant a lot of foreach blocks and manual grouping. With LINQ, I was able to replace 40+ lines of code with around 10.
var merchantSummaries = transactions
.Where(t => t.Date >= startDate && t.Date <= endDate)
.GroupBy(t => t.MerchantId)
.Select(g => new
{
MerchantId = g.Key,
TotalSales = g.Sum(t => t.Amount),
Count = g.Count()
})
.OrderByDescending(m => m.TotalSales)
.ToList();
It’s not just shorter, it’s earlier to read, easier to debug, and performs well when used correctly.
Key LINQ Concepts You Should Know
1. Deferred Execution
LINQ queries are not executed immediately. Instead, they’re executed when you enumerate them (e.g., using foreach
, ToList()
, or any terminal method).
This means LINQ builds a query expression tree or an iterator that remains dormant until you actually need the results.
Let’s walk through a simple example:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(n => n > 3); // Nothing happens here yet
numbers.Add(6);
foreach (var num in query) // Now the query runs with the updated list
{
Console.WriteLine(num);
}
Output:
4
5
6
Even though we defined the query before adding 6 to the list, LINQ evaluated the query at the time of enumeration, so the result includes the newly added number.
This is especially powerful in scenarios where you build a pipeline of transformations and filters but only want them executed when absolutely needed—like when returning paginated results or streaming data.
However, be cautious:
- If the underlying data changes between the time you define and execute the query, your results may change.
- If you’re accessing a database via EF Core, deferred execution means you can build up a dynamic SQL query, but it only hits the database once you enumerate.
You can force execution early by using terminal methods like .ToList()
, .ToArray()
, .Count()
, etc., which capture the results at that moment.
2. Method Syntax vs Query Syntax
In LINQ, there are two main styles of writing queries: query syntax and method syntax.
Query Syntax
This style resembles SQL and is more readable for those coming from a database background. It uses keywords like from
, where
, select
, and orderby
.
var highScores = from score in scores
where score > 80
orderby score descending
select score;
It’s easy to understand, especially for filtering and sorting.
Method Syntax
This style uses method chaining with lambda expressions. It is more flexible and powerful when working with complex queries.
var highScores = scores
.Where(s => s > 80)
.OrderByDescending(s => s);
Most developers (myself included) prefer method syntax in real-world projects. Why?
- It’s more concise.
- It allows you to chain multiple methods easily.
- It supports more advanced operations like
GroupBy
,Join
,SelectMany
, etc.
In my own projects, method syntax helped me build complex queries by chaining filters, transformations, and sorts in a readable way. Plus, IntelliSense in Visual Studio makes working with method syntax smoother.
Here’s a more advanced comparison:
Query Syntax:
var result = from t in transactions
where t.Amount > 1000
group t by t.MerchantId into g
select new
{
Merchant = g.Key,
Total = g.Sum(x => x.Amount)
};
Method Syntax:
var result = transactions
.Where(t => t.Amount > 1000)
.GroupBy(t => t.MerchantId)
.Select(g => new
{
Merchant = g.Key,
Total = g.Sum(x => x.Amount)
});
They do the same thing, but method syntax gives you a bit more control when queries get more complex.
In short: use what works best for you, but if you plan to work in a team or with EF Core, method syntax is usually the way to go.
3. Select vs SelectMany
Both Select
and SelectMany
are used to transform collections, but they behave differently.
Select:
Select
is a one-to-one projection—each input element maps to one output element.
var names = people.Select(p => p.Name);
This will return a new collection of just the names from the people
list. Every Person
object gives you one string result.
SelectMany
SelectMany
is a one-to-many projection—each input element maps to a collection, and SelectMany
flattens those collections into a single sequence.
Example:
var allTags = articles.SelectMany(a => a.Tags);
Each article might have multiple tags (a collection). SelectMany
combines all the tags from all articles into one flat list of tags.
When I worked on a blog feature, we used SelectMany
to extract all comment texts across multiple blog posts:
var allComments = blogPosts.SelectMany(p => p.Comments).Select(c => c.Text);
In short:
- Use
Select
if you’re returning a single result per item. - Use
SelectMany
if you’re returning multiple items per input and want to flatten the result.
4. Group By
GroupBy
is incredibly useful when you want to organize data into buckets based on a key, similar to SQL’s GROUP BY
.
var groupedByDept = employees.GroupBy(e => e.Department);
This will group employees by their department. The result is an IEnumerable<IGrouping<TKey, TElement>>
. Each group has a key and a list of items belonging to that key.
Here’s a real example from our SOA generator:
var salesByMerchant = transactions.GroupBy(t => t.MerchantId);
You can then iterate through the groups:
foreach (var group in salesByMerchant)
{
Console.WriteLine($"Merchant: {group.Key}");
foreach (var txn in group)
{
Console.WriteLine($" TxnID: {txn.Id}, Amount: {txn.Amount}");
}
}
You can also aggregate values:
var summary = transactions
.GroupBy(t => t.MerchantId)
.Select(g => new
{
Merchant = g.Key,
TotalAmount = g.Sum(x => x.Amount),
Count = g.Count()
});
This is incredibly powerful for reporting, analytics, and export logic.
5. ToList, ToArray, ToDictionary
These are terminal operators—they execute the query and give you concrete results.
.ToList()
Materializes the result into a List<T>
.
var activeUsers = users.Where(u => u.IsActive).ToList();
Use this when you want to cache results in memory or perform further list-specific operations.
.ToArray()
Same as .ToList()
, but returns an array.
var topThree = scores.OrderByDescending(s => s).Take(3).ToArray();
Arrays are slightly faster to access than lists, but less flexible.
.ToDictionary()
Turns a collection into a dictionary.
var usersById = users.ToDictionary(u => u.Id);
You can also customize the value:
var nameLookup = users.ToDictionary(u => u.Id, u => u.Name);
Just be careful – ToDictionary() will throw an exception if keys are duplicated.
In my own experience, I often use ToDictionary
when I need fast lookups, ToList
when I want to iterate multiple times, and ToArray
when I’m dealing with fixed-length data.
LINQ in Entity Framework (EF Core)
LINQ really shines when working with databases through Entity Framework Core (EF Core). It allows you to write strongly-typed queries that get translated into SQL by EF behind the scenes.
When you write a LINQ query against a DbSet<T>
, EF Core parses the expression tree and translates it to an SQL command. This means you get the benefits of compile-time checking, IntelliSense, and a cleaner syntax, all while generating optimized SQL queries.
Example:
var activeUsers = context.Users
.Where(u => u.IsActive && u.CreatedDate.Year == 2024)
.ToList();
EF Core will translate that into something like:
SELECT * FROM Users WHERE IsActive = 1 AND YEAR(CreatedDate) = 2024
Benefits of LINQ EF Core
- Type safety — No raw SQL strings prone to typos
- Refactor-friendly — Renaming a property updates everywhere
- Query composition — Easily build queries with conditional filters
Things to Watch Out For
While LINQ with EF Core is powerful, there are some caveats:
1. Client vs Server Evaluation
EF Core tries to convert everything to SQL. But if it encounters a method it doesn’t understand (like a custom C# method), it throws a runtime exception.
// This might fail if FormatName is a C# method
var users = context.Users
.Where(u => FormatName(u.FirstName) == "John")
.ToList();
- Always keep server-side logic SQL translatable.
- Use .AsEnumerable() if you need to switch to in-memory processing.
2. Premature ToList()
Calling .ToList() too early executes the query immediately. This prevents EF from optimizing the SQL and can result in unnecessary data being pulled into memory.
Bad:
var users = context.Users.ToList().Where(u => u.IsActive);
Good:
var users = context.Users.Where(u => u.IsActive).ToList();
3. Lazy vs. Eager Loading
If you’re navigating relationships (like User.Posts), make sure you explicitly load related data:
var user = context.Users
.Include(u => u.Posts)
.FirstOrDefault(u => u.Id == userId);
Lazy loading isn’t enabled by default in EF Core unless configured explicitly.
Real-World Tip
In one of our SOA tools, we built a dynamic query that changed based on input parameters:
var query = context.Transactions.AsQueryable();
if (startDate != null)
query = query.Where(t => t.Date >= startDate);
if (endDate != null)
query = query.Where(t => t.Date <= endDate);
if (!string.IsNullOrEmpty(merchantId))
query = query.Where(t => t.MerchantId == merchantId);
var result = query
.GroupBy(t => t.MerchantId)
.Select(g => new
{
MerchantId = g.Key,
TotalSales = g.Sum(t => t.Amount)
})
.ToList();
We used AsQueryable() to build the query conditionally without triggering execution until the final .ToList().
Performance Tips
1. Always check the generated SQL via ToQueryString()
Sometimes, your LINQ queries might look clean in C#, but generate inefficient SQL. EF Core provides a way to inspect the actual SQL that gets executed using .ToQueryString()
.
Example:
var query = context.Users.Where(u => u.IsActive);
string sql = query.ToQueryString();
Console.WriteLine(sql);
This will output the raw SQL that EF Core will execute against the database. It’s extremely useful for debugging performance issues or verifying that EF is generating the expected SQL.
2. Use Skip() and Take() for pagination
These are essential for paginated queries in web apps and dashboards. Instead of pulling all data into memory, you fetch only what’s needed.
Example:
var pageNumber = 2;
var pageSize = 10;
var pagedData = context.Transactions
.OrderBy(t => t.Date)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
This tells SQL to skip the first 10 results and take the next 10. EF Core translates this to OFFSET
and FETCH NEXT
SQL statements.
Important: Always use OrderBy()
before Skip()
and Take()
to ensure consistent and predictable results.
3. Enable EF Core Logging to Monitor Queries
You can configure EF Core to log the SQL queries it generates, which is helpful for troubleshooting and performance tuning.
If you’re using ASP.NET Core, you can enable it via appsettings.json
:
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information",
"Default": "Warning"
}
}
And in Program.cs
or Startup.cs
:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine, LogLevel.Information));
This will print the SQL statements to your console or log file, including parameters. Just remember to disable EnableSensitiveDataLogging()
in production to avoid leaking sensitive info.
My Favorite Use Case from a Real Project
In our SOA Generator, I had to prepare a list of merchants with their total sales, grouped by payment method, then sorted by sales volume.
var merchantSales = transactions
.GroupBy(t => new { t.MerchantId, t.PaymentMethod })
.Select(g => new
{
g.Key.MerchantId,
g.Key.PaymentMethod,
Total = g.Sum(x => x.Amount)
})
.OrderByDescending(x => x.Total)
.ToList();
This was used as input to generate hundreds of Excel files for each merchant. Without LINQ, it would’ve taken me days to build the logic and even longer to maintain it. LINQ made it clean, understandable, and efficient to test and debug.
Common Mistakes to Avoid
Here are some pitfalls I’ve personally run into (and seen others struggle with) when using LINQ:
1. Overusing .ToList()
Calling .ToList() after every filter or projection causes multiple in-memory executions instead of a single optimized one.
Bad:
Better:
2. Using unsupported Methods in EF Queries
LINQ with EF Core doesn’t support all C# methods. If a method can’t be translated to SQL, you’ll get a runtime error.
Always check that your query can be translated (use .ToQueryString() for this).
3. Not Handling Nulls
LINQ expressions can throw NullReferenceException if you’re not careful, especially when accessing nested properties.
Be defensive in your filters, especially with optional relationships.
LINQ is one of those tools that, once you master, you can’t imagine coding without. It allows you to express intent clearly, concisely, and in a way that’s easy for your future self and your teammates to understand. Whether you’re filtering, transforming, or joining data, LINQ offers a unified and expressive way to work with collections, databases and more.
References:
The insights and technical concepts shared in this post are inspired by both personal project experience and official documentation. To further explore LINQ and Entity Framework Core, I recommend the following resources:
Microsoft Documentation – LINQ
Microsoft Documentation – Entity Framework Core
LINQPad – The .NET Programmer’s Playground
TutorialsTeacher – LINQ Tutorials