Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 14 additions & 10 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,36 @@

This repository is the attendee-facing material for Brandon Minnick's two-day workshop **"Become An Expert With Async Await in C#"**. Attendees clone it, read the READMEs, and run the samples on their own laptops. Optimize every change for the attendee experience and for the teaching goal, not for a production codebase.

Most attendees open the starter project and jump straight into the code without reading the rest of the README. Every challenge app therefore has to guide an attendee through the whole challenge by itself, through its workshop guide and the comments in the code, without sending them back to the README for instructions, clues, or hints.

## What lives where

- Numbered top-level folders are workshop sections and must stay in teaching order. Day 1 is asynchronous programming: `0. Prerequisites`, `1. Thread Switching and Compiler Generated Code` (slides only), `2. Correcting Common Async Await Mistakes`, `3. .NET Internals`, `4. Creating Custom Implementation of Task`. Day 2 is parallel programming: `5. Asynchronous vs Parallel Programming`, `6. Coordinating Multiple Tasks`, `7. Data Parallelism`, `8. Concurrent Collections`, `9. Channels`.
- Day 2 material was adapted from Brandon's Dometrain course "From Zero to Hero: Parallel Programming in C#". Dometrain attendees said that course's console samples were not realistic enough, so every Day 2 sample is a Blazor Server app built around a bug a working .NET developer would actually ship: `OrderPortal` (singleton services under concurrent load), `ProductDetails` (a page awaiting its backend services one at a time), `ImportPortal` (a batch job that is sequential and uses `Parallel.ForEach` with an `async` lambda), `StockWatch` (a live dashboard with unsynchronized collections), and `TelemetryPipeline` (a webhook handler doing slow work inline). Keep new samples in that register: a realistic app with a realistic mistake.
- Each hands-on section has a `README.md` (attendee-facing challenge) and a `SOLUTION.md` (step-by-step walkthrough the host presents after the challenge and group review). Sections 2 and 4 through 9 have `1. Start` (intentionally imperfect) and `2. Finish` (completed) projects. Section 4's `CoffeeShop` Blazor app differs between Start and Finish only in `CustomTask.cs` (stubs that throw `NotImplementedException` with hints) and its port; its `Steps` folder is the attendee's guide, and the app re-verifies Steps 1 to 5 against the attendee's `CustomTask` on startup.
- `3. .NET Internals/4. SynchronizationContext/HackerNews` is a copy of `2. Correcting Common Async Await Mistakes/2. Finish/HackerNews` plus the thread and `SynchronizationContext` log lines in `News.razor.cs`. Changes to one usually belong in the other.
- Section numbers are embedded in paths in the root `README.md`, `.github/workflows/build.yml`, `.github/dependabot.yml`, and the module READMEs. When renaming or reordering sections, update all of them and grep for stale numbers before finishing.
- Every challenge app uses the guided steps format that Section 4 introduced. In Sections 2 and 5 through 9 the realistic page (Top stories, Checkout, Product, Import, Dashboard, Ingest) is the app itself at `/`, and `Components/Layout/WorkshopGuide.razor` is docked beside it on every screen: a Your challenge tab, one tab per step, progress, and the step's results and hints. Attendees missed a separate steps page behind a top-bar link, so never move the guide back behind navigation. Each of those apps has its own visual identity and an in-world guide (HackerNews a pull request code review, OrderPortal a launch checklist), while the guide's structure and labels stay the same everywhere. Sections 3 and 4 have no separate realistic page, so their apps open on a steps overview (InternalsLab's Workshop steps page, CoffeeShop's Home page) with a page per step; CoffeeShop's checks show expected results without per-check hints, and InternalsLab's steps are investigations. In Sections 2 and 5 through 9, each `Steps/Step*.cs` file is one bug or concept with a story, how to see it in the app, where to look, a task list, clues, and checks that run the attendee's real code and attach a hint to every expected result. In Sections 2 and 5 through 9, `Verification/StepVerifier.cs` checks every step on startup, in order, stops at the first failure, and logs each failed hint to the terminal; CoffeeShop does the same for Steps 1 to 5, and InternalsLab records progress in its `LabNotebook` instead. A step unlocks only after the step before it passes. In Sections 2 and 5 through 9, the `Steps` and `Verification` folders, the layout with its workshop guide, the realistic page's markup, and `app.css` are identical in Start and Finish.
- `3. .NET Internals` is one guided Blazor app, `InternalsLab`, with no Start or Finish. Its four steps (ThreadStatic, ExecutionContext, Principal, SynchronizationContext) are investigations: the attendee records predictions, runs the experiment, compares results, and answers multiple-choice explain questions, and a step passes once its experiment has run and every question is answered correctly. State lives in a singleton `LabNotebook` for as long as the app runs, and the `Steps` files hold the answers. The Principal experiment is an MVC controller with cookie sign-in inside the same app, because it has to observe a real ASP.NET Core request.
- Section numbers are embedded in paths in the root `README.md`, `0. Prerequisites/README.md`, and the module READMEs (`build.yml` and `dependabot.yml` build and scan from the repository root, so they name no section). When renaming or reordering sections, update all of them and grep for stale numbers before finishing.
- Every sample is a Blazor Server, ASP.NET Core MVC, or console app on .NET 10. No MAUI, no mobile workloads, no emulators: attendees only have the .NET SDK, an editor, and a browser.
- Samples must run offline. `8. Concurrent Collections` uses a simulated in-process market feed (`MarketDataService`) rather than a real quote API, so there is no API key to leak and no rate limit to hit in a room full of attendees.
- Samples must run offline. `8. Concurrent Collections` uses a simulated in-process market feed (`MarketDataService`) rather than a real quote API, so there is no API key to leak and no rate limit to hit in a room full of attendees. The HackerNews Top stories page is the one exception, and its workshop steps still run offline against a fake in-memory `IHackerNewsAPI`.

