BrandGhost
LINQ Filtering in C#: Where, Any, All, Contains, and OfType

LINQ Filtering in C#: Where, Any, All, Contains, and OfType

LINQ filtering in C# is the foundation of nearly every query you will ever write. Before you can sort, group, project, or aggregate your data, you almost always need to narrow down the set of elements you are working with. The LINQ filtering operators -- Where, Any, All, Contains, and OfType<T>() -- each serve a distinct purpose, and understanding when to reach for each one is the difference between clean, composable query chains and convoluted loops full of flags and break statements.

This article covers every filtering operator in depth, with realistic domain examples using orders, products, and customers, explicit null-handling strategies, and concrete performance considerations.


Where(predicate): The Core Filter Operator

Where is the workhorse of LINQ filtering. It accepts a predicate delegate and returns a lazy sequence containing only the elements for which that predicate returns true.

namespace ECommerce.Orders;

public sealed record Order(
    int Id,
    string CustomerId,
    decimal Total,
    OrderStatus Status,
    DateTimeOffset PlacedAt);

public enum OrderStatus { Pending, Processing, Shipped, Delivered, Cancelled }

// Basic filter
IEnumerable<Order> pendingOrders =
    orders.Where(o => o.Status == OrderStatus.Pending);

// Compound AND predicate
IEnumerable<Order> urgentOrders =
    orders.Where(o =>
        o.Status == OrderStatus.Processing &&
        o.Total > 1000m &&
        o.PlacedAt < DateTimeOffset.UtcNow.AddDays(-3));

// OR logic -- actionable statuses
IEnumerable<Order> actionableOrders =
    orders.Where(o =>
        o.Status == OrderStatus.Pending ||
        o.Status == OrderStatus.Processing);

Where is deferred -- it does no work until you iterate over the result. Chaining multiple Where calls is equivalent to combining their predicates with AND logic, but each call adds another lazy layer. This is usually negligible for in-memory collections, but it is worth knowing for readability and occasional profiling.

Null Handling in Where

When your data might contain nulls, handle them explicitly inside the predicate to avoid NullReferenceException at iteration time:

namespace ECommerce.Customers;

public sealed record Customer(
    int Id,
    string Name,
    string? Email,
    Address? ShippingAddress);

public sealed record Address(string City, string Country);

// Null-safe compound filter using null-conditional operators
IEnumerable<Customer> domesticWithEmail =
    customers.Where(c =>
        c.Email is not null &&
        c.ShippingAddress?.Country == "US");

// Defensive null guard before projection
IEnumerable<string> cities =
    customers
        .Where(c => c.ShippingAddress is not null)
        .Select(c => c.ShippingAddress!.City);

The null-conditional operator (?.) works correctly inside LINQ to Objects predicates. For LINQ to SQL or Entity Framework Core, some providers may not translate ?. correctly -- test with a real query provider and inspect the generated SQL.


Any(predicate) and All(predicate): Short-Circuit Semantics

Any and All are terminal operators -- they force evaluation and return a bool. Both short-circuit, stopping as soon as a result can be determined.

  • Any(predicate) -- returns true the moment a matching element is found; returns false only after exhausting the entire sequence without a match.
  • All(predicate) -- returns false the moment a non-matching element is found; returns true only after confirming every element satisfies the predicate.
namespace ECommerce.Inventory;

public sealed record Product(string Sku, string Name, decimal Price, int Stock);

// Does any product need restocking?
bool needsRestock = products.Any(p => p.Stock < 10);

// Are all products priced above their cost floor?
bool allProfitable = products.All(p => p.Price > p.CostFloor);

// Parameterless Any() -- is the collection non-empty?
bool hasProducts = products.Any();

Any() vs Count() > 0: A Common Performance Mistake

One of the most frequent LINQ anti-patterns in C# codebases is writing collection.Count() > 0 when all you need to know is whether any element exists:

namespace ECommerce.Reporting;

// Slow path -- enumerates every element to produce a count
if (orders.Where(o => o.Total > 500).Count() > 0)
{
    ProcessHighValueOrders();
}

// Fast path -- stops at the first matching element
if (orders.Any(o => o.Total > 500))
{
    ProcessHighValueOrders();
}

