Advanced C#

LINQ queries and deferred execution, async/await with Task, generics, and delegates and events.

LINQ

LINQ (Language Integrated Query) lets you query in-memory collections, databases (via EF Core), and XML with a single, consistent, declarative syntax — instead of hand-writing loops with manual filtering and accumulation.

C#
var products = new List<Product>
{
    new("Keyboard", 49.99m, "Electronics"),
    new("Desk", 199.99m, "Furniture"),
    new("Mouse", 19.99m, "Electronics"),
    new("Chair", 149.99m, "Furniture"),
};

// Method syntax — chained extension methods, the more commonly used style
var electronics = products
    .Where(p => p.Category == "Electronics")
    .OrderBy(p => p.Price)
    .Select(p => p.Name)
    .ToList();
// ["Mouse", "Keyboard"]

// Query syntax — SQL-like, functionally identical to the above
var electronicsQuery =
    from p in products
    where p.Category == "Electronics"
    orderby p.Price
    select p.Name;

decimal totalValue = products.Sum(p => p.Price);
Product? cheapest = products.MinBy(p => p.Price);
bool anyExpensive = products.Any(p => p.Price > 100);

public record Product(string Name, decimal Price, string Category);

Deferred execution is one of LINQ's most important — and most tested — behaviors: a query built with Where/Select/etc. doesn't actually run when you write it. It only executes when you enumerate the result (a foreach, or a terminal call like .ToList(), .Count(), .First()):

C#
var query = products.Where(p => p.Price > 50);   // not executed yet — just describes the query

products.Add(new Product("Monitor", 299.99m, "Electronics"));

foreach (var p in query)     // executes NOW, and sees the newly added Monitor too
{
    Console.WriteLine(p.Name);
}

Async/await with Task

C#'s async/await (predating JavaScript's near-identical syntax) lets asynchronous, I/O-bound code read like ordinary sequential code, without blocking a thread while waiting:

C#
public async Task<string> FetchUserNameAsync(int userId)
{
    using var client = new HttpClient();
    var response = await client.GetStringAsync($"https://api.example.com/users/{userId}");
    return response;
}

public async Task RunAsync()
{
    string name = await FetchUserNameAsync(1);
    Console.WriteLine(name);
}

An async method returns Task (no meaningful result) or Task<T> (a result of type T) — never the bare type directly, mirroring how a JavaScript async function always returns a Promise. Awaiting several independent operations one-by-one wastes time the same way it does in JavaScript — use Task.WhenAll to run them concurrently:

C#
// Sequential — total time ≈ sum of both calls
var user = await FetchUserAsync(1);
var orders = await FetchOrdersAsync(1);

// Parallel — total time ≈ the slower of the two
var userTask = FetchUserAsync(1);
var ordersTask = FetchOrdersAsync(1);
await Task.WhenAll(userTask, ordersTask);
var user2 = userTask.Result;
var orders2 = ordersTask.Result;

Generics

Generics let a type or method work with any type while preserving full compile-time type safety — no casting, no object-typed parameters:

C#
public class Box<T>
{
    private T value;

    public Box(T value) => this.value = value;

    public T GetValue() => value;
}

var intBox = new Box<int>(42);
var stringBox = new Box<string>("hello");

// A generic method with a constraint
public static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana"));  // banana

where T : IComparable<T> is a generic constraint — it restricts T to types that support .CompareTo(), so the method body can safely call it.

Delegates and events, briefly

A delegate is a type-safe reference to a method — essentially a strongly-typed function pointer. Action and Func are the built-in generic delegate types used almost everywhere in modern C#:

C#
Action<string> log = message => Console.WriteLine($"LOG: {message}");
log("Something happened");

Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3));   // 5

An event is a delegate-based publish/subscribe mechanism — a class exposes an event that other code can subscribe handlers to, without either side needing a direct reference to the other:

C#
public class Button
{
    public event Action? Clicked;

    public void SimulateClick()
    {
        Clicked?.Invoke();   // notify all subscribers, if any exist
    }
}

var button = new Button();
button.Clicked += () => Console.WriteLine("Button was clicked!");
button.SimulateClick();   // Button was clicked!

Common mistakes

  • Forgetting that a LINQ query is deferred — enumerating the same query variable twice re-runs it against the collection's current state, which can produce surprising results if the collection changed in between.
  • Blocking on async code with .Result or .Wait() instead of await-ing it — this can cause a deadlock in UI and ASP.NET (non-async) contexts, and defeats the entire purpose of using async in the first place.
  • Awaiting independent Tasks sequentially instead of starting them together and awaiting with Task.WhenAll.

Interview questions

Q: What does "LINQ has deferred execution" actually mean, and why does it matter? A LINQ query built with methods like .Where() or .Select() isn't evaluated when it's constructed — it's only evaluated when enumerated (a foreach, or a terminal operation like .ToList()/.Count()). This matters because the query re-runs against the collection's state at enumeration time, not at the time it was written — if the underlying collection changes in between, the results can differ from what you might expect, and enumerating the same query twice does the work twice.

Q: What actually happens when you await a Task? The calling method's execution pauses at that point and returns control to its caller, freeing up the current thread rather than blocking it. Under the hood, the compiler transforms the async method into a state machine: when the awaited Task eventually completes, execution resumes from where it left off — on a thread pool thread (or the original synchronization context, in UI apps) — continuing the method's remaining code.