Follow-up to spikersoft-issues#252 (PR spikersoft-angular#75). That PR stops locally-graded C# passes from reverting on reload, but they remain unverified server-side: no skills awarded, no cross-device sync. This ticket tracks making them server-verifiable.
Why C# is different today
For the other browser-graded runtimes (CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp), the SPA fetches each attempt from GET /Lessons/{n}/attempt while online. GetLessonAttemptQueryHandler persists the offline grading state to the Mongo offline-attempt store (30-day TTL) and returns the BrowserGradingState to the client. On flush, POST /Lessons/progress/batch finds that state by token, re-grades via the worker, and records a verified pass.
C# never does this. In In-Browser Compile mode the SPA generates the attempt entirely client-side in the SpikerSoft.Wasm worker, so the server never sees the token or state — every flushed C# pass is rejected state_expired.
Key enabler (confirmed)
SpikerSoft.Wasm builds the sameLessonStrategyRegistry and uses the sameRoslynCodeExecutor as the server (WasmCompilerEntry.cs). A server-issued C# attempt's LessonAttemptState is therefore byte-for-byte gradable by the WASM worker, and a server-issued token's state is re-gradable by the regrade worker — which already supports RoslynCSharp (LessonRegradeWorkerHostedService / LanguageForRuntime). So no grader changes are needed; only the attempt-issuance + routing wiring.
Plan
Backend (this ticket / first PR)
Add an opt-in browserGrading flag to GET /Lessons/{n}/attempt (threaded into GetLessonAttemptQuery).
In GetLessonAttemptQueryHandler, treat RoslynCSharp as browser-grading-capable only when the flag is set, so server-grading-mode C# students keep TestCode server-side and don't incur extra Mongo writes. The always-local runtimes stay unconditional (unchanged).
When enabled, persist the offline state and return BrowserGradingState, exactly like the other runtimes.
Tests for both the flagged and unflagged C# paths.
Frontend (separate follow-up PR, required to close the loop)
In csharp-runner.service.tsgetAttempt: when online + In-Browser Compile, fetch GET /Lessons/{n}/attempt?browserGrading=true, store the returned browserGradingState keyed by the server-issued token, grade locally via WASM with that state, and submit/flush with that token.
Keep the fully-local WASM attempt path as the offline fallback (those stay unverified but, per #252, survive reload).
Net result: an online C# In-Browser Compile pass flushes a server-issued token → regrade worker verifies → skills awarded + cross-device sync.
Notes / out of scope
C# tutorial completion in local mode has a parallel gap (the complete-tutorial endpoint's offline-store allowlist also omits RoslynCSharp); fold in once the graded-lesson path lands.
Locale: WASM GetAttempt is English-only, but grading is locale-independent (TestCode), so a server-issued localized C# attempt still grades correctly in-browser.
Follow-up to spikersoft-issues#252 (PR spikersoft-angular#75). That PR stops locally-graded C# passes from reverting on reload, but they remain **`unverified`** server-side: no skills awarded, no cross-device sync. This ticket tracks making them server-verifiable.
## Why C# is different today
For the other browser-graded runtimes (CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp), the SPA fetches each attempt from `GET /Lessons/{n}/attempt` while online. `GetLessonAttemptQueryHandler` persists the offline grading state to the Mongo offline-attempt store (30-day TTL) and returns the `BrowserGradingState` to the client. On flush, `POST /Lessons/progress/batch` finds that state by token, re-grades via the worker, and records a **verified** pass.
C# never does this. In In-Browser Compile mode the SPA generates the attempt **entirely client-side** in the `SpikerSoft.Wasm` worker, so the server never sees the token or state — every flushed C# pass is rejected `state_expired`.
## Key enabler (confirmed)
`SpikerSoft.Wasm` builds the **same** `LessonStrategyRegistry` and uses the **same** `RoslynCodeExecutor` as the server (`WasmCompilerEntry.cs`). A server-issued C# attempt's `LessonAttemptState` is therefore byte-for-byte gradable by the WASM worker, and a server-issued token's state is re-gradable by the regrade worker — which **already supports `RoslynCSharp`** (`LessonRegradeWorkerHostedService` / `LanguageForRuntime`). So no grader changes are needed; only the attempt-issuance + routing wiring.
## Plan
### Backend (this ticket / first PR)
- Add an opt-in `browserGrading` flag to `GET /Lessons/{n}/attempt` (threaded into `GetLessonAttemptQuery`).
- In `GetLessonAttemptQueryHandler`, treat `RoslynCSharp` as browser-grading-capable **only when the flag is set**, so server-grading-mode C# students keep TestCode server-side and don't incur extra Mongo writes. The always-local runtimes stay unconditional (unchanged).
- When enabled, persist the offline state and return `BrowserGradingState`, exactly like the other runtimes.
- Tests for both the flagged and unflagged C# paths.
### Frontend (separate follow-up PR, required to close the loop)
- In `csharp-runner.service.ts` `getAttempt`: when **online + In-Browser Compile**, fetch `GET /Lessons/{n}/attempt?browserGrading=true`, store the returned `browserGradingState` keyed by the **server-issued** token, grade locally via WASM with that state, and submit/flush with that token.
- Keep the fully-local WASM attempt path as the **offline** fallback (those stay unverified but, per #252, survive reload).
- Net result: an online C# In-Browser Compile pass flushes a server-issued token → regrade worker verifies → skills awarded + cross-device sync.
## Notes / out of scope
- C# **tutorial** completion in local mode has a parallel gap (the `complete-tutorial` endpoint's offline-store allowlist also omits `RoslynCSharp`); fold in once the graded-lesson path lands.
- Locale: WASM `GetAttempt` is English-only, but grading is locale-independent (TestCode), so a server-issued localized C# attempt still grades correctly in-browser.
Correction: reopening. Backend PR #17 landed the backend half (opt-in browserGrading flag on GET /Lessons/{n}/attempt), but the frontend follow-up this ticket lists as "required to close the loop" is not merged. On spikersoft-angularorigin/master, csharp-runner.service.tsgetAttempt still issues the C# attempt entirely client-side in the WASM path and, on the online path, calls GET /Lessons/{n}/attemptwithoutbrowserGrading=true. The only browserGrading reference in that file is the browserGradingFailed error string.
Until the SPA fetches the server-issued attempt (?browserGrading=true), stores the returned browserGradingState keyed by the server token, and flushes with that token, C# In-Browser Compile passes stay unverified (no skills awarded, no cross-device sync). Reopening to track that frontend wiring (now in progress).
Correction: reopening. Backend PR #17 landed the backend half (opt-in `browserGrading` flag on `GET /Lessons/{n}/attempt`), but the **frontend follow-up this ticket lists as "required to close the loop" is not merged**. On `spikersoft-angular` `origin/master`, `csharp-runner.service.ts` `getAttempt` still issues the C# attempt entirely client-side in the WASM path and, on the online path, calls `GET /Lessons/{n}/attempt` **without** `browserGrading=true`. The only `browserGrading` reference in that file is the `browserGradingFailed` error string.
Until the SPA fetches the server-issued attempt (`?browserGrading=true`), stores the returned `browserGradingState` keyed by the server token, and flushes with that token, C# In-Browser Compile passes stay `unverified` (no skills awarded, no cross-device sync). Reopening to track that frontend wiring (now in progress).
Frontend wiring opened: spikersoft-angular PR #77. The C# runner now fetches GET /Lessons/{n}/attempt?browserGrading=true when online + In-Browser Compile, grades the server-issued state in WASM, and flushes with the server token so the regrade worker verifies the pass (skills + cross-device sync). Offline still falls back to the fully-local attempt (#252). Will close this ticket once PR #77 is merged.
Frontend wiring opened: spikersoft-angular PR #77. The C# runner now fetches `GET /Lessons/{n}/attempt?browserGrading=true` when online + In-Browser Compile, grades the server-issued state in WASM, and flushes with the server token so the regrade worker verifies the pass (skills + cross-device sync). Offline still falls back to the fully-local attempt (#252). Will close this ticket once PR #77 is merged.
Backend: spikersoft-backend PR #17 — opt-in browserGrading=true on GET /Lessons/{n}/attempt, persists the C# attempt state under the issued token.
Frontend: spikersoft-angular PR #77 — the C# runner fetches that server-issued attempt when online + In-Browser Compile, grades the state in WASM, and flushes with the server token so the regrade worker verifies the pass (skills awarded + cross-device sync). Offline still falls back to the fully-local attempt (#252).
Closing.
Resolved. Both halves are now merged to `master`:
- Backend: spikersoft-backend PR #17 — opt-in `browserGrading=true` on `GET /Lessons/{n}/attempt`, persists the C# attempt state under the issued token.
- Frontend: spikersoft-angular PR #77 — the C# runner fetches that server-issued attempt when online + In-Browser Compile, grades the state in WASM, and flushes with the server token so the regrade worker verifies the pass (skills awarded + cross-device sync). Offline still falls back to the fully-local attempt (#252).
Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Follow-up to spikersoft-issues#252 (PR spikersoft-angular#75). That PR stops locally-graded C# passes from reverting on reload, but they remain
unverifiedserver-side: no skills awarded, no cross-device sync. This ticket tracks making them server-verifiable.Why C# is different today
For the other browser-graded runtimes (CPython, NodeJavaScript, RegexBrowser, BlinkX86, DuckDbSql, ClangC, ClangCpp), the SPA fetches each attempt from
GET /Lessons/{n}/attemptwhile online.GetLessonAttemptQueryHandlerpersists the offline grading state to the Mongo offline-attempt store (30-day TTL) and returns theBrowserGradingStateto the client. On flush,POST /Lessons/progress/batchfinds that state by token, re-grades via the worker, and records a verified pass.C# never does this. In In-Browser Compile mode the SPA generates the attempt entirely client-side in the
SpikerSoft.Wasmworker, so the server never sees the token or state — every flushed C# pass is rejectedstate_expired.Key enabler (confirmed)
SpikerSoft.Wasmbuilds the sameLessonStrategyRegistryand uses the sameRoslynCodeExecutoras the server (WasmCompilerEntry.cs). A server-issued C# attempt'sLessonAttemptStateis therefore byte-for-byte gradable by the WASM worker, and a server-issued token's state is re-gradable by the regrade worker — which already supportsRoslynCSharp(LessonRegradeWorkerHostedService/LanguageForRuntime). So no grader changes are needed; only the attempt-issuance + routing wiring.Plan
Backend (this ticket / first PR)
browserGradingflag toGET /Lessons/{n}/attempt(threaded intoGetLessonAttemptQuery).GetLessonAttemptQueryHandler, treatRoslynCSharpas browser-grading-capable only when the flag is set, so server-grading-mode C# students keep TestCode server-side and don't incur extra Mongo writes. The always-local runtimes stay unconditional (unchanged).BrowserGradingState, exactly like the other runtimes.Frontend (separate follow-up PR, required to close the loop)
csharp-runner.service.tsgetAttempt: when online + In-Browser Compile, fetchGET /Lessons/{n}/attempt?browserGrading=true, store the returnedbrowserGradingStatekeyed by the server-issued token, grade locally via WASM with that state, and submit/flush with that token.Notes / out of scope
complete-tutorialendpoint's offline-store allowlist also omitsRoslynCSharp); fold in once the graded-lesson path lands.GetAttemptis English-only, but grading is locale-independent (TestCode), so a server-issued localized C# attempt still grades correctly in-browser.Resolved in spikersoft-backend PR #17 (merged to
master): server-side verification of C# in-browser compile via opt-in browser-grading attempts (awards skills + enables cross-device sync). Closing.Correction: reopening. Backend PR #17 landed the backend half (opt-in
browserGradingflag onGET /Lessons/{n}/attempt), but the frontend follow-up this ticket lists as "required to close the loop" is not merged. Onspikersoft-angularorigin/master,csharp-runner.service.tsgetAttemptstill issues the C# attempt entirely client-side in the WASM path and, on the online path, callsGET /Lessons/{n}/attemptwithoutbrowserGrading=true. The onlybrowserGradingreference in that file is thebrowserGradingFailederror string.Until the SPA fetches the server-issued attempt (
?browserGrading=true), stores the returnedbrowserGradingStatekeyed by the server token, and flushes with that token, C# In-Browser Compile passes stayunverified(no skills awarded, no cross-device sync). Reopening to track that frontend wiring (now in progress).Frontend wiring opened: spikersoft-angular PR #77. The C# runner now fetches
GET /Lessons/{n}/attempt?browserGrading=truewhen online + In-Browser Compile, grades the server-issued state in WASM, and flushes with the server token so the regrade worker verifies the pass (skills + cross-device sync). Offline still falls back to the fully-local attempt (#252). Will close this ticket once PR #77 is merged.Resolved. Both halves are now merged to
master:browserGrading=trueonGET /Lessons/{n}/attempt, persists the C# attempt state under the issued token.Closing.