For a collection of 100,000 orders where the first order qualifies, Any() touches one element; Count() touches all 100,000. This is a meaningful real-world difference when data comes from a slow source or when the predicate itself is expensive.

All() and the Vacuous Truth Edge Case

All() returns true for an empty collection -- there are no elements to violate the predicate, so the condition holds vacuously. If an empty set should not satisfy your business rule, add a guard:

namespace ECommerce.Fulfillment;

// Vacuous truth -- returns true when orders is empty
bool allShippedNaive = orders.All(o => o.Status == OrderStatus.Shipped);

// Correct: explicitly handle the empty case
bool allShipped =
    orders.Any() &&
    orders.All(o => o.Status == OrderStatus.Shipped);

Contains(element): Exact Match Testing

Contains tests whether a sequence includes a specific element. With no second argument, it uses the default equality for T. You can pass an IEqualityComparer<T> to override equality.

namespace ECommerce.Catalog;

// Membership test against a primitive value
bool hasDiscontinued = statuses.Contains(ProductStatus.Discontinued);

// Anti-pattern: Contains on a List inside Where is O(n * m)
var skuList = new List<string> { "SKU001", "SKU002", "SKU003" };
var slow = products.Where(p => skuList.Contains(p.Sku)); // O(n * m)

// Correct: use HashSet for O(1) lookups inside Where
var skuSet = new HashSet<string> { "SKU001", "SKU002", "SKU003" };
var fast = products.Where(p => skuSet.Contains(p.Sku)); // O(n)

This HashSet conversion pattern matters significantly when the lookup set is large. If you're checking 10,000 products against a list of 500 featured SKUs, the difference between List.Contains (5,000,000 comparisons) and HashSet.Contains (10,000 comparisons) is dramatic.

Contains with IEqualityComparer

For case-insensitive string matching or domain-specific equality:

namespace ECommerce.Search;

// Case-insensitive tag filtering
var targetTags = new[] { "sale", "featured", "new" };

IEnumerable<Product> taggedProducts =
    products.Where(p =>
        p.Tags.Any(tag =>
            targetTags.Contains(tag, StringComparer.OrdinalIgnoreCase)));

// Checking if a sequence directly contains a string regardless of case
bool hasTag = product.Tags.Contains("SALE", StringComparer.OrdinalIgnoreCase);

OfType<T>(): Filtering by Runtime Type

OfType<T>() filters a sequence to only elements that are assignable to T, and it casts each surviving element to T in the output. This is the right tool when you have a heterogeneous collection and want a strongly-typed subset.

namespace ECommerce.Events;

public abstract record DomainEvent(DateTimeOffset OccurredAt);
public sealed record OrderPlaced(int OrderId, DateTimeOffset OccurredAt)
    : DomainEvent(OccurredAt);
public sealed record OrderCancelled(int OrderId, string Reason, DateTimeOffset OccurredAt)
    : DomainEvent(OccurredAt);
public sealed record PaymentReceived(int OrderId, decimal Amount, DateTimeOffset OccurredAt)
    : DomainEvent(OccurredAt);

IEnumerable<DomainEvent> allEvents = GetEventLog();

// Extract only cancellations
IEnumerable<OrderCancelled> cancellations =
    allEvents.OfType<OrderCancelled>();

// Extract large payments
IEnumerable<PaymentReceived> largePayments =
    allEvents
        .OfType<PaymentReceived>()
        .Where(p => p.Amount > 500m);

This pattern is central to Plugin Architecture in C#. When you load a collection of IPlugin objects at startup, OfType<IExportPlugin>() extracts only the plugins that implement a specific capability, with no casting ceremony in calling code.

The important contrast is between OfType<T>() and Cast<T>():

  • OfType<T>() -- silently skips elements that are not assignable to T
  • Cast<T>() -- throws InvalidCastException on the first incompatible element

Use Cast when you know all elements are T and want to surface type errors as exceptions. Use OfType when a mixed bag is expected and skipping non-matching elements is the correct behavior.


Combining and Composing Predicates

For scenarios where filtering criteria are built at runtime -- such as a search screen with optional filters -- chaining Where calls is the cleanest approach:

