Backend Development with .NET
Session 07
C# Language Features
Enums, Generics, Collections & Delegates
Eng. Seif Mansour  ·  Andalusia Academy
Week 4  ·  2.5 hours
Session Goals
  • Use enums to represent fixed sets of named values
  • Write and use generic classes and methods with type constraints
  • Choose and use the correct collection type for a given problem
  • Write extension methods, delegates, and lambda expressions
  • Understand async/await and why every Web API method uses it
Agenda
TimeSegmentTypeDuration
0:00EnumsTheory + Demo20 min
0:20GenericsTheory + Demo25 min
0:45CollectionsTheory + Demo25 min
1:10Break10 min
1:20Extension methodsTheory + Demo15 min
1:35Delegates & lambdasTheory + Demo25 min
2:00async/awaitTheory + Demo20 min
2:20LabLab10 min
Enums
Named sets of integer constants — replace magic numbers and magic strings with readable, type-safe values.
In this section
  • Defining and using enums
  • Switch expressions on enums
  • Int and string conversions
  • Flags enums
Defining an Enum
  • An enum defines a named set of integer constants
  • Members are automatically numbered from 0 unless you override
  • Use enums instead of magic numbers or strings for fixed states
  • The compiler catches typos — invalid values won't compile
public enum TaskStatus
{
    Pending,       // = 0
    InProgress,    // = 1
    Completed,     // = 2
    Cancelled      // = 3
}

TaskStatus status = TaskStatus.InProgress;

if (status == TaskStatus.Completed)
{
    Console.WriteLine("Done!");
}
Switch Expression on an Enum

Pattern matching with switch expressions keeps enum-to-string mappings exhaustive and readable.

string label = status switch
{
    TaskStatus.Pending    => "Not started",
    TaskStatus.InProgress => "In progress",
    TaskStatus.Completed  => "Done",
    TaskStatus.Cancelled  => "Cancelled",
    _                     => "Unknown"
};
Tip
The compiler warns if you miss a case — this is one of the main benefits of enums over plain strings.
Enum Conversions

To/from int:

int value = (int)TaskStatus.InProgress;
// value = 1

TaskStatus s = (TaskStatus)2;
// s = Completed

To/from string:

TaskStatus parsed =
    Enum.Parse<TaskStatus>("Completed");

bool ok = Enum.TryParse<TaskStatus>(
    "Invalid",
    out TaskStatus result);
// ok = false — no exception thrown
Warning
Enum.Parse throws if the string does not match. Prefer TryParse when input comes from users or external systems.
Flags Enum

Add [Flags] to combine multiple values with bitwise OR. Each member must be a distinct power of 2.

[Flags]
public enum Permissions
{
    None   = 0,
    Read   = 1,
    Write  = 2,
    Delete = 4,
    Admin  = Read | Write | Delete
}

Permissions userPerms = Permissions.Read | Permissions.Write;
bool canWrite = userPerms.HasFlag(Permissions.Write);   // true
Generics
Write type-safe code once — let the compiler apply it to any type without boxing or duplication.
In this section
  • Why generics exist
  • Generic methods and classes
  • Type constraints
The Problem Generics Solve

Without generics:

static int[] DoubleInts(int[] items)
    { ... }
static string[] DoubleStrings(string[] items)
    { ... }
// one method per type — not scalable
  • Code duplication
  • No compile-time type safety with object
  • Boxing overhead for value types

With generics:

static T[] Duplicate<T>(T[] items)
{
    T[] result = new T[items.Length * 2];
    items.CopyTo(result, 0);
    items.CopyTo(result, items.Length);
    return result;
}

int[] doubled =
    Duplicate(new[] { 1, 2, 3 });
string[] names =
    Duplicate(new[] { "Alice", "Bob" });
Generic Class
  • The type parameter T is resolved at compile time
  • Box<int> and Box<string> are distinct types
  • No casting or boxing — fully type-safe
public class Box<T>
{
    public T Value { get; private set; }

    public Box(T value) => Value = value;

    public override string ToString()
        => $"Box<{typeof(T).Name}>({Value})";
}

var intBox = new Box<int>(42);
var strBox = new Box<string>("hello");
Type Constraints

Constraints tell the compiler what capabilities T must have, so you can call methods on it safely.

// T must implement IComparable<T>
static T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

