โ† All C# lessons

C# / Chapter 05

Interfaces, Generics & LINQ

Design abstractions with interfaces, write reusable generic code, and query in-memory data with expressive LINQ pipelines.

Chapter roadmap

Design abstractions with interfaces, write reusable generic code, and query in-memory data with expressive LINQ pipelines. Work through the concepts in order, type every example yourself, and keep a short debugging log that records predictions, errors, fixes, and what each fix taught you.

Learning objectives

  • Define focused interfaces
  • Use generic types and constraints
  • Compose LINQ operations
  • Understand deferred execution

05.1

Interfaces as contracts

An interface names a capability without choosing one implementation. Keep interfaces focused so consumers depend only on operations they use. Multiple implementations can serve production, testing, or alternate storage without changing the calling logic.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
public interface IRepository<T>
{
    T? FindById(int id);
    IReadOnlyList<T> GetAll();
    void Add(T item);
}
Result
Any repository implementation must provide lookup, listing, and add behavior.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

05.2

Generic types and constraints

Generics preserve type safety while sharing an algorithm or data structure across types. Constraints state required capabilities such as a base type, interface, reference type, value type, or parameterless constructor. Add constraints only when the implementation actually uses them.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
static T MaxBy<T, TKey>(IEnumerable<T> items, Func<T, TKey> selector)
    where TKey : IComparable<TKey>
{
    return items.MaxBy(selector)
        ?? throw new InvalidOperationException("Sequence is empty");
}
Result
The same method can select the greatest student, product, or event by a comparable key.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

05.3

LINQ filtering and projection

LINQ methods such as Where, Select, OrderBy, and GroupBy form readable pipelines over IEnumerable<T>. Each step returns a new sequence description. Name lambda parameters clearly and split a long pipeline into meaningful intermediate queries.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
var honorNames = students
    .Where(student => student.Average >= 90)
    .OrderByDescending(student => student.Average)
    .Select(student => student.Name)
    .ToList();
Result
A materialized list of honor students ordered by average.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

05.4

Deferred execution and aggregation

Most LINQ sequence operators use deferred execution: the source is read when the result is enumerated. ToList materializes a snapshot. Aggregates such as Count, Sum, Average, Any, and All execute immediately and require a deliberate policy for empty sequences.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
IEnumerable<int> passing = scores.Where(score => score >= 70);
scores.Add(95);
Console.WriteLine(passing.Count());
List<int> snapshot = passing.ToList();
Result
The deferred query sees later source changes; the list snapshot does not.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

Chapter assignments

Complete these in order. Later assignments assume that earlier skills are working. Do not only test the sample values; design tests that challenge boundaries, missing values, incorrect types, empty collections, and other likely failures.

  1. A5.1

    Define a focused interface with two implementations and a fake used by a test or demonstration.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  2. A5.2

    Write generic utilities for swapping, selecting bounds, and grouping items with appropriate constraints.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  3. A5.3

    Create LINQ reports using Where, Select, OrderBy, GroupBy, and aggregate operators over a realistic dataset.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  4. A5.4

    Demonstrate deferred execution versus materialization and explain when a stable snapshot is required.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

Chapter project

Inventory Reporting Service

Inventory Reporting Service โ€” Depend on an inventory repository interface, query products with LINQ, group by category, calculate stock value, identify reorder needs, and expose immutable report records. Provide both in-memory and file-style repository implementations.

Required process

  1. Write a short specification listing inputs, outputs, rules, and failure cases.
  2. Break the work into small functions, queries, modules, or classes appropriate to the language.
  3. Build the smallest working version before adding optional features.
  4. Test normal, boundary, empty, and invalid cases and record the results.
  5. Refactor names and duplication, then write a concise user guide.

Key terms

  • interface
  • contract
  • generic
  • type parameter
  • constraint
  • LINQ
  • lambda
  • deferred execution
  • materialization