async/await and why every Web API method uses it| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | Enums | Theory + Demo | 20 min |
| 0:20 | Generics | Theory + Demo | 25 min |
| 0:45 | Collections | Theory + Demo | 25 min |
| 1:10 | Break | — | 10 min |
| 1:20 | Extension methods | Theory + Demo | 15 min |
| 1:35 | Delegates & lambdas | Theory + Demo | 25 min |
| 2:00 | async/await | Theory + Demo | 20 min |
| 2:20 | Lab | Lab | 10 min |
public enum TaskStatus
{
Pending, // = 0
InProgress, // = 1
Completed, // = 2
Cancelled // = 3
}
TaskStatus status = TaskStatus.InProgress;
if (status == TaskStatus.Completed)
{
Console.WriteLine("Done!");
}
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"
};
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
Enum.Parse throws if the string does not match. Prefer TryParse when input comes from users or external systems.
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
Without generics:
static int[] DoubleInts(int[] items)
{ ... }
static string[] DoubleStrings(string[] items)
{ ... }
// one method per type — not scalable
objectWith 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" });
T is resolved at compile timeBox<int> and Box<string> are distinct typespublic 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");
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();
}
| Constraint | Meaning |
|---|---|
where T : class | T must be a reference type |
where T : struct | T must be a value type |
where T : new() | T must have a public parameterless constructor |
where T : ISomeInterface | T must implement that interface |
List<T>Dictionary<TKey, TValue>HashSet<T>IEnumerable<T>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();
TryGetValue to avoid exceptions on missing keysvar 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}");
}
scores["Unknown"] throws KeyNotFoundException. Always use TryGetValue or ContainsKey first.
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" });
| Type | Use 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 |
IEnumerable<T> as method parameter types — it keeps callers free to pass any collection type.
static classthis TypeName name.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..."
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
Use these instead of declaring your own delegates for common signatures.
| Type | Signature | Use |
|---|---|---|
Action | void () | 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 |
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));
Task<T> and the async/await patternTask<T> represents an operation that will complete in the future and return a value of type Tawait must be marked asyncawait suspends the method without blocking the threadTask 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);
| Rule | Why |
|---|---|
Mark a method async if it uses await | Compiler requirement |
Return Task or Task<T>, not void | async 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 suffix | Convention signals callers to use await |
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);
}
async Task<IActionResult> is the standard return type for controller actions that perform I/O.
Using the course's Task Management domain:
TaskStatus enum with values: Pending, InProgress, Completed, Cancelled.Repository<T> with a private List<T>, methods Add(T item), GetAll() returning IEnumerable<T>, and a Count property.FilterByPredicate<T> on IEnumerable<T> that returns all items matching a Predicate<T>.Func<TaskItem, string> that formats a task as a one-line summary. Apply it with Select to a list and print all results.[Flags] for bitmask valuesT can beList<T> for ordered data, Dictionary for key lookup, HashSet for membership, IEnumerable<T> as method parametersFunc, Action, Predicate cover most needsasync and return Task<T> — never block with .Result or .Wait()async Task<IActionResult> — exactly what you learned todayBuild a small in-memory task tracker that applies the features from this session:
Priority enum: Low, Medium, High, CriticalRepository<T> that stores items in a List<T> and exposes Add, GetAll, and FindAll(Predicate<T>)IEnumerable<TaskItem> — for example, filtering by status or sorting by priorityFunc<TaskItem, string> with Select to produce a formatted summary line for each task and print the resultsasync method that simulates a delay with Task.Delay and is called with await[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.