static T CreateInstance<T>() where T : class, new()
{
    return new T();
}
ConstraintMeaning
where T : classT must be a reference type
where T : structT must be a value type
where T : new()T must have a public parameterless constructor
where T : ISomeInterfaceT must implement that interface
Collections
Generic containers for dynamic data — choose the right one to get the right performance characteristics.
In this section
  • List<T>
  • Dictionary<TKey, TValue>
  • HashSet<T>
  • IEnumerable<T>
  • Choosing the right type
List<T> — Ordered, Resizable
  • Elements have a defined order
  • Access by index in O(1)
  • Append at the end in amortized O(1)
  • Insert/remove in the middle is O(n)
var fruits = new List<string>
    { "apple", "banana" };

fruits.Add("cherry");
fruits.Insert(1, "avocado");
fruits.Remove("banana");
fruits.RemoveAt(0);

Console.WriteLine(fruits.Count);
Console.WriteLine(fruits.Contains("cherry"));
Console.WriteLine(fruits[0]);

fruits.Sort();
Dictionary<TKey, TValue> — Key-Value Lookup
  • Lookup by key is O(1) on average
  • Keys must be unique
  • Use TryGetValue to avoid exceptions on missing keys
var scores = new Dictionary<string, int>
{
    { "Alice", 95 },
    { "Bob", 82 }
};

scores["Carol"] = 91;

scores.TryGetValue("Bob", out int bobScore);

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}
Warning
Accessing a missing key with scores["Unknown"] throws KeyNotFoundException. Always use TryGetValue or ContainsKey first.
HashSet<T> & IEnumerable<T>

HashSet<T> — unique membership

var tags = new HashSet<string>
    { "csharp", "dotnet" };
tags.Add("csharp"); // no duplicate added
bool has = tags.Contains("dotnet"); // true

IEnumerable<T> — accept any collection

static void PrintAll(
    IEnumerable<string> items)
{
    foreach (var item in items)
        Console.WriteLine(item);
}

// works with List, array, HashSet...
PrintAll(new List<string> { "a", "b" });
PrintAll(new string[] { "x", "y" });
Choosing the Right Collection
TypeUse when
List<T>Ordered list; need index access or frequent add/remove
Dictionary<K,V>Fast lookup by a unique key
HashSet<T>Only care about membership; no duplicates allowed
IEnumerable<T>Parameter type when you only need to iterate
Tip
Prefer IEnumerable<T> as method parameter types — it keeps callers free to pass any collection type.
Extension Methods, Delegates & Lambdas
Pass behaviour as data — the foundation of LINQ and functional-style C# programming.
In this section
  • Extension methods
  • Delegates and built-in types
  • Lambda expressions
  • Delegates with collections (LINQ preview)
Extension Methods
  • Add methods to existing types without subclassing
  • Must live in a static class
  • First parameter uses this TypeName name
  • Called like an instance method on that type
Did you know?
Every LINQ method — .Where(), .Select(), .FirstOrDefault() — is an extension method on IEnumerable<T>.
public static class StringExtensions
{
    public static bool IsNullOrEmpty(this string value)
        => string.IsNullOrEmpty(value);

    public static string Truncate(
        this string value, int maxLength)
    {
        if (value.Length <= maxLength) return value;
        return value[..maxLength] + "...";
    }
}

string title = "This is a very long title";
Console.WriteLine(title.Truncate(20));
// "This is a very long..."
Delegates

A delegate is a type that holds a reference to a method — it lets you pass behaviour as a parameter.

delegate int MathOperation(int a, int b);

static int Add(int a, int b)      => a + b;
static int Multiply(int a, int b) => a * b;

MathOperation op = Add;
Console.WriteLine(op(3, 4));   // 7

op = Multiply;
Console.WriteLine(op(3, 4));   // 12
Built-in Delegate Types

Use these instead of declaring your own delegates for common signatures.

TypeSignatureUse
Actionvoid ()No return value, no parameters
Action<T>void (T)No return value, one parameter
Func<TResult>TResult ()Returns a value, no parameters
Func<T, TResult>TResult (T)Returns a value, one parameter
Predicate<T>bool (T)Condition check on a value
Lambda Expressions

Anonymous methods written inline — the most common way to use delegates in practice.

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

Action<string> print =
    msg => Console.WriteLine(msg);

Predicate<int> isEven =
    n => n % 2 == 0;

