The most natural thing to do when you need to display a filtered list in Blazor is to write a @foreach loop and put an @if inside it. It works. But it is the wrong pattern — it mixes presentation and logic, makes the template unreadable, and forces Blazor to diff a full-sized list on every render even when most items are filtered out.
LINQ computed properties solve all three problems. This article shows exactly how.
The Problem: Filtering Inside the Loop
Here is the pattern you want to stop writing:
@* BAD: logic inside the template *@
@foreach (var student in students)
{
@if (student.Score >= 60 && student.IsEnrolled)
{
<div class="student-card">
<h3>@student.Name</h3>
<p>Score: @student.Score</p>
</div>
}
}
The problems: the template now contains business logic, Blazor iterates all students every render, and adding a second filter condition makes the @if grow into something unreadable.
Deferred Execution
LINQ methods like Where, OrderBy, and Select return query objects — they do not execute until you enumerate the result. This means a computed property that chains LINQ operators only does real work at the moment Blazor renders the @foreach. No intermediate list is allocated unless you call .ToList().
// This line allocates nothing yet.
IEnumerable<Student> query = students.Where(s => s.Score >= 60);
Computed Properties in Blazor
Move all filtering logic into a C# computed property (a get-only property). The template stays clean; the logic is testable in isolation.
private string activeFilter = "All";
private string activeSortCol = "Name";
private IEnumerable<Student> FilteredItems =>
students
.Where(s => activeFilter == "All" || s.Status == activeFilter)
.OrderBy(s => activeSortCol switch
{
"Score" => (object)s.Score,
"Status" => s.Status,
_ => s.Name
});
@* GOOD: template is pure presentation *@
@foreach (var student in FilteredItems)
{
<div class="student-card">
<h3>@student.Name</h3>
<p>Score: @student.Score</p>
</div>
}
Chaining Operators
Operators chain seamlessly. Each returns a new query — no intermediate allocation, one deferred execution at render time:
private IEnumerable<Student> ChainedItems =>
students
.Where(s => s.IsEnrolled)
.Where(s => s.Score >= 60)
.OrderByDescending(s => s.Score);
Read this as: "give me enrolled students who passed, in descending score order." The query is expressed in the same order you would say it in English.
The Four Daily-Use Patterns
These four operators appear in nearly every Blazor component that works with lists:
// 1. Does any item match a condition?
bool hasHighScorers = students.Any(s => s.Score > 90);
// 2. How many items match?
int passingCount = students.Count(s => s.Score >= 60);
// 3. Unique values for a dropdown or tag cloud
IEnumerable<string> departments =
students.Select(s => s.Department).Distinct();
// 4. Safe first match (returns null instead of throwing)
Student? topStudent = students
.OrderByDescending(s => s.Score)
.FirstOrDefault();
FirstOrDefault returns null when the sequence is empty. Always null-check the result before using it.
When to Call .ToList()
Deferred execution is usually a benefit, but there are two cases where you want to materialise the query into a List<T>:
- You need to pass the result to a method that expects a
List<T>orIList<T>. - The query is expensive and you reference the result multiple times in the same render — without
.ToList(), the query re-executes on each access.
// Without .ToList(): query runs twice (once for Count, once for foreach)
private IEnumerable<Student> Filtered => students.Where(s => s.Score >= 60);
// With .ToList(): query runs once, result is cached for the render
private List<Student> FilteredList => students.Where(s => s.Score >= 60).ToList();
The trade-off is allocation (a new List<T> every render) versus deferred execution. For small lists, the difference is negligible. For large datasets, profile before deciding.
Key Takeaways
- Never filter with
@ifinside@foreach— move all filtering logic into a computed property. - LINQ operators use deferred execution: the query runs at enumeration time, not when the property is declared.
- Chain
Where,OrderBy, andSelectfreely — each returns a new query object with no intermediate allocation. Any,Count,Select().Distinct(), andFirstOrDefaultare the four operators you will use every day.- Call
.ToList()when you need a concrete list or when you access the result more than once in the same render cycle.