## Non-negotiable rules

1. **Code and walkthroughs stay in sync, in both directions.** Any change to sample code must be reflected in that section's `README.md` and `SOLUTION.md` (snippets and prose), and any walkthrough change must be reflected in the code. Check every duplicated copy: Start, Finish, and the SynchronizationContext copy.
2. **Never "fix" the Start projects.** Code marked `// ToDo Refactor` and the compiler warnings it produces (for example CS4014) *are* the challenge. Do not refactor it, suppress its warnings, or run analyzer-based formatting over it. The only permitted change is the element ordering described under "C# element ordering" below: moving a member, with its `// ToDo Refactor` comment attached, never changes what it does.
3. **Write for attendees, not the host.** READMEs must flow naturally into the challenge without revealing the answer. Solutions belong only in `SOLUTION.md` and the `2. Finish` folders.
4. **Discourage AI from solving the challenges.** Every challenge begins with a note whose first sentences are exactly: `> **Note:** Please avoid letting AI Agents solve the challenges for you. You're smart. You got this.` followed by what AI *may* be used for (understanding the code, clarifying concepts, interpreting errors).
1. **Code and walkthroughs stay in sync, in both directions.** Any change to sample code must be reflected in that section's `README.md` and `SOLUTION.md` (snippets and prose), and any walkthrough change must be reflected in the code. The app's step text (story, tasks, clues, and check hints) is part of the walkthrough too. Check every duplicated copy: Start and Finish.
2. **Never "fix" the Start projects.** Code marked `// ToDo Refactor (Step N)` and the compiler warnings it produces (for example CS4014) *are* the challenge. The step number names the workshop step that checks that line. Do not refactor it, suppress its warnings, or run analyzer-based formatting over it. The only permitted change is the element ordering described under "C# element ordering" below: moving a member, with its `// ToDo Refactor` comment attached, never changes what it does. Every step must fail on Start for its own reason and pass on Finish, reliably: amplify race conditions in the checks rather than hoping a single run catches them.
3. **Write for attendees, not the host.** READMEs and the in-app step text must flow naturally into the challenge without revealing the answer. Tasks and hints stay at the level of the README requirements; clues can be more direct because attendees open them on purpose. Solutions belong only in `SOLUTION.md` and the `2. Finish` folders.
4. **Discourage AI from solving the challenges.** Every challenge begins with a note whose first sentences are exactly: `> **Note:** Please avoid letting AI Agents solve the challenges for you. You're smart. You got this.` followed by what AI *may* be used for (understanding the code, clarifying concepts, interpreting errors). Every challenge app repeats it in its Your challenge panel.
5. **Keep it simple.** Make the smallest correct change. No null-forgiving operator (`!`), no `#pragma` suppressions, no abstractions for one-off work, and no committed helper tooling or scripts beyond `BuildAllSolutions.ps1`. If a bulk edit is needed, do the edit rather than adding automation to the repo.
6. **Style rules are warnings, not errors,** and live in `.editorconfig`. That includes the StyleCop element-ordering rules, which `Directory.Build.props` suppresses with `NoWarn` in every configuration except Release so attendees working in Debug never see them. Do not add build-blocking style enforcement that would interrupt attendees while they iterate; ordering is enforced by the Release builds in `BuildAllSolutions.ps1`, which attendees do not run.
6. **New style rules are warnings, not errors,** and live in `.editorconfig` (a few naming and correctness rules there are errors; do not add more). That includes the StyleCop element-ordering rules, which `Directory.Build.props` suppresses with `NoWarn` in every configuration except Release so attendees working in Debug never see them. Do not add build-blocking style enforcement that would interrupt attendees while they iterate; ordering is enforced by the Release builds in `BuildAllSolutions.ps1`, which attendees do not run.
7. **The Prerequisites README must stay foolproof.** It has to work on Windows and macOS, with or without admin rights, with Visual Studio, Rider, or VS Code, with Git blocked (ZIP download), and behind corporate networks that block Hacker News. Do not remove a path just because it looks redundant.

