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> or IList<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 @if inside @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, and Select freely — each returns a new query object with no intermediate allocation.
  • Any, Count, Select().Distinct(), and FirstOrDefault are 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.