namespace ECommerce.Filtering;

public sealed record OrderSearchRequest(
    OrderStatus? Status,
    decimal? MinTotal,
    string? CustomerId,
    DateTimeOffset? PlacedAfter);

public static IEnumerable<Order> ApplyFilters(
    IEnumerable<Order> orders,
    OrderSearchRequest request)
{
    IEnumerable<Order> result = orders;

    if (request.Status.HasValue)
    {
        result = result.Where(o => o.Status == request.Status.Value);
    }

    if (request.MinTotal.HasValue)
    {
        result = result.Where(o => o.Total >= request.MinTotal.Value);
    }

    if (request.CustomerId is not null)
    {
        result = result.Where(o => o.CustomerId == request.CustomerId);
    }

    if (request.PlacedAfter.HasValue)
    {
        result = result.Where(o => o.PlacedAt >= request.PlacedAfter.Value);
    }

    return result;
}

Each Where call is lazy and independent -- only the elements that pass all active filters are ever materialized. Adding a new filter criterion is one new if block; it does not require modifying a sprawling compound expression.

For a more advanced predicate-building approach, the Decorator Pattern provides a composable way to wrap filter logic -- each decorator wraps the previous one with an additional constraint, keeping each criterion independently testable and replaceable.


Filtering by Enum Values

Filtering by enum values is extremely common in domain-rich C# applications. LINQ's Where operator combines naturally with enum comparisons:

namespace ECommerce.Reporting;

// Filter by single value
var shipped = orders.Where(o => o.Status == OrderStatus.Shipped);

// Filter by a set of values -- use HashSet for performance
var activeStatuses = new HashSet<OrderStatus>
{
    OrderStatus.Pending,
    OrderStatus.Processing
};
var activeOrders = orders.Where(o => activeStatuses.Contains(o.Status));

// Exclude one status
var notCancelled = orders.Where(o => o.Status != OrderStatus.Cancelled);

For a deep dive into the enum type itself and how it works under the hood, see the C# Enum: Complete Guide. When your filtering logic needs exhaustive branching per enum value -- for example, routing orders to different processing queues based on status -- C# Enum Switch patterns give you compiler-enforced completeness, which is a strong safety net as the enum grows.


Performance Considerations for LINQ Filtering

Order predicates from most selective to least

When chaining multiple Where calls, put the predicate that eliminates the most elements first. Later predicates only run on elements that survived earlier ones:

namespace ECommerce.Analytics;

// If 95% of orders are under $5,000 total value, filter that first
orders
    .Where(o => o.Total > 5000m)       // eliminates 95% immediately
    .Where(o => !o.IsRefunded)          // runs on remaining 5%
    .Where(o => o.Region == "EU");      // runs on even smaller subset

Avoid double enumeration

Where returns an IEnumerable<T>. If you use the result in both a Count() call and a foreach, the source is evaluated twice. Materialize once when you need multiple passes:

namespace ECommerce.Fulfillment;

// Double enumeration -- source scanned twice
var largeOrders = orders.Where(o => o.Total > 1000m);
int count = largeOrders.Count();      // full scan
foreach (var o in largeOrders) { }   // full scan again

// Single enumeration -- materialize once
var largeOrdersList = orders.Where(o => o.Total > 1000m).ToList();
int count = largeOrdersList.Count;    // O(1) -- List.Count property
foreach (var o in largeOrdersList) { }

TryGetNonEnumeratedCount for pre-check sizing

When you need to know the count of a filtered collection before deciding whether to proceed -- but want to avoid full enumeration -- .NET 6's TryGetNonEnumeratedCount can help when the underlying collection implements ICollection<T>:

namespace ECommerce.Batch;

var filtered = orders.Where(o => o.Status == OrderStatus.Pending);

if (filtered.TryGetNonEnumeratedCount(out int count) && count == 0)
{
    return; // Skip processing entirely -- fast path, no scan
}

Note that TryGetNonEnumeratedCount returns false for Where results (since count cannot be known without enumeration), but it returns true for List<T>, arrays, and other collections that implement ICollection<T> without a Where filter applied.

The interplay between filtering logic and overall application architecture matters too. Feature Slicing vs Clean Architecture addresses where filtering logic should live -- a decision that has a real impact on how testable and reusable your predicates end up being.