## Build and validate

- Full validation, required before finishing any code change: `pwsh ./BuildAllSolutions.ps1`. It runs `dotnet format whitespace` and then `dotnet build -c Release --no-incremental` for every `.slnx`, and it fails when any build reports a StyleCop element-ordering warning (`SA1201`, `SA1202`, `SA1204`, `SA1214`, `SA1215`). Keep the script on `dotnet format whitespace`: full `dotnet format` fails on the .NET 10 SDK and analyzer/code-style formatting would erase the intentional Start warnings.
- Single solution: `dotnet build "<section>/<folder>/<Name>.slnx"`.
- Run a web sample: `dotnet run --project "<path>/<Name>.csproj"`. Ports are fixed and referenced by the READMEs: PrincipalExample `5000`, HackerNews Start `5001` / Finish `5002`, SynchronizationContext HackerNews `5004`, StockWatch Start `5005` / Finish `5006`, OrderPortal Start `5007` / Finish `5008`, ProductDetails Start `5009` / Finish `5010`, ImportPortal Start `5011` / Finish `5012`, TelemetryPipeline Start `5013` / Finish `5014`, CoffeeShop Start `5015` / Finish `5016`.
- `Properties/launchSettings.json` is gitignored, but the HackerNews copies are force-tracked because `ASPNETCORE_ENVIRONMENT=Development` is required for `dotnet run` to serve `_framework/blazor.web.js`. Keep them tracked and keep the environment variable.
- Run a web sample: `dotnet run --project "<path>/<Name>.csproj"`. Ports are fixed and referenced by the READMEs: HackerNews Start `5001` / Finish `5002`, InternalsLab `5003`, StockWatch Start `5005` / Finish `5006`, OrderPortal Start `5007` / Finish `5008`, ProductDetails Start `5009` / Finish `5010`, ImportPortal Start `5011` / Finish `5012`, TelemetryPipeline Start `5013` / Finish `5014`, CoffeeShop Start `5015` / Finish `5016`.
- `Properties/launchSettings.json` is gitignored, but every web sample's copy is force-tracked because `ASPNETCORE_ENVIRONMENT=Development` is required for `dotnet run` to serve `_framework/blazor.web.js`. Keep them tracked and keep the environment variable.
- A step's checks create their own instances of the services they exercise, never the app's singletons, and must never crash the app: an unhandled exception on a thread pool thread, a timer callback, or an `async void` ends the process, so gate any check that could provoke one behind the step that fixes it. Bugs inside a page's `*.razor.cs` are checked by rendering a verifier-only subclass of the page with `HtmlRenderer`.

## Code conventions beyond .editorconfig

Expand Down
15 changes: 8 additions & 7 deletions 0. Prerequisites/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ dotnet run --project "2. Correcting Common Async Await Mistakes/2. Finish/Hacker

## 1. Install or Open an Editor

One of these three is required. A plain text editor is not enough: the .NET Internals section on Day 1 is spent setting breakpoints and inspecting threads, and none of that works from `dotnet run`.
One of these three is required. A plain text editor is not enough: the .NET Internals section on Day 1 invites you to set breakpoints and inspect threads, and we debug through its experiments together in the group review. None of that works from `dotnet run`.

