Chapter 03 Csharp 3 Linq

5 min read
Rapid overview

Chapter 03 — C# 3: LINQ and everything that comes with it

Purpose of this chapter: understand the collection of C# 3 language features that (together) enable LINQ to be expressive and practical: implicit typing, lambdas, extension methods, query syntax, etc.


3.1 Automatically implemented properties

Auto-properties remove boilerplate when a property is just a simple backing field:

public string Name { get; set; }

Interview nuance:

  • Auto-properties are great for simple state exposure; once you need invariants/validation, you usually move to an explicit backing field or validation in the setter/constructor.
  • In C# 3 specifically, auto-properties don’t support initializers or true read-only auto-properties (later versions do).

3.2 Implicit typing

3.2.1 Typing terminology (what interviewers mean)

Keep these terms crisp:

  • Static typing vs dynamic typing: when binding/type checks happen (compile time vs runtime).
  • Explicit typing vs implicit typing: whether you must write the type name in source, or the compiler infers it from context.

3.2.2 Implicitly typed local variables (var)

var is still statically typed; it just asks the compiler to infer the type:

var language = "C#"; // inferred as string

Rules worth remembering:

  • Must be initialized at declaration.
  • The initializer must have a compile-time type (e.g., var x = null; is invalid without more context).

When to use it:

  • The type is obvious from the RHS (var x = new Dictionary<string,int>(); can be obvious, too).
  • The type is anonymous.
  • The explicit type would reduce readability (very long generic types) and the variable name carries meaning.

When to avoid it:

  • When the type itself is important information (e.g., distinguishing IQueryable<T> vs IEnumerable<T> at a boundary).

3.2.3 Implicitly typed arrays

Useful when the compiler can infer a single element type:

var numbers = new[] { 1, 2, 3 }; // inferred as int[]

Interview trap:

  • Mixed numeric literals can change the inferred element type (e.g., new[] { 1, 2L } → long[]).

3.3 Object and collection initializers

3.3.1 Why they matter

Initializers let you build objects/collections in a single expression, which:

  • Reduces “set this then set that” noise.
  • Makes projection code (LINQ results) much cleaner.

3.3.2 Object initializers

var p = new Person { FirstName = "Ada", LastName = "Lovelace" };

Interview nuance:

  • This is still “construction + assignments” (not constructor parameters), so it doesn’t enforce invariants the way constructors do.

3.3.3 Collection initializers

var list = new List<int> { 1, 2, 3 };

3.4 Anonymous types

Anonymous types are a lightweight way to create “shape-only” results, common in projections:

var summary = new { Name = p.Name, p.Age };

3.4.1 Syntax and behavior

Key behaviors to be fluent with:

  • Properties are read-only (effectively immutable from the consumer side).
  • Equality is structural (based on property values) for the generated type.

3.4.2 Compiler-generated type

Practical takeaway:

  • The actual runtime type exists but isn’t easily nameable; this is a major reason var is used in LINQ projections.

3.4.3 Limitations

  • Best used inside methods/queries; don’t try to leak them across public APIs.
  • If you need a stable contract, define a DTO/record instead.

3.5 Lambda expressions

Lambdas are an inline way to express behavior:

Func<int, int> square = x => x * x;

3.5.1 Syntax

  • Expression lambdas: x => x * 2
  • Statement lambdas: x => { var y = x * 2; return y; }

3.5.2 Capturing variables (closures)

Closures capture variables, not values. Interview-relevant pitfalls:

  • Capturing loop variables incorrectly (classic foreach/for issues in older versions).
  • Capturing a mutable variable shared across async continuations.

Rule of thumb:

  • Prefer creating a local copy inside the loop if you’re capturing it (or use modern C# behavior consciously).

3.5.3 Expression trees

Lambdas can be represented as:

  • Delegates (executable code)
  • Expression trees (data describing the code)

Why it matters:

  • LINQ providers (e.g., EF) can translate expression trees to SQL instead of executing them in memory.

Interview trap:

  • “Why did my query run client-side?” → often because you converted to IEnumerable<T> too early or used a method the provider can’t translate.

3.6 Extension methods

Extension methods let you “add” methods to existing types without modifying them.

3.6.1 Declaring an extension method

public static class StringExtensions
{
    public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
}

3.6.2 Invoking extension methods

Looks like an instance call:

if (input.IsBlank()) { /* ... */ }

3.6.3 Chaining calls (the LINQ style)

LINQ is largely “extension method pipelines”:

var top = products
    .Where(p => p.StockCount > 0)
    .OrderByDescending(p => p.Price)
    .Select(p => new { p.Name, p.Price });

Interview nuance:

  • Extension methods are resolved at compile time. If you accidentally use Enumerable. vs Queryable., you can change execution location (in-memory vs provider).

3.7 Query expressions (query syntax)

Query expressions are syntax sugar over method calls. They translate to calls like Where, Select, SelectMany, Join, etc.

3.7.1 Query expressions translate from C# to C#

Practical takeaway:

  • Query syntax and method syntax are equivalent; choose whichever is clearer for the specific query.

3.7.2 Range variables and transparent identifiers

When you chain multiple from/join clauses, the compiler may synthesize intermediate shapes to carry state forward.

Interview angle:

  • Understanding that translation exists helps debug odd-looking types and projection behavior.

3.8 The end result: LINQ

One query can involve multiple features at once:

  • var (because the result type might be anonymous)
  • anonymous types (projection)
  • query syntax (readability)
  • lambdas + extension methods (translation target)
  • expression trees (provider translation for IQueryable<T>)

Key interview competency:

  • Explain where code executes (database/provider vs in-memory) and why.