Frequently Asked Questions

What is the difference between Where and OfType in LINQ?

Where filters elements of a known type T using a Boolean predicate applied to each element's properties or state. OfType<TResult>() filters a heterogeneous collection by the runtime type of each element, returning only elements that are assignable to TResult. Use Where for predicate-based filtering on homogeneous collections; use OfType when you have a mixed-type collection and want to extract all instances of a specific type.

Why does Any() perform better than Count() > 0?

Any() short-circuits -- it stops evaluating as soon as it finds one qualifying element (or confirms the sequence is non-empty). Count() must visit every element before returning. For a collection of a million elements where the first one matches, Any() reads one element; Count() reads all one million. Always prefer Any() when you only need to know whether at least one element exists or satisfies a condition.

Does All() return true for an empty collection?

Yes. All() follows the mathematical rule of vacuous truth: if there are no elements that could violate the condition, the condition holds. orders.All(o => o.IsShipped) returns true when orders is empty. If an empty result set should not satisfy your business rule, add an explicit guard: orders.Any() && orders.All(o => o.IsShipped).

Can I use null-conditional operators inside LINQ Where predicates?

Yes, for LINQ to Objects (in-memory collections), null-conditional operators (?., ??) work correctly inside predicates. For Entity Framework Core or other LINQ providers that translate expressions to SQL, some null-conditional patterns may not translate correctly. Always verify the generated SQL when using advanced C# expression features inside EF Core queries.

How do I filter a list by multiple enum values cleanly?

Use a HashSet<TEnum> containing the allowed values and test with Contains inside Where. This is O(n) total and scales cleanly as you add more allowed values:

var allowed = new HashSet<OrderStatus> { OrderStatus.Pending, OrderStatus.Processing };
var active = orders.Where(o => allowed.Contains(o.Status));

What is the difference between Cast() and OfType()?

Cast<T>() attempts to cast every element in the source sequence to T and throws InvalidCastException on the first element that is not assignable to T. OfType<T>() silently skips any element not assignable to T. Use Cast when you know all elements are T and want errors to surface immediately; use OfType when a mixed-type collection is expected and filtering out incompatible elements is the correct behavior.

How do I combine predicates dynamically at runtime in LINQ?

The cleanest approach for optional, runtime-composed filters is to chain Where calls conditionally -- add a Where only when the criterion is active. Each Where is lazy and independent. For functional predicate composition, you can build a list of Func<T, bool> and reduce them: predicates.Aggregate((a, b) => x => a(x) && b(x)). The Aggregate approach produces a single predicate delegate from any number of conditions.


Conclusion

LINQ filtering in C# is the entry point to nearly every query pipeline you will build. Where handles predicate-based narrowing with full support for compound logic and null-safe expressions. Any and All answer existence and universality questions with short-circuit efficiency. Contains tests set membership -- and always reaches for HashSet<T> when that membership test runs inside a Where. OfType<T>() cleanly handles heterogeneous collections without casting noise in calling code.

The performance rules are straightforward: order predicates from most selective to least, avoid double enumeration on unfiltered lazy sequences, and never write Count() > 0 when Any() is available. Apply these consistently and your filtering code will be both correct and efficient.

LINQ Grouping in C#: GroupBy, ToLookup, CountBy, and AggregateBy

Master LINQ grouping in C# with GroupBy, ToLookup, and the powerful .NET 9 CountBy and AggregateBy methods for cleaner data aggregation.

LINQ in C#: Complete Guide to Language Integrated Query (.NET 6-9)

Master LINQ in C# with this complete guide covering filtering, projection, ordering, grouping, joins, and every new operator added in .NET 6 through .NET 10.

Weekly Recap: LINQ, C# Regex, and Design Pattern Deep Dives [May 2026]

This week brings deep coverage of LINQ in C# (filtering, projection, ordering, and the complete guide), advanced regex topics including named capture groups and pattern syntax, plus practical guides on the bridge, facade, and flyweight design patterns. Plus four new videos covering platform team work, agentic systems, and developer mindset.

An error has occurred. This application may no longer respond until reloaded. Reload