* [Visual Studio 2026](https://visualstudio.microsoft.com/downloads/) (18.0 or later) on Windows. .NET 10 is not offered by Visual Studio 2022, so if that is what you have, use Visual Studio Code or Rider instead.
* [JetBrains Rider](https://www.jetbrains.com/rider/download/) (2025.3 or later) on Windows or macOS
* [Visual Studio Code](https://code.visualstudio.com/) with the current [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension. This is the smallest install of the three.

If your work laptop blocks all three, tell us before the workshop starts rather than on the morning. We can pair you with someone for the debugging exercises.
If your work laptop blocks all three, tell us before the workshop starts rather than on the morning of the workshop. We can pair you with someone for the debugging exercises.

## 2. Install .NET 10 SDK

Expand Down Expand Up @@ -164,7 +164,7 @@ Then run the Blazor sample:
dotnet run --project "2. Correcting Common Async Await Mistakes/2. Finish/HackerNews/HackerNews.csproj"
```

Open [http://localhost:5002](http://localhost:5002). Confirm that the **HackerNews** Blazor app loads and displays top stories.
Open [http://localhost:5002](http://localhost:5002). The **HackerNews** Blazor app opens on its **Top stories** page, with a workshop guide docked beside it. Confirm that the page displays top stories, and that the guide shows 5 of 5 steps pass once its checks finish, about 20 seconds after the app starts.

To stop the app, return to the terminal and press **Ctrl+C**.

Expand All @@ -181,7 +181,7 @@ Before continuing to the workshop exercises, prove your machine is ready:
3. Run `dotnet --version` and confirm it reports .NET 10.
4. Build the completed HackerNews sample from step 4.
5. Run the completed HackerNews sample from step 4.
6. Open the local URL and confirm that the app displays top stories.
6. Open the local URL and confirm that the app displays top stories, and that the workshop guide beside them shows 5 of 5 steps pass.
7. Write down any blocker, exact error message, operating system, editor, and install path you used.

If every check passes, you are ready for the workshop.
Expand All @@ -207,15 +207,16 @@ If `localhost:5002` does not load:
* If port `5002` is already in use, run:

```console
dotnet run --project "2. Correcting Common Async Await Mistakes/2. Finish/HackerNews/HackerNews.csproj" --urls http://localhost:5012
dotnet run --project "2. Correcting Common Async Await Mistakes/2. Finish/HackerNews/HackerNews.csproj" --urls http://localhost:5099
```

Then open [http://localhost:5012](http://localhost:5012).
Then open [http://localhost:5099](http://localhost:5099).

If your company blocks access to Hacker News:

* The app can still build.
* The live story list may show a refresh error until you are on a network that allows access to `https://hacker-news.firebaseio.com`.
* The workshop guide still shows 5 of 5 steps pass, because its checks use an in-memory copy of Hacker News instead of the real site.
* The live story list on **Top stories** may show a refresh error until you are on a network that allows access to `https://hacker-news.firebaseio.com`.

## 7. Maintainer Build Check

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@inherits LayoutComponentBase

<main class="shell">
@Body
</main>
<div class="workspace">
<main class="app-pane">
@Body
</main>

<WorkshopGuide />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@inherits StepBadgeBase

@if (IsCompact)
{
<span class="badge badge-compact @CssClass" title="@Label"><span aria-hidden="true">@Icon</span><span class="visually-hidden">@Label</span></span>
}
else
{
<span class="badge @CssClass">@Icon @Label</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Components;

namespace HackerNews.Components.Layout;

public partial class StepBadgeBase : ComponentBase
{
[Parameter, EditorRequired]
public StepStatus Status { get; set; }

[Parameter]
public bool IsUnlocked { get; set; } = true;

// Only the icon, for the collapsed guide
[Parameter]
public bool IsCompact { get; set; }

public string Icon => Status is StepStatus.Running ? "⏳" : !IsUnlocked ? "🔒" : Status switch
{
StepStatus.Passed => "✅",
StepStatus.Failed or StepStatus.TimedOut or StepStatus.Crashed => "❌",
_ => "•",
};

public string Label => Status is StepStatus.Running ? "Running" : !IsUnlocked ? "Locked" : Status switch
{
StepStatus.Passed => "Passed",
StepStatus.Failed => "Failed",
StepStatus.TimedOut => "Timed out",
StepStatus.Crashed => "Crashed",
StepStatus.Stopped => "Stopped",
_ => "Not run",
};

public string CssClass => Status is StepStatus.Running ? "badge-running" : !IsUnlocked ? "badge-locked" : Status switch
{
StepStatus.Passed => "badge-passed",
StepStatus.Failed or StepStatus.TimedOut or StepStatus.Crashed => "badge-failed",
_ => "badge-idle",
};
}
Loading