Console.WriteLine(square(5));  // 25
print("Hello");
Console.WriteLine(isEven(4));  // true
var numbers = new List<int>
    { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var evens   = numbers.Where(n => n % 2 == 0);
var squares = numbers.Select(n => n * n);
int? first  = numbers.FirstOrDefault(n => n > 5);

var words = new List<string>
    { "banana", "apple", "kiwi" };
words.Sort((a, b) =>
    a.Length.CompareTo(b.Length));
async / await
Non-blocking I/O for Web APIs — release the thread while waiting, handle more requests with fewer resources.
In this section
  • Why async matters in Web APIs
  • Task<T> and the async/await pattern
  • Rules and common mistakes
  • Preview: async controller actions
Why async Matters in Web APIs
  • Almost every Web API operation involves I/O — reading from a database, calling an external service, reading a file
  • With synchronous code the thread blocks and sits idle while waiting for I/O to complete
  • With async code the thread is released back to the thread pool during the wait — it can serve other requests
  • Result: the same server handles more concurrent requests with fewer threads
Key Idea
Every controller action and service method in the course will be async. This is not optional in ASP.NET Core — it is the expected pattern.
Task<T> and await
  • Task<T> represents an operation that will complete in the future and return a value of type T
  • A method that uses await must be marked async
  • await suspends the method without blocking the thread
  • Async methods return Task or Task<T>
public async Task<string> FetchDataAsync()
{
    await Task.Delay(1000);   // simulates slow I/O
    return "Data loaded";
}

// Calling it
string result = await FetchDataAsync();
Console.WriteLine(result);
async/await — Rules & Pitfalls
RuleWhy
Mark a method async if it uses awaitCompiler requirement
Return Task or Task<T>, not voidasync void exceptions cannot be caught by callers
Never call .Result or .Wait()Blocks the thread; causes deadlocks in ASP.NET Core
Name async methods with Async suffixConvention signals callers to use await
Controllers/TasksController.cs
Async Controller Action (Preview)

This is the pattern every controller in the course will follow starting in Session 08.

[HttpGet("{id}")]
public async Task<IActionResult> GetTask(int id)
{
    var task = await _taskService.GetByIdAsync(id);
    if (task == null) return NotFound();
    return Ok(task);
}
Pattern
async Task<IActionResult> is the standard return type for controller actions that perform I/O.
Lab

Using the course's Task Management domain:

  1. Define a TaskStatus enum with values: Pending, InProgress, Completed, Cancelled.
  2. Create a generic class Repository<T> with a private List<T>, methods Add(T item), GetAll() returning IEnumerable<T>, and a Count property.
  3. Add an extension method FilterByPredicate<T> on IEnumerable<T> that returns all items matching a Predicate<T>.
  4. Write a Func<TaskItem, string> that formats a task as a one-line summary. Apply it with Select to a list and print all results.
Summary
  • Enums replace magic numbers and strings with named, type-safe constants — use [Flags] for bitmask values
  • Generics let you write one implementation that works for any type; constraints narrow what T can be
  • Use List<T> for ordered data, Dictionary for key lookup, HashSet for membership, IEnumerable<T> as method parameters
  • Extension methods add behaviour to existing types without modifying them — they power LINQ
  • Delegates and lambdas let you pass behaviour as data; built-in types Func, Action, Predicate cover most needs
  • Every Web API method should be async and return Task<T> — never block with .Result or .Wait()
What's Next
  • Session 08 — First .NET Web API & Project Architecture
  • We will create a real ASP.NET Core Web API project from scratch
  • Controllers, routing, and the project structure you'll use for the rest of the course
  • Every action method will be async Task<IActionResult> — exactly what you learned today
Before Next Session
Make sure .NET SDK 8 is installed and Visual Studio / VS Code is set up — you will need it from Session 08 onward.
Assignment

Build a small in-memory task tracker that applies the features from this session:

  • Define a Priority enum: Low, Medium, High, Critical
  • Create a generic Repository<T> that stores items in a List<T> and exposes Add, GetAll, and FindAll(Predicate<T>)
  • Add at least two extension methods on IEnumerable<TaskItem> — for example, filtering by status or sorting by priority
  • Use a Func<TaskItem, string> with Select to produce a formatted summary line for each task and print the results
  • Wrap at least one operation in an async method that simulates a delay with Task.Delay and is called with await
Bonus
Add a [Flags] enum Tag (e.g., Bug, Feature, Urgent) and let each task hold combined tags. Implement a filter that returns tasks matching any of a given set of tags using HasFlag.
Questions?
Session 07 — C# Language Features: Enums, Generics, Collections & Delegates